Compile a flow into a Storyline
Turn a multi-step process into a stateful directed graph — create, validate, publish.
create_storylinevalidate_storylinepublish_storylinelist_storylinesupdate_storylineA Storyline is a directed graph hung on an agent: it turns "answer one question" into "carry a whole
task end to end" (intake, tutoring, coaching…). Nodes are steps (each with its own task / skills / kb /
tools + writable profile dimensions); exits carry conditions (ai / user_choice / rule / callback
/ goto_storyline). The runtime never edits the agent's soul or safety boundary — it is constant from
the first node to the last, which is exactly what a regulated or child-facing product needs.
Create a draft
create_storyline(
agent_name="Support", key="loan-intake", name="Loan intake",
profile_schema={"docs_ready": {"type": "bool"}}, # dims that accumulate across nodes
graph={"nodes": [
{"node_key": "n1", "title": "Understand need", "flags": {"is_entry": True},
"on_enter_opening": "Hi — which kind of loan are you applying for?",
"exits": [{"kind": "ai", "label": "need clear", "to_node_key": "n2",
"ai_criteria": "the user stated the loan type and rough amount"}]},
{"node_key": "n2", "title": "Collect documents",
"task": "Collect the checklist items; chase missing ones across turns.",
"exits": [{"kind": "user_choice", "label": "documents ready", "to_node_key": "n3",
"user_choice": {"button_text": "I've uploaded everything"},
"writes": [{"ref": "dim", "key": "docs_ready", "op": "set", "value": True}]}]},
{"node_key": "n3", "title": "Hand off", "task": "Summarise the case and hand off to a human.",
"flags": {"is_terminal": True}, "exits": []}
]},
allow_agent_enroll=True,
enroll_trigger="the visitor says they want to apply for a loan",
)★ Validate, then publish ★
validate_storyline(storyline_id="…") # → {"ok": true} is required to publish
publish_storyline(storyline_id="…") # freezes an immutable version and goes liveEnrollment — who enters, and when (all require publishing)
is_default=True— one per agent; auto-enrolls on the first conversation.allow_agent_enroll=True+enroll_trigger— the agent enrolls the user when it judges the trigger.entry="user"(or"both") — the flow is listed in the chat widget's guided-flows menu and the New-chat dropdown, so the user starts it deliberately (a button, not a model judgement). Give it adisplay_nameand a one-linedescription— that's what the menu shows. Combine withconcurrency="session"for "one case per conversation" flows: each pick opens a fresh case.- Manual assignment in the console.
What the user sees — visibility, banner, and the read-only map
user_visibility controls the end-user presentation of a running flow (default invisible):
invisible— no UI at all; the flow drives silently (previous behaviour).named— a slim banner above the chat shows the flow's name. Nothing else.trail— banner + a View button opening a read-only map: steps already taken carry their titles; untaken branches and future steps are anonymous grey blocks (redacted server-side — the user sees how long the flow is, not what's in it).full— banner with stepx / y+ the full map: every step named, colour-coded done / current / upcoming.
Extras: allow_exit=True adds an Exit button to the banner (an exited flow does not
auto-re-enroll in that conversation; a user-picked flow can be re-entered from the menu, progress
kept). show_profile=True lists the profile dimensions' current values under the map (numbers as
progress bars when the schema declares min/max). Per node, rewind: "reset" | "keep" lets the
user click a taken step in the full map and go back to it — reset restores the profile snapshot
taken on arrival, keep keeps collected data (study/review flows). Real-world side effects (filed
records, submitted documents) are never rolled back, so mark only side-effect-free steps.
On IM channels (Telegram/WhatsApp) the /storyline command reports the current flow and, for
trail/full, returns a short-lived signed link to the same read-only map.
The legacy learner_visibility parameter name and its hidden / completed_only values are still
accepted (they map to invisible / trail) but are deprecated — use user_visibility.
Concurrency — does progress follow the person, or the case?
concurrency decides how many runs one user can have on this line:
"user"(default) — one run per person; every conversation continues the same progress. Right for curricula, onboarding, KYC: "how far has this person got"."session"— one run per conversation; a new chat starts a fresh, independent case. Right for licence applications, support tickets, per-product flows: "how far has this case got". The same user can run three product applications in three chats, each with its own progress and blackboard.
Put state where it belongs: case state on the blackboard (it lives and dies with the run —
the product name, this application's document list), facts about the person in profile
dimensions (they follow the user across runs and storylines — company details, verified
identity). One case per conversation is deliberate: to open another case, open another chat.
Changing concurrency affects only future enrolments; runs already in flight keep their key.
Checklists, gated choices, files, and deterministic actions
Four node-level capabilities turn "the AI decides when we're done" into accumulated, deterministic state:
checklist—[{key, label}, …]on a node. Each turn the platform checks items off from what the visitor actually provides (uploads, or descriptions with concrete details — promises don't count), and an exit rule{"op": ">=", "left": {"ref": "checklist"}, "right": {"value": 1}}advances the moment everything is checked. The read-only map shows the user a live ✓/○ list. This is the right shape for application/approval flows: step 1 announces what's needed, step 2's checklist collects it item by item.choices_offer: "on_ready"— game-style pacing for choice buttons: they stay hidden (and text-matching them is disabled — no skipping ahead by typing the button word) until the step is done — checklist complete, or achoices_ready_criteriayou provide is judged true. Useuser_choicefor genuine decisions between alternatives; a lone "continue" button is a smell.{"ref": "files"}in exit rules — the number of files/images uploaded while at this node (resets on transition).files >= 2is a fully deterministic "documents received" gate.questions— per-node icebreaker chips (max 6): the questions a user would likely ask at this step, shown as tappable suggestions while they're on it. Pair withon_enter_openingso every step both introduces itself and offers a way in — a user should never wonder "what am I supposed to do here".on_enter_actions/on_complete_actions—[{tool, args}]the platform executes deterministically when the step is entered / completes the flow (e.g. file the lead withmcp__agent4__submit_leadat the terminal step). String args interpolate dimension and blackboard keys with curly-brace templates. Failures are logged and never break the chat.
Context stays flat no matter how long the storyline is: each turn injects only the current
node (task, resources, choices), the profile dimensions, and a size-capped blackboard — never the
whole graph or past steps. Long journeys (a school year, a season-long game arc) should be chained
storylines (on_complete: goto_next): each line's blackboard starts fresh while profile dimensions
carry over, which is exactly "summarise the past, keep the present verbatim".
The exit judge now sees a recent-conversation window and the collected state, not just the last
message — but keep ai_criteria about things the visitor said or did; the checklist mechanism
above is still stronger than any prose criteria.
Design pitfalls — each of these has produced a stuck flow in production
You (the agent building this) are the most likely author of these bugs. Check every one before publishing:
- A
ruleexit needs a writer.readiness >= 100is a dead end unless something actually writesreadiness: an exit'swrites(deterministic — preferred), a node'sprofile_writeswithsource: "ai"(scored from conversation), another storyline, or manual console edits.validate_storylinenow flags this aswarn.rule_unwritten_dim— treat that warning as a bug unless you know an outside writer exists. This exact mistake shipped once: a three-step intake whose second step gated on a dimension nothing wrote; users could never pass it. - AI-scored dimensions almost never hit exact bounds. If the dimension is scored by the model,
>= 100(or== max) practically never fires — models score conservatively. Gate on a realistic threshold (>= 70), or make the step's completion auser_choiceand use the exit'swritesto set the value deterministically. user_choicematches the button text exactly — and the label must say what it does. The visitor's message must equalbutton_text. On visible lines the widget renders these as real buttons, so write complete, precise verb phrases — "Start document collection", not "Start" (start what? — real feedback from testing). Keep them distinct and in the audience's language. Oninvisiblelines there are no buttons — restate the options in the nodetask. Preferuser_choicefor genuine decisions (which branch, submit vs. keep editing); "am I done with this step" is the AI/rule exits' job, not a button.- Publishing does not touch runs already in flight. Enrollments stay pinned to the revision
they started on — a bug-fix release reaches only new runs. To move existing users, call
POST /storylines/{id}/enrollments/migrate(resetback to the entry ormoveto a node — both re-pin the run to the current published revision). - Walk the line as a user before handing it out. Open the share's
/s/page, enter the flow, click through every button and branch to the end. The editor's test-run simulates the graph; only the real page exercises openings, buttons, visibility and completion together.
Exits are priority-ordered (list order): deterministic rule / user_choice / callback are judged
first, ai last. Build if/else, retry loops and AND-joins out of deterministic exits — don't gamble on
the LLM. Edit a draft with update_storyline (PUT semantics; keep every node_key stable, since exits
and the funnel reference it).
Report back. Return the published Storyline's console link —
https://console.agent4.io/#/storylines/<id>(the id fromcreate_storyline) — with a one-line summary of the flow and how a user enters it.