Skip to content

AI stages ​

A stage can belong to the AI instead of a person: actor="ai". The AI answers it the way a person would, in the stage's own fields. It can classify, extract, flag and decide where the work goes next, and it lands on the same record as everyone else.

tsx
<Stage
  name="check"
  actor="ai"
  transitions={[{ targetStageId: 'certificate', condition: { decision: { eq: 'changes' } } }]}
>
  <Action
    id="read_certificate"
    type="ai_prompt"
    on="enter"
    files={['certificate']}
    prompt="A supplier sent certificate {{cert_number}} from {{issuer}}, covering {{scope}}. …"
  />
  <Field name="standard" type="select" label="Standard" required options={STANDARDS} />
  <Field name="expires_on" type="date" label="Expires" />
  <Field name="decision" type="radio" required options={[{ value: 'ok' }, { value: 'changes' }]} />
  <Remarks on="cert_number" />
  <Deadline after="1bd" goTo="sign_off" />
</Stage>

cert_number, issuer, scope and the certificate upload are fields in the supplier's stage before this one.

How the AI knows what to do ​

The model never sees your JSX. The engine reads it and hands the model two things: a letter to read and a form to fill in. Then it takes the reply and writes it into the stage.

The letter is the prompt. Each {{…}} is a blank. Before sending, the engine fills each one with what someone already answered in the session, so the model receives:

A supplier sent certificate Q-123 from TÜV, covering machining of steel parts. …

Each file named in files goes along with it (see Reading documents). That is all the model knows: the letter, the files, and nothing else from the session.

The form is the stage's fields. The engine turns them into a strict answer sheet (a JSON schema) and the provider holds the model to it:

In the stageWhat the model is told
<Field name="standard" type="select" required options={STANDARDS}/>standard: exactly one of iso9001, iso14001, iso45001, other
<Field name="expires_on" type="date"/>expires_on: a date, or null when it can't tell
<Field name="decision" type="radio" required options={…}/>decision: ok or changes, nothing else
<Remarks on="cert_number"/>remarks.cert_number: a note for the people, or null

A field's label, and the labels of its options, go along as its description: iso9001 = ISO 9001 (quality). The model can't invent a field or answer "maybe" to a radio.

The reply comes back in that shape:

json
{
  "answers": { "standard": "iso9001", "expires_on": "2027-05-01", "decision": "changes" },
  "remarks": { "cert_number": "The number on the document doesn't match Q-123." }
}

The engine does the rest. It records each answer as if a person had typed it, marked as the AI's. It checks the answers the way it checks a person's. Then the stage's transitions read decision: changes sends the work back to the supplier with the remark, and ok moves it on. The AI fills in the decision; your routing makes the call.

In short: the prompt is the question, the {{blanks}} are the data, the fields are the answer sheet, and the transitions decide what happens next. So write the prompt the way you'd brief a colleague: what they're looking at, and what counts as ok.

How it runs ​

  1. The stage is entered and its ai_prompt action with on="enter" runs. That action is the stage's agent. Don't give it a schema: the engine builds one from the stage's fields, so the model answers in exactly those fields. Required fields must be answered and optional ones may be null. Each <Remarks on> in the stage adds a place for a note on that field.
  2. The engine writes the answer as field events from actor ai with source: 'ai', and each note as a remark by ai. Anything the stage doesn't declare is ignored.
  3. The stage is checked like a person's (required fields, options, dates, limits), then completed, its onComplete actions run and routing reads the answers. So the AI's decision picks the next stage, including sending the work back.

The AI can do nothing a person's stage can't: it fills its own fields, remarks where the stage says, and hands on. It never edits another stage's answers.

What the AI doesHow you declare it
Classify a documentA select or radio field
Extract valuestext, date, number, currency … fields
Flag a field<Remarks on="field"> in the AI stage
Decide, or send it backA decision field and the stage's transitions
Complete the stageIt completes by itself once its answer passes the checks
Recommendai="propose" on a field in the next person's stage

AI help in a person's stage ​

The AI doesn't need a stage of its own to help. Mark fields ai in a person's stage, and the stage's one ai_prompt answers them too. It can run on enter, or as a field's action, so it runs as soon as the person has given it something to go on:

tsx
<Stage name="upload" actor="role:supplier">
  <Field name="description" type="textarea" required>
    <Action id="classify" type="ai_prompt" on="blur" prompt="Classify: {{description}}" />
  </Field>
  <Field name="category" type="select" options={CATEGORIES} required ai="propose" />
  <Field name="period" type="text" ai="fill" />
