Skip to content

Tutorial: a reviewed AI reply

In this tutorial you build a process that answers customer complaints. An agent classifies the complaint, a second agent drafts the reply, and a support lead approves the draft or rejects it with a comment. A rejected draft goes back to the agent, which rewrites it using that comment, at most three times.

Along the way you use everything a real process needs:

You will see Where
Paste APL and get a diagram Step 3
A repeat limit on a loop (↻ 3) and named routes Step 4
Review outcomes with a required comment Step 5
A route drawn on the canvas (a two-day timeout) Step 6
A local, mocked Dry Run Step 8
Real agents, a human decision and a revised draft Steps 9–12
The audit trail and a fallback model Steps 13–14

It takes about 20 minutes.

Before you start

  • The development platform is running and you can sign in to Studio at http://studio.localhost.
  • An AI provider is set in Studio → Settings → AI Providers (for example a Gemini key). Without one, agent steps wait — nothing breaks.
  • Two development accounts: alice (alice / alice) builds and runs the process, bob (bob / bob) reviews the drafts.

Sign in as alice. Click the project name in the header, open New project, and name it Customer care. You become its Owner, Maintainer, Operator and Viewer.

The New project tab of the project dialog, filled with “Customer care”

The review step will be offered to the support-leads group. Bob is not in that group in the identity provider, so you give it to him inside this project only:

  1. Ask bob to sign in to Studio once, so the platform knows him.
  2. As alice, open Admin → Projects → Add Member, pick bob, tick Viewer, and type support-leads under Task Groups (Human Tasks).

Editing bob’s project roles: Viewer, with the task group support-leads

The project members table: alice with all roles, bob as Viewer with the task group support-leads

A project task group works exactly like an identity group for human tasks, but only in this project. Without the Admin tab (it needs the abada-admin group), an owner can do the same through the API:

Terminal window
# Find bob's principal id, then make him a Viewer with the task group.
curl "$ABADA_URL/api/v1/projects/$PROJECT_ID/principals?query=bob" -H "Authorization: Bearer $TOKEN"
curl -X PUT "$ABADA_URL/api/v1/projects/$PROJECT_ID/members/$BOB_PRINCIPAL_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"roles": ["VIEWER"], "reviewLanes": [], "taskGroups": ["support-leads"]}'

In the Project panel, click + next to Project (New Process), choose Paste APL, paste the document below and name the file complaint_reply.

The New Process dialog with Paste APL selected and the document pasted

complaint-reply.apl.yaml
version: abada.io/v1
metadata:
key: complaint_reply
name: Complaint reply
owner: customer-care
variables:
- name: complaint
type: object
required: true
flow:
entry: complaint-received
nodes:
- id: complaint-received
type: webhook
description: Complaint received
next: classify
# Advice, checked by the engine: the answer must match the schema and be confident enough.
- id: classify
type: agent
description: Classify the complaint
model: gemini-3.6-flash
prompt: |
Classify this customer complaint. Give its category, its severity
(low, medium or high) and a one-sentence summary.
inputs:
complaint: ${complaint}
result_variable: classification
output_schema:
type: object
required: [category, severity, summary]
properties:
category: { type: string }
severity: { enum: [low, medium, high] }
summary: { type: string }
confidence_threshold: 70
on_low_confidence: triage
next: draft
# Rewritten after each rejection with the reviewer's comment, three times at most.
- id: draft
type: agent
description: Draft the reply
model: gemini-3.6-flash
fallback_models: [gpt-5-mini]
prompt: |
Write a short, polite reply to the customer. Offer a remedy that fits
the severity. Reviewer feedback on the previous draft, if any: ${review_comment}
inputs:
complaint: ${complaint}
classification: ${classification}
review_comment: ${review_comment}
result_variable: reply
output_schema:
type: object
required: [subject, body]
properties:
subject: { type: string }
body: { type: string }
loop:
max_iterations: 3
on_exhausted: triage
on_timeout: { after: PT30M, then: triage }
next: review
# A person decides; a rejection must say why.
- id: review
type: human-input
description: Review the reply
assignees: [support-leads]
sla_hours: 4
escalate_to: [managers]
outcomes:
approve: { next: sent }
reject: { next: draft, comment: required }
- id: triage
type: human-input
description: Handle the complaint by hand
assignees: [managers]
next: sent
- id: sent
type: end
description: Reply approved

The same file is in the repository: examples/apl/complaint-reply.apl.yaml.

What each part asks the engine to guarantee:

  • classify is advice. Its answer must match output_schema and carry a confidence of at least 70; otherwise the engine sends the case to triage instead of trusting it.
  • draft reads review_comment, which is empty on the first pass and holds the reviewer’s comment after a rejection. Its loop lets the process come back here three times at most, then hands the case to triage. on_timeout stops a draft that takes more than 30 minutes, and fallback_models keeps it going if gemini-3.6-flash is rate-limited.
  • review has two outcomes. Reject goes back to draft and cannot be sent without a comment. After four hours (sla_hours) the task stays open but managers can pick it up too.

Studio draws the document. The ↻ 3 badge marks the repeat limit, and each route carries its name: approve and reject leave the review, low confidence, timeout and limit reached lead to triage.

