Skip to content

Deadlines ​

A review nobody gets to is a process that stops. Put a <Deadline> in a stage to remind someone, hand the work to someone else, or close the session when time runs out.

Try it

An expense claim with a reminder and an escalation in the playground runs in your browser. Skip to … in the toolbar moves the clock forward to the next deadline.

tsx
<Stage name="review" actor="role:manager">
  <Field name="decision" type="select" required options={…} />

  {/* Two business days in: remind them. The stage stays open. */}
  <Deadline after="2bd">
    <Action id="remind_manager" type="email" to="{{manager_email}}" subject="Still waiting: {{description}}" />
  </Deadline>

  {/* Five business days in: finance decides instead. */}
  <Deadline after="5bd" goTo="escalation" />
</Stage>

<Stage name="claim" actor="role:employee">
  …
  {/* A claim nobody submits for 30 days is closed. */}
  <Deadline after="30d" expire />
</Stage>

A deadline can do three things:

What happens
Actions onlyA reminder: the actions inside run, and the stage stays open. You can set several.
goTo="stage"The stage is left unsubmitted (stage_expired), and the session moves to that stage. Any actions inside run first.
expireThe session ends with status expired. After that, the engine accepts no answers and no submits.

When the clock runs ​

  • The clock starts when the stage is entered. Each round gets new clocks, so a claim that was sent back and resubmitted gives the manager a fresh two days. See Loop-backs and rounds.
  • A deadline fires once per round, and only while the stage is open. Once someone submits the stage, its remaining deadlines never fire.
  • No deadline fires while the session is waiting on an action (a KYC check, a slow webhook). That action has its own timeoutSeconds, and deadlines are checked again once it resolves.
  • The engine looks for due deadlines every minute. A deadline fires within about a minute of its time.

A goTo target is entered even if its renderIf is false. That is how an escalation stage can stay off the normal path: give it a renderIf that is false once the manager has decided, and only the deadline takes the session there.

Durations and business days ​

after
30m, 4hminutes, hours
2dcalendar days (48 hours)
3bdbusiness days: weekdays that are not holidays, at the same local time of day

Business days depend on where the person is. A Friday in Zagreb is not a Friday in Los Angeles. Give the session a time zone and your holidays when you start it:

ts
await fetch(`${ENGINE}/api/v1/workflows/expense_claim_v1/sessions`, {
  method: 'POST',
  headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json' },
  body: JSON.stringify({
    role: 'role:employee',
    timeZone: 'Europe/Zagreb',           // IANA name; default UTC
    holidays: ['2026-12-25', '2026-12-26'], // local dates, besides Saturdays and Sundays
  }),
})

Entered Friday at 16:00 in Zagreb, 3bd is due Wednesday at 16:00 there. Clock changes are taken into account. There are no built-in national calendars: you send the holidays you observe.

Seeing what is coming ​

  • useDeadlines() gives the current stage's pending deadlines with their due times, so you can show "reminder goes out Wednesday".
  • GET /workflows/:workflowId/sessions/:sid (API key) lists them as deadlines, with the session's timeZone and holidays.
  • The ledger records each firing as timer_fired { stage, deadline, round }, followed by what it did.

The engine fires them, within a minute of when they fall due. The playground can skip ahead to the next one, to see a reminder or an escalation without waiting days.

Rules ​

  • A deadline's actions are never blocking, because nothing waits for them. Deploy refuses a blocking one.
  • after must be a duration, and a goTo target must exist. A deadline needs at least one of: actions, goTo or expire. It cannot have both goTo and expire.
  • Put <Deadline> directly in a <Stage> or one of its <Step>s. Its <Action>s take no on.