</Stage>
  • ai="fill" writes the value while the field is still empty (a field event from actor ai, source: 'ai'). The person can change it like any answer.
  • ai="propose" records a proposal: the value sits under the field as a suggestion with a Use it button until the field is answered. Taking it is the person's own answer, and typing something else answers it too. A fill onto a field the person already answered becomes a proposal: the AI never writes over a person.
  • The person still submits the stage. The AI recommends; it never decides for them.

Proposals are in state.proposals (field → { value, by, at }) and on useField().proposal ({ value, accept }) for your own components. They follow the field's visibleTo. Every ai field is nullable in the schema: "I don't know" is always an answer. The stage needs exactly one ai_prompt, which can't be blocking or carry its own schema.

Reading documents ​

An ai_prompt reads uploads when you name their fields in files:

tsx
<Action id="read_certificate" type="ai_prompt" on="enter" files={['certificate']} prompt="…" />

The engine fetches each file from your bucket and checks it against the hash recorded at upload. It types the file by its bytes, then sends it before the prompt: PDFs as documents, PNG, JPEG, WebP and GIF as images, and plain text as text. At most 10 files and 20 MB per call. Anything else fails the action with the file's name, never its contents. On openai_compatible a PDF goes as a file part, which not every provider takes.

With ai fields, a document goes in and the category, dates and amounts come out as fills and suggestions for the person to check.

Personal data ​

The model is someone else's server, so the engine keeps fields marked vault or pii away from it, and deploy refuses a workflow that would send one:

  • In a prompt: {{owner_name}} in an ai_prompt's prompt or system, when owner_name is vault or pii (or sits in a group that holds one).
  • As a file: a vault or pii file field in files.
  • As an answer: the AI can't answer a vault or pii field in its own stage or a person's. Its answer is also its action's result, kept in plain text like every result.

What the engine can't see is what is inside a document or a free-text answer: a bank statement has a name on it. If a document holds personal data the model must not read, don't give it to the model. Mark the field vault and deploy will stop you. An action's result is readable by everyone in the session unless the action has a visibleTo, so give the agent the same visibleTo as the stage when its answers are private.

When it goes wrong ​

If the model call fails, or its answer doesn't pass the stage's checks (a value that isn't one of the options, a missing required field), the reason is recorded as action_failed on the agent and the stage stays open. Give an AI stage a <Deadline goTo> so a person takes over:

tsx
<Deadline after="1bd" goTo="sign_off" />

A slow model parks the stage like any async action and it carries on when the answer arrives. The action's timeoutSeconds (default 20 minutes for an ai_prompt) still applies.

What can be in an AI stage ​

Fields an AI can fill: text, textarea, email, tel, url, date, datetime, time, number, currency, rating, year, month, boolean, yesno, checkbox, select, radio, multicheck and tags. Deploy refuses an AI stage with a file, signature, geo, address, matrix, ranking, repeating group or custom field, and an AI stage without an ai_prompt on enter.

Nobody else can act as the AI: the engine mints no token for ai, and ai takes no participant.

Classifying with Jev ​

For a stage that only sorts, flags or routes, provider="typesafe" sends it to TypeSafe's Jev instead of a language model. Jev answers in well under a second, at a fraction of the cost, and can only answer one of the choices you gave it. The stage is written the same way:

tsx
<Stage
  name="triage"
  actor="ai"
  transitions={[{ targetStageId: 'on_call', condition: { urgent: { eq: true } } }]}
>
  <Action id="triage" type="ai_prompt" provider="typesafe" on="enter"
    prompt="Supplier {{supplier}} writes: {{message}}" />
  <Field name="urgent" type="yesno" label="Needs attention today?" required />
  <Field name="category" type="radio" required options={[
    { value: 'billing', label: 'Billing question' },
    { value: 'delivery', label: 'Late or wrong delivery' },
  ]} />
</Stage>

The prompt is the text Jev reads. Each field becomes a question: a yesno, boolean or checkbox is a yes/no (yes when Jev puts it at 50% or more), and a select or radio picks one option, described by its label. The field's label is the question. Fields marked ai in a person's stage work the same way.

Jev writes nothing, so deploy refuses a provider="typesafe" agent whose stage has any other kind of field (text, dates, numbers, several options), asks for remarks, or reads files. Put the key in the environment's TYPESAFE_API_KEY secret (or name another with secretRef); model defaults to jev-latest.

In the sandbox ​

The playground runs AI stages locally. The sandbox's stand-in model answers with values that fit the schema, and the same answers always give the same result, so both branches of a decision can be reached by changing the input. Example: react/workflow/playground/certificate-check.tsx.