user_visibility — what the end user sees of their own run: "invisible" (default, no UI),
"named" (a banner with the storyline name only), "trail" (banner + read-only view where
untaken branches and future steps are redacted grey blocks), "full" (banner with step x/y +
full read-only map). `learner_visibility` is the deprecated old name (legacy values
hidden/completed_only still accepted and mapped).
concurrency — who the progress follows: "user" (default) = progress belongs to the person, all
of that user's sessions share one run — fits curricula / onboarding / KYC; "session" =
progress belongs to the case, each session gets its own run, a new conversation = a new
application — fits licence applications / tickets / per-product flows. Case state goes to the
blackboard (travels with the run); facts about the person go to profile dimensions (shared
across runs).
graph = {"nodes":[Node,...], "edges":[]} (edges are derived from exits, may be left empty).
Node = {
node_key: stable uuid (unchanged across edits; exits/funnels reference it), title,
task (may interpolate {dimension}/{blackboard.key}),
type: "task"(default) | "document_review"(visual pre-review of uploads) |
"export"(structured report) | "parallel"(parallel branches / AND-join),
review: {"checkpoints":[str]} — type=document_review: per-item visual checkpoints,
export: {"sections":[str]} — type=export: report sections,
parallel: {"branches":[{"key":str,"label":str,"to_node_key":str},...]}
— type=parallel: declares required branches, each pointing at a sub-flow entry; the user
may do them in any order, the engine tracks completion, and only when ALL are done does
the node take its single join exit (put it at exits[0]). A branch sub-flow's last step
just exits back to this parallel node — no hand-written completion flags.
flags: {"is_entry":bool, "is_terminal":bool},
on_enter_opening: something to say proactively on entry (empty = silent transition),
ai_eval_trigger: natural-language condition for when to run AI evaluation (empty = every turn),
callback: {"mode":"none"|"backend"|"ui_redirect", "wait_timeout_secs":int, "signal_name":str},
profile_writes: [{"dim":str, "source":"ai"|"rule"|"callback"}] — dimensions this node writes,
resources: {"skills":[str],"knowledge_bases":[str],"tools":[str],
"resource_mode":"additive"|"replace"},
exits: [Exit,...] (list order = priority; deterministic rule/callback/user_choice are
evaluated first, ai last)
}
Exit = {"kind":..., "label":str, "to_node_key":str,
"ai_criteria":str — kind=ai: one natural-language criterion,
"user_choice":{"button_text":str} — kind=user_choice,
"rule_ast":RuleAst — kind=rule (see below; an AST, not a string),
"callback_signal":"done"|"timeout"|"canceled" — kind=callback,
"target_storyline_id":str, — kind=goto_storyline
"writes":[{"ref":"dim"|"blackboard","key":str,"op":"set"|"inc","value":<num|str|bool>},...]}
— deterministic state writes when this exit is taken (this is how profile_writes with
source='rule' actually land): set = assign (completion flags / branch flags),
inc = increment (loop/retry counters, value defaults to 1). dim writes are constrained
by profile_schema.
kind ∈ ai|user_choice|rule|callback|goto_storyline.
Common control flow composes deterministically (never bet on the LLM): if/else/switch = several
rule exits on one node (order = priority); loop/retry = a back-edge + an inc counter in writes
+ a rule cap gate; AND-join = a parallel node, or hub + completion flags + an "and" rule.
RuleAst is one of:
comparison {"op":">="|">"|"<="|"<"|"=="|"!=",
"left":{"ref":"dim"|"blackboard","key":str}, "right":{"value":<num|str|bool>}}
boolean {"op":"and"|"or", "clauses":[RuleAst,...]}
profile_schema = dimension definitions, e.g.
{"listening":{"type":"int","min":0,"max":100,"visible_to_user":true}}.
on_complete="goto_next" requires next_storyline_id (validate/publish reject otherwise).
Enrolment (who enters this line, when): is_default=true auto-enrols on first conversation (at
most one per agent); allow_agent_enroll=true lets the agent enrol users mid-conversation — in
that case ALWAYS write enroll_trigger (one natural-language "enter when", e.g. "the visitor
says they want to apply for a loan"), otherwise the agent has no trigger basis and almost never
enrols; takes effect after publishing, independent of is_default and manual assignment.
See /docs/tenant-guide/storylines.