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 datetimeBulk billing runs
Anniversary benefit resets, scheduled reminders, deferred state changesOrg-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 (:00 or :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
StatusMeaning
pendingCreated. Waiting for the next cron run (every 30 minutes) to pick it up.
queuedCron has dispatched it; execution will follow within seconds.
executedFunction has started running. Visible to the function during execution.
successfulFunction returned without throwing. Terminal.
failedFunction threw, OR a platform sweep marked it failed (see Failure modes). Terminal.
cancelledManually cancelled before execution. Terminal.

Execution ordering

  • Instructions for the same policy execute serially in execution_datetime order (SQS FIFO with policy_id as the message group).
  • Instructions for different policies execute in parallel, up to the platform's LIFECYCLE_HOOK_CONCURRENCY limit (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_instruction action from a lifecycle hook (below), which resolves
module_id from the policy automatically. Use the HTTP surface for
admin/back-office scheduling.

Required validation:

  • execution_datetime must be at least 15 minutes in the future
  • execution_datetime must land on a 30-minute boundary — minutes are 00 or 30, seconds/milliseconds zero (e.g. 14:00:00.000Z or 14:30:00.000Z, never 14:15:00.000Z)
  • If claim_id is provided, the claim must belong to policy_id

Optional fields:

FieldDefaultNotes
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.
descriptionnullHuman-readable description. Surfaces in admin tooling and logs.
hiddenfalseHides the instruction from standard listings. Requires the instructions:read_hidden permission to view.
manually_executablefalseAllows 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 with HTTP 200. No new row is created.
  • Different module payloads at the same slot: both rows are created (HTTP 201 for 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 (Postgres NULLS NOT DISTINCT).
  • JSON key order in module is 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:

FieldEffect when omittedEffect when supplied
function_nameCancels pending instructions for all functions on the policy.Cancels only pending instructions for that function.
claim_idIn 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_reasonWhenWhat it means
nullThe function threw an exception during executionFunction-level error. Details are in the product module run log.
function_not_foundThe named function_name is not defined on the product moduleMisconfiguration — 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.
staleexecution_datetime was more than 24 hours in the past when the cron picked it upPlatform refused to dispatch (probably the cron was offline; the original intent is likely no longer relevant).
stuck_in_queuedThe instruction sat in queued for more than 30 minutes without progressingSQS message likely lost, or worker crashed before execution started. Platform sweep marked it failed.
stuck_in_executedThe instruction sat in executed for more than 30 minutes without reaching a terminal stateWorker 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 to queued and retried
via SQS redelivery. Only if redeliveries are exhausted does the sweep mark it
failed with stuck_in_queued.

Distinguishing platform vs function failures: a populated failure_reason
means the platform marked the row failed (sweep or missing function);
failure_reason: null means 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_theft

Supported 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 window

instructions-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

PermissionGrants
instructions:readList + fetch non-hidden instructions
instructions:read_hiddenAlso see instructions where hidden: true
instructions:createPOST /instructions
instructions:triggerPOST /instructions/{id}/trigger
instructions:cancelPOST /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 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 policy separately.
  • Use stable keys — if your function expects module.benefit_key, don't have callers send module.benefitKey for some and module.benefit_key for others.
  • Schema-validate inside the function — the platform doesn't validate module shape (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.

MethodPathPurpose
POST/instructionsCreate (module-scoped: module_type + module_id)
GET/instructionsList (filter by policy_id, claim_id, module_id, …)
HEAD/instructionsCount (returns X-Total-Count; being renamed to X-Root-Total-Count)
GET/instructions/{instruction_id}Single fetch by ID
GET/instructions-statsDashboard aggregates (time-bucket backlog, status-over-time)
GET/instructions-status-countsStatus counts over a from/to window
POST/instructions/{instruction_id}/triggerManual trigger
POST/instructions/{instruction_id}/cancelCancel one pending

Related guides


Did this page help you?