<bmx-workflow-engine>
Runs a workflow in the page: give it the definition bmx-workflow-designer
makes and an instance (or start one), and it shows the step the instance is
at as a task card - the actions open to the person (actor), a comment,
the values the workflow carries, the time left, and the history - and takes
the action they choose. view="none" runs it with no card: a page drives
it from script (start, fire, available) and reads the events.
8 properties · 5 events · 9 methods · 7 parts
Example
Take an action as the manager, then switch person.
Show markup
<p style="margin: 0 0 12px">
<label>Act as
<select id="ex-we-who">
<option value="mia">Mia (Manager)</option>
<option value="finn">Finn (Finance)</option>
<option value="eve">Eve (Employee)</option>
</select>
</label>
<label style="margin-inline-start: 12px">Amount <input id="ex-we-amount" type="number" value="250" style="inline-size: 7rem"></label>
<button type="button" id="ex-we-new">New claim</button>
</p>
<bmx-workflow-engine id="ex-we"></bmx-workflow-engine>
<p class="note" id="ex-we-out" role="status" style="margin: 12px 0 0">Take an action as the manager, then switch person.</p>
<script type="module">
await customElements.whenDefined('bmx-workflow-engine');
const we = document.getElementById('ex-we');
const def = {
id: 'expenses', name: 'Expense claim', revision: 3,
roles: [
{ id: 'employee', name: 'Employee', color: '#a5d6a7' },
{ id: 'manager', name: 'Manager', color: '#90caf9' },
{ id: 'finance', name: 'Finance', color: '#ffcc80' },
],
variables: [
{ name: 'amount', type: 'number', label: 'Amount', default: 0 },
{ name: 'region', type: 'text', label: 'Region', default: 'UK' },
{ name: 'tier', type: 'text', label: 'Tier' },
{ name: 'paid', type: 'boolean', label: 'Paid', default: false },
],
states: [
{ id: 'start', name: 'Submitted', kind: 'start', x: 160, y: 460 },
{ id: 'route', name: 'Route by amount', kind: 'decision', x: 340, y: 460 },
{ id: 'fix', name: 'Fix and resubmit', kind: 'task', role: 'employee', x: 340, y: 40 },
{ id: 'manager', name: 'Manager review', kind: 'approval', role: 'manager', description: 'Check the receipts against the policy.', sla: { hours: 24, escalateTo: 'finance' }, actions: [{ type: 'emit', event: 'notify', data: { to: 'manager' } }], x: 560, y: 180 },
{ id: 'finance', name: 'Finance review', kind: 'approval', role: 'finance', description: 'Check the budget and pay.', actions: [{ type: 'set', variable: 'tier', value: '=IF(amount > 5000, "high", "standard")' }], x: 560, y: 320 },
{ id: 'hold', name: 'Wait for budget', kind: 'wait', role: 'finance', sla: { days: 5 }, x: 820, y: 320 },
{ id: 'paid', name: 'Paid', kind: 'end', actions: [{ type: 'set', variable: 'paid', value: true }], x: 820, y: 460 },
{ id: 'rejected', name: 'Rejected', kind: 'end', x: 1040, y: 180 },
],
transitions: [
{ id: 'go', from: 'start', to: 'route', auto: true },
{ id: 'small', from: 'route', to: 'manager', guard: '=amount <= 1000' },
{ id: 'large', from: 'route', to: 'finance' },
{ id: 'approve', from: 'manager', to: 'paid', name: 'Approve' },
{ id: 'reject', from: 'manager', to: 'rejected', name: 'Reject', requireComment: true },
{ id: 'back', from: 'manager', to: 'fix', name: 'Send back', requireComment: true },
{ id: 'pay', from: 'finance', to: 'paid', name: 'Pay', guard: '=AND(amount > 0, region = "UK")' },
{ id: 'park', from: 'finance', to: 'hold', name: 'Hold' },
{ id: 'release', from: 'hold', to: 'paid', name: 'Release' },
{ id: 'freject', from: 'finance', to: 'rejected', name: 'Reject', requireComment: true },
{ id: 'resubmit', from: 'fix', to: 'route', name: 'Resubmit' },
],
};
const people = {
mia: { id: 'mia', name: 'Mia', roles: ['manager'] },
finn: { id: 'finn', name: 'Finn', roles: ['finance'] },
eve: { id: 'eve', name: 'Eve', roles: ['employee'] },
};
const who = document.getElementById('ex-we-who');
const out = document.getElementById('ex-we-out');
we.definition = def;
we.actor = people[who.value];
who.addEventListener('change', () => (we.actor = people[who.value]));
const start = () => we.start({ amount: Number(document.getElementById('ex-we-amount').value) || 0 });
document.getElementById('ex-we-new').addEventListener('click', start);
await start();
we.addEventListener('bmxWorkflowChange', e => (out.textContent = `Now at "${def.states.find(s => s.id === e.detail.to).name}" - the page saves the instance.`));
we.addEventListener('bmxWorkflowEmit', e => (out.textContent = `Event "${e.detail.event}" - the page would notify ${e.detail.data?.to ?? 'someone'}.`));
</script>
<bmx-workflow-engine id="claim"></bmx-workflow-engine>
<script>
claim.definition = await api.workflow('expenses');
claim.instance = await api.claim(42); // or: await claim.start({ amount: 250 })
claim.actor = { id: 'mia', name: 'Mia', roles: ['manager'] };
claim.addEventListener('bmxWorkflowChange', e => api.saveClaim(42, e.detail.instance));
claim.addEventListener('bmxWorkflowEmit', e => api.notify(e.detail.event, e.detail.data));
</script>
The same engine runs on a server as bmx-workflow.mjs (Node 18+), so a
service can check every action the page sends. Instances are plain JSON;
nothing is stored or sent. Time limits are checked each minute and, with
auto-escalate, overdue steps move to their escalation state.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
actor |
actor |
BmxWorkflowActor | string |
— | Who is acting: { id, name, roles }, or JSON. Without one, every action is open (an administrator). |
autoEscalate |
auto-escalate |
boolean |
false |
Move overdue steps to their escalation state, checked each minute. |
definition |
definition |
BmxWorkflowDefinition | string |
'' |
The workflow definition, as an object or JSON. |
editableValues |
editable-values |
boolean |
false |
Let the person change the workflow's values before acting. |
instance |
instance |
BmxWorkflowInstance | string |
— | The instance being run, as an object or JSON. Mutable: every step writes the new instance. |
locale |
locale |
string |
— | The language for times. Default: the page's. |
strings |
strings |
Partial<BmxWorkflowEngineStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
view |
view |
'card' | 'none' |
'card' |
card shows the task card; none runs with nothing shown. |
Events
| Event | Detail | Description |
|---|---|---|
bmxWorkflowAction |
BmxWorkflowActionDetail |
Fired before an action is taken from the card or fire. Cancel it to refuse. |
bmxWorkflowChange |
BmxWorkflowChangeDetail |
Fired after the instance moves: save the instance. |
bmxWorkflowComplete |
{ instance: BmxWorkflowInstance; } |
Fired when the instance reaches an end state. |
bmxWorkflowEmit |
BmxWorkflowEmitted & { instance: BmxWorkflowInstance; } |
Fired for each event the definition hands to the page (its emit actions). |
bmxWorkflowError |
{ code: string; error: string; } |
Fired when something cannot be done (a refused action, a broken definition). |
Methods
| Method | Signature | Description |
|---|---|---|
available |
available() => Promise<BmxWorkflowTransition[]> |
The actions open to the actor now. |
cancel |
cancel(comment?: string) => Promise<void> |
Stop the instance. |
escalate |
escalate() => Promise<boolean> |
Move the instance to its escalation state if its step is overdue. Resolves whether it moved. |
fire |
fire(transition: string, options?: { comment?: string; variables?: Record<string, unknown>; }) => Promise<BmxWorkflowResult> |
Take an action (transition id or name), with a comment and values. Resolves the result. |
getEngine |
getEngine() => Promise<Engine> |
The engine for the definition: the same object bmx-workflow.mjs makes, for anything not on the element. |
getInstance |
getInstance() => Promise<BmxWorkflowInstance | null> |
The instance now, or null. |
getTimer |
getTimer() => Promise<BmxWorkflowTimer | null> |
The current step's time limit, or null. |
options |
options() => Promise<BmxWorkflowOption[]> |
Every action from the current state, open or closed, with why. |
start |
start(variables?: Record<string, unknown>) => Promise<BmxWorkflowResult> |
Start a new instance with values (and the definition's defaults). Resolves the result. |
CSS shadow parts
| Part | Description |
|---|---|
action |
One action button. |
actions |
The action buttons. |
card |
The task card. |
comment |
The comment box. |
details |
The values the workflow carries. |
header |
The state's name and status. |
history |
The history list. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-workflow-engine-background |
The card's background. |
--bmx-workflow-engine-width |
The card's width. |