Skip to content

Concepts

Workflow, stage, step, field

Workflow   what runs            <Workflow id="loan_v1">
└ Stage    who does it          <Stage name="apply" actor="role:applicant">
  └ Step   one page of it       <Step name="income">
    └ Field  one answer         <Field name="salary" type="currency" required />
  • A stage belongs to one actor — a role like role:applicant or role:underwriter. Only that role may answer its fields or submit it. Stages are the security boundary.
  • A step is pagination inside a stage. It has no security meaning; showIf hides a step.
  • A field is one answer, validated on the client for feedback and on the server for real. <FieldGroup> is a repeatable group (dependents, line items).

The JSX is the source of truth, but the engine never runs your code. clipless deploy reads the workflow out of your file — stages, roles, fields, rules — and the engine enforces that.

Sessions and actors

A session is one run of a workflow: one claim, one application. Your backend starts it with an API key (POST /workflows/:id/sessions) and gets a token for an actor. A token is scoped to one session and one role; your frontend passes it to <Workflow runtimeToken>.

The engine takes the actor from the token, never from the request. It refuses:

  • events from a role that does not own the active stage,
  • fields that are not in that stage,
  • anything once the stage is submitted and locked,
  • answers to a mode="readonly" stage.

Routing

When a stage is submitted, the engine validates it, runs its actions, locks it, and decides what comes next:

  1. The first of the stage's transitions whose condition holds — which may point back to an earlier stage (Loop-backs).
  2. Otherwise the next stage in order whose renderIf holds. A stage whose renderIf fails is skipped.
  3. No stage left: the session is complete.

Conditions are plain objects, evaluated against the answers and action results:

ts
{ decision: 'Approve' }                       // equals
{ decision: ['Approve', 'Escalate'] }         // one of
{ amount: { gte: 1000 } }                     // eq ne gt gte lt lte in nin
{ country: 'US', amount: { gt: 0 } }          // all of
{ $or: [{ vip: true }, { amount: { lt: 50 } }] }
{ $not: { country: 'US' } }
{ 'risk.level': 'high' }                      // an action's result, by action id

The same conditions drive renderIf (stage), showIf (step, field), requiredIf and disabledIf (field).

The ledger

Nothing is updated in place. Every change is an event appended to the session's ledger, with a sequence number and a SHA-256 hash of the one before it, so tampering shows.

From the clientFrom the engine
field — an answer was setstage_enter / stage_complete / stage_reopen
step_enter — moved to a stepstage_transition — routed to a stage
repeat_add / repeat_remove / repeat_undoaction_dispatch / action_complete / action_failed
remark — a comment on a field

Typing is not saved letter by letter. The screen updates on every keystroke; the ledger gets onefield event per field when typing pauses (half a second), or at once when the field loses focus, the stage is submitted, the step changes, a field action needs the value, or the page is closed.

The session's state — current stage, answers, action results, what is locked — is the ledger replayed. So a draft is just a ledger that stops, history is the ledger read back, and "what did the applicant enter before the reviewer asked for changes" is answered without anyone designing for it.

Answers the engine writes itself — prefill given at the start, answers carried from last year — are field events from system, marked with where they came from.

Rounds

Each entry into a stage is a round. Usually every stage has one. When a workflow loops back — a manager sends a claim back to the employee — the claim gets a round 2 and the review a round 2 after it, and each earlier round keeps the answers it left. useRounds() lists them. See Loop-backs and rounds.

Visibility

By default everyone in a session can read every answer. visibleTo narrows it:

tsx
<Stage name="underwriting" actor="role:underwriter" visibleTo={['role:loan_officer']}>
  <Field name="internal_notes" type="textarea" />                  {/* underwriter + officer */}
  <Field name="apr" type="number" visibleTo="everyone" />          {/* re-opened to all */}
</Stage>

A stage's owner always sees its own answers. Fields inherit the stage's visibleTo; <Action visibleTo> hides an action's result. The engine redacts every response per role — state, ledger, history — so a hidden answer never reaches that role's browser. Deploy refuses a workflow in which a role's own conditions read something that role cannot see.

Sensitive data

  • vault — encrypted at rest, with a key derived per session. Erasing one session's key (crypto-shredding) makes its vault answers, and its generated documents, unreadable without touching the ledger's hash chain or any other session.
  • pii — personal data that is not encrypted: kept out of the plaintext cache on the session row and masked in clipless session unless --reveal.

Revisions

Every deploy of a changed workflow is kept as a revision. A session stays on the revision it started on: deploying a new required field does not break the sessions already running, and a finished session is always read against the form it was filled in with.

Environments

An API key belongs to one environment — say test or production — and sees only that environment's sessions. Secrets (clipless secrets) and the webhook signing secret are per environment too.