Instructions
Instructions
Instructions are first-class platform entities that let a product module schedule a function to execute at a specific date and time in the future.
Instructions are the preferred scheduling mechanism on the platform. Use them for any deferred work — anniversary benefit resets, time-bounded reminders, scheduled state changes, end-of-term cleanup.
Instructions vs. scheduled functions
Default to instructions. Reach for a scheduled function only when the work genuinely cannot be expressed as pre-created per-policy instructions.
| Use instructions for (the default) | Use scheduled functions for (rare exceptions) |
|---|---|
| One-shot, per-policy work at a specific datetime | Bulk billing runs |
| Anniversary benefit resets, scheduled reminders, deferred state changes | Org-wide periodic tasks that iterate across policies |
| Per-policy / per-claim context that the function needs at execution time |
Scheduled functions use should be limited as much as possible — at worst one or two daily functions. The rest of the functionality should be managed by the instructions logic.
Scheduled functions are cron-driven and fan out across the full policy population on each tick. Instructions are pre-created per policy and only fire at their specific execution_datetime.
How they work
An instruction stores:
- The policy (and optionally a claim) it targets
- The function name to invoke
- A module payload — free-form data passed to the function at execution
- An execution datetime — when to run, must land on a 30-minute boundary (
:00or:30)
The platform runs a cron job every 30 minutes that picks up every pending instruction whose execution_datetime has passed. Each is dispatched via a shared FIFO queue partitioned by policy (per-policy ordering preserved), and the named function is invoked on the policy's product module.
Lifecycle
pending → queued → executed → successful
↘ failed
↘ cancelled
| Status | Meaning |
|---|---|
pending | Created. Waiting for the next cron run (every 30 minutes) to pick it up. |
queued | Cron has dispatched it; execution will follow within seconds. |
executed | Function has started running. Visible to the function during execution. |
successful | Function returned without throwing. Terminal. |
failed | Function threw, OR a platform sweep marked it failed (see Failure modes). Terminal. |
cancelled | Manually cancelled before execution. Terminal. |
Execution ordering
- Instructions for the same policy execute serially in
execution_datetimeorder (SQS FIFO withpolicy_idas the message group). - Instructions for different policies execute in parallel, up to the platform's
LIFECYCLE_HOOK_CONCURRENCYlimit (default 50). - Plan for this when scheduling: if two instructions on the same policy in the same slot have an implicit ordering requirement, the platform respects insert order — but don't rely on cross-policy ordering.
Creating an instruction
Via API
Instructions are created against a module — identified by module_type and
module_id. Both product_module and collection_module are dispatchable
(each has a registered adapter); for product_module, module_id is your
policy's product module ID. For product-module instructions you must also
supply policy_id so the engine can dispatch against the policy — omitting it
is rejected with a 400.
POST /v1/insurance/instructions
Content-Type: application/json
Authorization: Basic <api_key>:
{
"module_type": "product_module",
"module_id": "11111111-2222-3333-4444-555555555555",
"policy_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"function_name": "reset_benefit_home_contents_theft",
"module": {
"benefit_key": "home_contents_theft",
"benefit_start_date": "2025-06-01"
},
"execution_datetime": "2026-06-01T14:00:00.000Z",
"description": "Reset the home contents theft benefit on its anniversary",
"manually_executable": true
}Most product modules don't call this API directly — they return an
add_instructionaction from a lifecycle hook (below), which resolves
module_idfrom the policy automatically. Use the HTTP surface for
admin/back-office scheduling.
Required validation:
execution_datetimemust be at least 15 minutes in the futureexecution_datetimemust land on a 30-minute boundary — minutes are00or30, seconds/milliseconds zero (e.g.14:00:00.000Zor14:30:00.000Z, never14:15:00.000Z)- If
claim_idis provided, the claim must belong topolicy_id
Optional fields:
| Field | Default | Notes |
|---|---|---|
policy_id | (none) | Required for product_module instructions (dispatch needs the policy). |
claim_id | (none) | Associates the instruction with a specific claim. The claim is then passed to the function as the claim argument. |
description | null | Human-readable description. Surfaces in admin tooling and logs. |
hidden | false | Hides the instruction from standard listings. Requires the instructions:read_hidden permission to view. |
manually_executable | false | Allows triggering via the /trigger endpoint before execution_datetime. Default is false on every entry point (HTTP and the add_instruction PM action). |
Response: 201 Created with the new instruction, OR 200 OK with the existing instruction if an exact duplicate already exists (see Idempotency).
From product module code
Return an add_instruction action from any lifecycle hook:
async function afterClaimApproved({ policy, claim }) {
return [
{
name: 'add_instruction',
function_name: 'reset_benefit_home_contents_theft',
module: {
benefit_key: 'home_contents_theft',
benefit_start_date: claim.approved_date,
},
execution_datetime: '2026-06-01T14:00:00.000Z',
description: 'Reset the home contents theft benefit on its anniversary',
},
];
}The same validation rules apply (15 min lead, 30-minute boundary). The claim_id is inferred from the hook context when omitted, so the example above automatically associates the instruction with the claim being approved.
The instruction function
When the cron tick fires, the platform invokes the named function on the product module with a single argument:
async function reset_benefit_home_contents_theft({
policy, // PlatformPolicy
policyholder, // PlatformPolicyholder
claim, // PlatformClaim — only when instruction was created with claim_id
instruction_data, // the `module` payload supplied at creation
instruction, // the full PlatformInstruction row
}) {
return [
{
name: 'update_policy_module_data',
data: {
[instruction_data.benefit_key]: {
remaining_claims: 1,
remaining_cover_amount: 50000,
},
},
},
];
}The function returns the standard product-module-action array — same as any lifecycle hook. Return undefined (or omit the return) for a no-op.
instruction_data and instruction.module carry the same value — instruction_data is provided as a separate argument for convenience and to match the legacy in-PM-code naming.
Full type definitions for InstructionFunctionArgs, PlatformInstruction, AddInstructionAction, CancelInstructionAction, and BulkCancelInstructionsAction are available in the root.d.ts file that ships with your product module.
Idempotency
The platform deduplicates exact-duplicate instructions automatically. The uniqueness key is (organization, environment, module_id, policy_id, claim_id, function_name, execution_datetime, module).
- Same key already exists in
pending: the existing instruction is returned withHTTP 200. No new row is created. - Different
modulepayloads at the same slot: both rows are created (HTTP 201for both). Useful for scheduling the same function against multiple distinct contexts at the same hour — e.g. resetting two different benefits. - Different
claim_ids at the same slot: both rows are created. - Two policy-only (no
claim_id) instructions with otherwise identical fields: treated as exact duplicates (PostgresNULLS NOT DISTINCT). - JSON key order in
moduleis normalised:{ a: 1, b: 2 }and{ b: 2, a: 1 }are duplicates.
This means product modules can defensively re-create the same instruction without worrying about double-execution — re-runs are no-ops.
Cancelling an instruction
Via API
POST /v1/insurance/instructions/{instruction_id}/cancel
Authorization: Basic <api_key>:Only pending instructions can be cancelled. Cancelled rows do not block re-creation in the same slot.
From product module code
Cancel a single known instruction by ID:
return [
{ name: 'cancel_instruction', instruction_id: 'aabbccdd-...' },
];Bulk cancel by criteria
When you want to clear a schedule but don't have each instruction_id in hand —
e.g. on policy lapse, or when a benefit is removed — return a
bulk_cancel_instructions action. It cancels all matching pending
instructions for the current policy in one go. The policy is taken from the
lifecycle-hook context (you never supply a policy ID).
async function afterPolicyLapsed({ policy }) {
return [
// Cancel every pending instruction for this policy:
{ name: 'bulk_cancel_instructions' },
];
}Both fields are optional and narrow the set:
| Field | Effect when omitted | Effect when supplied |
|---|---|---|
function_name | Cancels pending instructions for all functions on the policy. | Cancels only pending instructions for that function. |
claim_id | In a claim lifecycle hook, defaults to the context claim. In a policy hook, matches the policy's instructions regardless of claim. | Cancels only pending instructions associated with that claim. |
// Clear just the anniversary resets for one benefit, scoped to a claim:
return [
{
name: 'bulk_cancel_instructions',
function_name: 'reset_benefit_home_contents_theft',
claim_id: 'aabbccdd-...',
},
];Only pending instructions are cancelled — rows already queued, executed, or
in a terminal state are left untouched. Hidden-but-pending instructions are
included. The action is safe to return defensively: if nothing matches, it's a
no-op (no error). There is no HTTP equivalent — API callers cancel one
instruction at a time via the single-ID /cancel endpoint above.
Manually triggering an instruction
For instructions created with manually_executable: true, you can fire them before their execution_datetime:
POST /v1/insurance/instructions/{instruction_id}/trigger
Authorization: Basic <api_key>:The instruction transitions pending → executed → successful/failed synchronously. Returns 200 on success.
Failure modes
An instruction can land in failed either because the function itself errored,
or because a platform sweep / dispatch step marked it failed. The failure_reason
field tells the two apart:
failure_reason | When | What it means |
|---|---|---|
null | The function threw an exception during execution | Function-level error. Details are in the product module run log. |
function_not_found | The named function_name is not defined on the product module | Misconfiguration — check the function exists with that exact name. The product module run log carries a platform-emitted "Function '<name>' is not defined in this product module" message. |
stale | execution_datetime was more than 24 hours in the past when the cron picked it up | Platform refused to dispatch (probably the cron was offline; the original intent is likely no longer relevant). |
stuck_in_queued | The instruction sat in queued for more than 30 minutes without progressing | SQS message likely lost, or worker crashed before execution started. Platform sweep marked it failed. |
stuck_in_executed | The instruction sat in executed for more than 30 minutes without reaching a terminal state | Worker crashed mid-execution. Platform sweep marked it failed. |
failed instructions are not retried automatically. To re-execute, create a new instruction (or, for manually_executable rows that are still pending, use the /trigger endpoint).
Transient infrastructure failures are not terminal. If the execution
service fails to invoke the function (Lambda SDK error, execution service
5xx, network timeout), the instruction is reverted toqueuedand retried
via SQS redelivery. Only if redeliveries are exhausted does the sweep mark it
failedwithstuck_in_queued.
Distinguishing platform vs function failures: a populated
failure_reason
means the platform marked the row failed (sweep or missing function);
failure_reason: nullmeans the function threw — look in the product
module run log for the stack trace.
Listing instructions
There is a single generic list/count surface — scope it to a policy, claim, or
module with query filters (there is no policy-nested list route).
GET /v1/insurance/instructions
HEAD /v1/insurance/instructions # returns just X-Total-Count (being renamed to X-Root-Total-Count)
GET /v1/insurance/instructions?policy_id={policy_id}&status=pending,failed
GET /v1/insurance/instructions?function_name=reset_benefit_home_contents_theftSupported filters: status (comma-separated), policy_id, claim_id, module_type, module_id, function_name, manually_executable, execution_datetime_from, execution_datetime_to. Pagination via the standard page + page_size (default 30, max 100). product_module_id is accepted as a deprecated alias for module_id (ignored when module_id is supplied).
Single fetch:
GET /v1/insurance/instructions/{instruction_id}Dashboard aggregates
Two read-only endpoints back the admin dashboard:
GET /v1/insurance/instructions-stats # time-bucket backlog (overdue / next 1h / 4h / 24h), status-over-time, scheduled-over-time, total
GET /v1/insurance/instructions-status-counts?from={iso}&to={iso} # status counts over a time windowinstructions-stats accepts a subset of the list filters: status, module_type, module_id, execution_datetime_from, execution_datetime_to. instructions-status-counts additionally accepts optional status, module_type, and module_id filters alongside the required from/to. Both require instructions:read (and respect instructions:read_hidden for hidden rows).
Permissions
| Permission | Grants |
|---|---|
instructions:read | List + fetch non-hidden instructions |
instructions:read_hidden | Also see instructions where hidden: true |
instructions:create | POST /instructions |
instructions:trigger | POST /instructions/{id}/trigger |
instructions:cancel | POST /instructions/{id}/cancel |
API keys get the standard set via organization_api_key_permissions. Configure per-user via roles in the management dashboard.
Operational guidance for product module authors
Scheduling for large policy populations
If you're scheduling instructions for many policies in the same slot (anniversary on a busy day, regulatory deadline), prefer spreading execution_datetime across multiple 30-minute boundaries rather than concentrating on one. The platform processes one slot's batch per job run (every 30 minutes); concentrating many thousands of instructions on the same slot means execution will spill into subsequent runs and may delay other instructions.
A common pattern: distribute by policy_id's last hex character or by a hash, so anniversary work for a fraction of the population fires each slot.
Population-scale scheduled work — anniversary sweeps across the whole book, staggered renewals — is exactly the workload to model as spread-out instructions rather than a scheduled function.
Defensive re-creation
Because exact duplicates are idempotent (return 200, no new row), you can safely re-call add_instruction from a lifecycle hook that fires multiple times. The platform will only create the row once. If you need to force a new row at the same slot, vary the module payload (e.g. add a created_for_event: '<event_id>' field).
Working with the module payload
module payloadmodule is free-form Record<string, any>. It's stored as Postgres JSONB and returned to the function as a plain object. Best practices:
- Keep it small — typical payloads are a few hundred bytes. Don't dump entire policy snapshots; the function receives
policyseparately. - Use stable keys — if your function expects
module.benefit_key, don't have callers sendmodule.benefitKeyfor some andmodule.benefit_keyfor others. - Schema-validate inside the function — the platform doesn't validate
moduleshape (it's per-product-module), so use Joi or zod inside your function to catch bad payloads at execution time.
Detecting platform-side vs function-side failures
When investigating a failed instruction:
if (instruction.failure_reason) {
// Platform marked it failed (stale, stuck_in_queued, stuck_in_executed,
// or function_not_found)
// — a config/ops issue, not a thrown exception in your function
} else {
// Function threw an exception
// — check the product module run log for the stack trace
}API reference
Full OpenAPI specification: see instructions.yaml in the API docs site, or import it directly into Postman / Insomnia.
| Method | Path | Purpose |
|---|---|---|
POST | /instructions | Create (module-scoped: module_type + module_id) |
GET | /instructions | List (filter by policy_id, claim_id, module_id, …) |
HEAD | /instructions | Count (returns X-Total-Count; being renamed to X-Root-Total-Count) |
GET | /instructions/{instruction_id} | Single fetch by ID |
GET | /instructions-stats | Dashboard aggregates (time-bucket backlog, status-over-time) |
GET | /instructions-status-counts | Status counts over a from/to window |
POST | /instructions/{instruction_id}/trigger | Manual trigger |
POST | /instructions/{instruction_id}/cancel | Cancel one pending |
Related guides
- Product module functions — the broader system instructions plug into
- Scheduled functions — for the rare org-wide recurring work that can't be expressed as instructions
- Lifecycle hooks — where most
add_instructionactions are returned from - Product module actions — the full list of actions a PM function can return
Updated about 4 hours ago