Workflow
Workflow
$workflow runs asynchronous operations through declarative lifecycles. A
definition states where a command may run and what state and data changes occur
while it is pending, after success, and after failure. Application code only
requests the operation.
Use $machine for synchronous event-driven state. Use $workflow when an
operation can wait, fail, time out, be cancelled, or require recovery.
Create A Workflow
app.workflow('checkout', {
initial: 'ready',
data: {
receipt: null,
failure: '',
},
commands: {
submit: {
from: 'ready',
pending: 'processing',
execute: ({ input, signal }) =>
checkoutApi.submit(input, { signal }),
success: {
to: 'completed',
update({ data, output }) {
data.receipt = output;
data.failure = '';
},
},
failure: {
to: 'failed',
update({ data, diagnostic }) {
data.failure = diagnostic.message;
},
},
},
},
});
Inject the named workflow and request the command:
app.controller('CheckoutCtrl', ["checkout", function (checkout) {
this.checkout = checkout;
this.submit = async (order) => {
const result = await checkout.run('submit', order);
if (!result.ok) {
console.error(result.diagnostics);
}
};
}]);
Templates observe workflow state and data directly:
<button
ng-click="$ctrl.submit($ctrl.order)"
ng-disabled="!$ctrl.checkout.can('submit')"
>
Submit
</button>
<p ng-if="$ctrl.checkout.state === 'processing'">Processing</p>
<p ng-if="$ctrl.checkout.state === 'failed'">
{{ $ctrl.checkout.data.failure }}
</p>
Command Lifecycle
Every command declares:
from: state or states from which the command may run.pending: state entered before execution starts.execute: optional synchronous or asynchronous operation.success: state entered when execution returns.failure: state entered when execution throws or rejects.
pending, success, and failure accept either a state name or an object with
to and update:
success: {
to: 'completed',
update({ data, input, output, command }) {
data.receipt = output;
},
}
Lifecycle updates are the writable boundary for workflow data. The data passed
to execute is readonly, including nested plain objects, arrays, maps, and
sets. This prevents a failed command from leaving untracked imperative changes.
Immediate Commands
An operation that only changes workflow state can omit execute:
commands: {
reset: {
from: ['completed', 'failed'],
pending: 'resetting',
success: {
to: 'ready',
update({ data }) {
data.receipt = null;
data.failure = '';
},
},
failure: 'failed',
},
}
await checkout.run('reset');
External approvals, WebSocket messages, user actions, and repair requests enter
a workflow as commands. Workflows do not expose a separate event or send()
API.
Results
run() always resolves to a WorkflowResult:
const result = await checkout.run('submit', order);
if (result.ok) {
console.log(result.output);
} else if (result.status === 'timeout') {
console.warn('The operation timed out.');
} else {
console.warn(result.diagnostics);
}
Successful results have status completed. Failed results use failed,
cancelled, timeout, or rejected.
rejected means execution did not fail unexpectedly. The command was unknown,
was not allowed from the current state, violated its concurrency declaration,
or called reject() with a controlled diagnostic.
Controlled Rejection
Use reject() for an expected business decision:
execute({ input, reject }) {
if (!input.acceptedTerms) {
return reject({
code: 'checkout.termsRequired',
message: 'Terms must be accepted.',
recoverable: true,
});
}
return checkoutApi.submit(input);
}
The framework records the diagnostic and applies the declared failure lifecycle.
Cancellation And Timeout
Commands receive an AbortSignal and may register cleanup callbacks:
execute({ input, signal, cleanup }) {
const request = checkoutApi.submit(input, { signal });
cleanup(() => request.releaseResources());
return request;
}
Cancel one command or every running command:
checkout.cancel('submit');
checkout.cancel();
Declare timeout duration and outcome with the command:
submit: {
from: 'ready',
pending: 'processing',
commandTimeout: 5000,
execute: submitOrder,
success: 'completed',
failure: 'failed',
cancelled: 'cancelled',
timeout: 'timed-out',
}
When cancelled or timeout is omitted, the command uses its failure
lifecycle.
Concurrency And Retry
Concurrency belongs to the declaration:
submit: {
from: ['ready', 'processing'],
pending: 'processing',
concurrency: 'reject',
retry: 2,
execute: submitOrder,
success: 'completed',
failure: 'failed',
}
rejectis the default and rejects another run of the same command.queueexecutes runs of the same command in request order.parallelallows overlapping execution when the declared source states also permit it.retryis the number of automatic retries after unexpected execution failure.
Callers cannot override these properties through run(). Operational behavior
therefore remains part of the workflow definition.
Diagnostics And History
workflow.diagnostics contains normalized, serializable evidence from failed
operations. workflow.history records command start, completion, and final
failure.
workflow.diagnostics;
workflow.history;
Use diagnosticLimit and historyLimit to bound retained entries:
app.workflow('checkout', {
initial: 'ready',
data: {},
diagnosticLimit: 100,
historyLimit: 500,
commands: checkoutCommands,
});
Snapshot And Restore
Snapshots contain the workflow id, state, data, diagnostics, and history:
const snapshot = checkout.snapshot();
localStorage.setItem('checkout', JSON.stringify(snapshot));
checkout.restore(JSON.parse(localStorage.getItem('checkout')));
Use migrateSnapshot when persisted data predates the current snapshot shape:
migrateSnapshot(snapshot) {
if (snapshot.version === 0) {
return {
version: 1,
id: 'checkout',
state: snapshot.stage,
data: snapshot.data,
diagnostics: [],
history: [],
};
}
return snapshot;
}
Supervisor
A workflow supervisor owns persistence and recovery for a named group:
app.workflowSupervisor('checkoutProcesses', {
workflows: {
checkout: checkoutDefinition,
fulfillment: fulfillmentDefinition,
},
persistence: 'indexeddb',
autoPersist: true,
autoRecover: true,
});
app.controller('AdminCtrl', ["checkoutProcesses", function (checkoutProcesses) {
this.checkout = checkoutProcesses.workflow('checkout');
}]);
Supervisors can also use a persistence adapter:
const persistence = {
async load(id) {
return database.load(id);
},
async save(id, snapshot) {
await database.save(id, snapshot);
},
};
autoPersist saves after every settled command. autoRecover restores the
latest snapshot and reruns the latest recoverable failed command for each
workflow.
Worker Runtime
The optional workflow worker runtime transports run, snapshot, and
restore. It does not define another orchestration protocol:
const host = createWorkflowWorkerHost({
workflows: { checkout },
});
const client = createWorkflowWorkerClient(workerConnection);
const result = await client.run('checkout', 'submit', order);
Runtime API
workflow.id;
workflow.state;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.can('submit');
workflow.run('submit', order);
workflow.cancel('submit');
workflow.snapshot();
workflow.restore(snapshot);
The runtime intentionally has no send(), manual lifecycle transition, or
per-run orchestration options.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.