FabricFabricPlatform
Platform referenceIntegrations

Temporal binding

Declared workflows, governed activities, stable identity, compensation, and the boundary between orchestration and audit truth.

@fabricorg/platform-temporal is a separately versioned host-side binding. createGovernedActionActivities turns Temporal activity input into a governed Host invocation with a stable temporal:<workflowId>:<stepId> idempotency key, correlation, child-action causation, and workflow/run/step execution evidence. It never invokes a domain handler directly.

createSagaSignalDispatcher maps versioned saga or approval notifications to Temporal signals. startGovernedWorkflow uses a stable workflow ID and use_existing conflict policy. Worker deployments must record build and deployment identity for safe rollover.

Temporal owns durable domain orchestration and waiting; Fabric Harness owns durable agent sessions; Platform owns governed mutations and canonical audit evidence. Temporal history never substitutes for Platform invocation, policy, approval, adapter, or event records. Cancellation and compensation are modeled as explicit governed steps with stable step IDs.

Declaring durability

A capability declares the workflows it owns in its manifest, so durability is a reviewable fact of the contract rather than a property of how workers happen to be deployed. Each entry names the workflow, the task queue it runs on, the event that starts it by choreography, the durable deadlines it carries, and which governed action reverses each compensatable step.

A timer declares the configuration key its duration resolves from, never a literal. A right-to-cure window is then a declared, diffable fact that each program supplies a value for.

capability: {
  displayName: "Recovery",
  description: "Field recovery operations.",
  stability: "beta",
  workflows: [{
    name: "OrderLifecycle",
    taskQueue: "recovery.fieldsvc",
    startedBy: "AssignmentAccepted",
    timers: [{ name: "right-to-cure", configKey: "program.cureWindow" }],
    compensations: [{ step: "schedule-transport", actionId: "recovery.releaseTransport" }],
  }],
}

The field is additive, so MODULE_MANIFEST_VERSION stays at 2. Both the module registry and the compiler validate it — the compiler because a serialized manifest reaches generators without passing through a registry. A compensating action must be idempotent: an unwind is retried like any other durable step, so a reversal that cannot be safely repeated turns one failure into a different kind of corruption.

diffModuleManifests classifies this plane rather than reporting one blanket capability change. Adding a workflow, a deadline or a reversal is compatible. Moving a task queue, changing what starts a workflow, dropping a deadline or repointing a reversal is potentially_breaking. Removing a workflow or a reversal is breaking.

Compensation

runCompensatedSaga runs steps in order and accumulates reversals as each succeeds. On failure it unwinds newest-first, so the last effect applied is the first withdrawn. Every reversal enters through the governed Host under compensationInvocationIdentity — a distinct, deterministic key — so a retried or replayed unwind is not applied twice.

Two properties are worth stating plainly. A step that resolves with a non-successful status — failed, blocked_by_policy, validation_failed, reconciliation_required, waiting_for_approval — triggers the unwind; only an explicitly completed step counts as success, because the governed Host resolves rather than throws for most refusals. And a reversal that itself fails is recorded on SagaCompensationError rather than swallowed, so a partial unwind is visible instead of silent.

Step identities must be unique within a workflow and may not use the reserved compensate: prefix, which would collide with a reversal's identity. Supply the manifest's workflows entry as workflow and every step's reversal is checked against what the capability declared.

await runCompensatedSaga(activities.invokeGovernedAction, {
  workflowId, runId, tenantId, spaceId, actorId, actorType: "natural_person",
  workflow,                       // the manifest declaration; reversals are held to it
  steps: [
    { stepId: "schedule-transport", actionId: "recovery.scheduleTransport", parameters,
      compensateWith: { actionId: "recovery.releaseTransport" } },
    { stepId: "settle", actionId: "recovery.settle", parameters },
  ],
});

Verified, not asserted

The binding ships a conformance suite that runs against a real Temporal server rather than a mock. It covers a durable deadline carried across a whole flow, an audit trail reconstructable from Platform's own store without reading Temporal history, refusal of a duplicate workflow id, worker-restart resume of an in-flight execution, and a worker-version rollover where a workflow starts under one build and completes under the next.

Two environments are used deliberately. The ten-day deadline runs on the time-skipping test environment, because waiting ten days is not a test. Worker-restart resume and rollover run on a real local server: the time-skipping server does not re-dispatch a pending task to a second worker, which is a property of that server rather than of the binding, and testing a restart against it would prove nothing.

On this page