The diagram: Complaint received, Classify, Draft with a ↻ 3 badge, Review, Triage and Sent, with labelled routes

Click Review. The inspector shows the assignees, the service level, the escalation group and the outcomes, each with its target and a comment required switch.

The Review inspector: support-leads, SLA 4 hours, escalate to managers, approve to Sent, reject to Draft with comment required

Click Draft. The Loop section holds the repeat limit (3, then Triage); further down, Routes holds the timeout and the fallback models.

The Draft inspector: repeat limit 3 then Triage, model gemini-3.6-flash and the prompt

The Draft routes: timeout PT30M to Triage and the fallback model gpt-5-mini

Everything you change here is written to the APL, and every change is one undo step (⌘Z / Ctrl+Z).

Nobody should forget a draft for a week. Drag from the right edge of Review to Triage. Studio asks what the connection means and offers only the routes a review step supports:

The connect menu from Review to Triage: Outcome approve, Outcome reject, On error, On timeout…

Choose On timeout…, type P2D (two days, ISO-8601) and click Add timeout. Studio checks the duration the way the engine does (between one second and 365 days).

The timeout form with P2D and the Add timeout button

The new route appears at once:

The diagram with a new timeout route from Review to Triage

Switch to APL YAML. The drawn route is now an on_timeout block on the review step, and the status bar says Valid APL: the engine checked the draft while you edited.

The APL YAML view showing on_timeout after P2D then triage on the review step, and Valid APL in the status bar

Click Dry Run, paste this payload and start:

{
"complaint": {
"customer": "Claire Dupont",
"order": "A-1042",
"message": "My order arrived nine days late. I paid 7.90 EUR for shipping and want it back."
}
}

Dry Run runs in your browser: nothing is saved, deployed or sent to a model. It stops at each agent so you can type a mocked answer, and at the review it offers every route of the step — Approve, Reject and the new Timeout. Try Reject to watch the token go back to Draft.

Dry Run stopped at Review with the buttons Approve → Sent, Reject → Draft and Timeout → Triage, and the run log

Finish with Approve → Sent. Studio remembers that this exact revision passed a Dry Run.

Click Deploy & Start, paste the same payload, and confirm. Studio saves the document, deploys it as an immutable version (v1) and starts an instance.

The Deploy & Start dialog with the complaint payload

The live instance opens on the canvas. Both agents run through the Agent Worker, outside any database transaction, and the instance stops at Review. Click Draft to see its attempt: the model that answered, the confidence, the token counts, and Output contract OK — the engine checked the answer against the schema before storing it.

The live instance waiting at Review, with the Draft attempt telemetry: model, provider, latency, output contract OK and tokens

The Variables tab shows what the agents produced:

The instance variables: classification and the first draft of the reply

Sign in as bob and open Task Inbox. Review the reply is waiting for the support-leads group. Open it and click Claim.

Bob’s inbox with the Review the reply task

The Decision panel shows one button per outcome. Bob finds the draft too thin, writes why, and clicks Reject. Without the comment, Studio — and the engine — would refuse the rejection.

The Decision panel with the comment “Too short for a 9-day delay. Add a 10% voucher for the next order.” and the Approve and Reject buttons

The engine stores the decision in review_outcome, the comment in review_comment, and sends the process back to Draft. The agent writes a second draft with the comment in its inputs, and a new review task appears.

In the new task, draft_iteration is 2, review_comment holds bob’s words, and the reply now offers the voucher:

The second review task: draft_iteration 2, review_outcome reject, review_comment, and a reply that adds a 10% voucher

Bob claims it and clicks Approve. The instance completes.

Sign in as alice again and open Operations: the instance is Completed, and the Agent Worker is online.

Operations: completed Complaint reply instances, no instance needing intervention, the agent worker online

Open the instance. The canvas highlights the path it took — including the way back from Review to Draft — and the Audit Trail lists every step: who claimed and decided each task, which worker ran each agent, and the route each decision took (outcome reject → draft).

The completed instance: the taken path including the loop back to Draft, and the audit trail with “outcome reject → draft” by bob

The history records that bob wrote a comment and how long it was, never the text itself; the text lives in the instance variables.

If gemini-3.6-flash answers 429 Too Many Requests, the worker tries the next model of fallback_models instead of failing. The attempt shows which model actually answered and which one it replaced:

Attempt telemetry: executed model gpt-5-mini, fallback for gemini-3.6-flash, output contract OK

If every model is unavailable, the attempt waits and retries without using up max_attempts; on_timeout (30 minutes here) bounds the wait. See Operate running processes for incidents and retries.

flowchart LR
    S([Complaint received]) --> C[classify · agent]
    C -->|low confidence| T[triage · managers]
    C --> D[draft · agent ↻ 3]
    D -->|timeout 30 min| T
    D -->|limit reached| T
    D --> R{review · support-leads}
    R -->|approve| E([Sent])
    R -->|reject + comment| D
    R -->|timeout 2 days| T
    T --> E

The agents advise; the engine checks every answer and every decision; people approve; and PostgreSQL keeps the whole story.