All docs

Docs · Get started

Tutorial

the approval flow

One real flow, built end to end: a customer email arrives, AI drafts the reply, a human approves it, the chassis sends it. Three rules, one small service of your own.

This is the shape of most operational work — something arrives, a machine does the legwork, a person decides, the system follows through — so it makes a good first stack. Here is the whole thing:

OPS/support/
  0100_DRAFT/draft.txcl       # AI drafts a reply
  0200_APPROVE/approve.txcl   # a human decides (the flow waits)
  0300_SEND/send.txcl         # approved → send it
  0300_SEND/declined.txcl     # declined → record and stop

The numbered directories are the steps of the flow, in order. As a flow, it looks like this — one box per operation, with the two 0300 rules side by side because rules at the same step run (or resonate) in parallel:

support
input0100draft0200approve0300senddeclinedoutput

Every rule below is shown in full.

Step 1 — AI drafts, with the mission attached

OPS/support/0100_DRAFT/draft.txcl:

WHEN @src == "lmtp"
SET .saga.name = "q3-retention",
    .saga.goal = "keep churn under 2%"
WITH system = "You draft warm, concise support replies. The mission is retention: keep this customer.",
     prompt  = "Draft a reply to this customer email:\n\n{{@lmtp.msg.text}}",
     intent  = "draft_support_reply"
EXEC "ai://chat"

Reading it: fire when mail arrives (@src == "lmtp"); stamp the saga onto the document — sagas are just fields you choose, no registration — so every later participant knows why this matter exists; hand the email’s text to ai://chat ({{@lmtp.msg.text}} reads the parsed message body from the envelope). The model’s draft merges back as .text.

Step 2 — a human decides, and the flow waits

OPS/support/0200_APPROVE/approve.txcl:

WHEN .text != ""
WITH mode = "async", timeout = "2h"
EXEC "https://approve.internal.example.com/review"

mode = "async" makes this a continuation: the flow suspends — durably, surviving restarts — until your reviewer service calls back.

The reviewer service is yours, and it’s small. It receives the document (the email, the draft, the saga) plus a callback contract — a callback_url and a single-use token in the X-Txco-Continuation-Token header. It does three things:

  1. Returns 202 Accepted immediately.
  2. Puts the draft in front of a person — email yourself an approve/decline link, post it to a channel, render a page.
  3. When the person clicks, POSTs the verdict to the callback_url:
{
  "status": "completed",
  "output": {
    "approved": true,
    "reply": "<the final text>"
  }
}

output is arbitrary JSON — it deep-merges into the document exactly like any operation’s response — and the flow advances, whether the click came two minutes or two hours later.

Step 3 — follow through

Two rules at the same step; only the one that resonates fires.

OPS/support/0300_SEND/send.txcl:

WHEN .approved == true
SET ._sendmail.subject = &concat("Re: ", @lmtp.msg.subject),
    ._sendmail.from    = "support@acme.example",
    ._sendmail.to      = @lmtp.msg.from.0.addr,
    ._sendmail.body    = .reply
EXEC "txco://sendmail"
EMIT @halt = true

OPS/support/0300_SEND/declined.txcl:

WHEN .approved == false
EMIT .outcome = "declined", @halt = true

The send rule assembles the _sendmail contract straight from the document — reply-to address from the original mail, body from the approved text. (Outbound mail requires a configured relay, and the from domain must be a verified hostname of your tenant — the anti-spoof rule.)

Run it

Mail reaches the stack through the LMTP head behind a Postfix — wiring in lmtp.md. But you don’t need mail to develop the flow: mocks let you run the whole stack first — point step 1 and step 2 at fixtures (mock-response.json + X-Txco-Mocks: support/**), fire events with curl, and watch the logic route, merge, and halt before any real model, reviewer, or mailbox exists.

txco apply
txco trace last

The trace is the payoff: three steps, the model’s tokens, the suspend at step 2 with the hours-long gap plainly visible in the timings, the resume, the send — the whole story of one matter, on disk, readable by you or by an AI you ask to debug it.

What you just used

One stack exercised most of the system: multi-protocol ingress, AI as an operation, intent as data (sagas), a human in the loop via continuations, outbound email, parallel rules at a step, and the trace. Every piece is swappable the same way — the approver could become a Slack bot, the model a different provider, the channel a web form — without touching the rules around it.

Edit this page · View as markdown