v2.6.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--bmx-workflow-engine-background The card's background.
--bmx-workflow-engine-width The card's width.