Skip to content

Patterns

Runnable examples in examples/. Diagrams auto-generated from each example's graph.namespaces() (always in sync with code).

If you need… See Related docs
Invariants + pinned reactions Order control-flow
Human-in-the-loop approval Expense control-flow
Tool-calling agent + AG-UI streaming Conversation agui, reducers
Supervisor loop / fan-in Task reducers
Scatter fan-out / map-reduce Batch control-flow
Safety gates + live streaming Content streaming, concepts
Retries + escalation via raises= Question control-flow
πŸ—οΈ Diagram vocabulary
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Example["Namespace"]
      direction LR
      Command{{Command}}:::cmd
      DomainEvent(DomainEvent):::devt
      Halted([Halted]):::halt
      Invariant{Invariant}:::inv
      Rejected(Rejected):::devt
    end
    IntegrationEvent[/IntegrationEvent/]:::intg
    SystemEvent([SystemEvent]):::syst
    _seed_[ ]:::entry ==> Command
    Command --> DomainEvent
    Command -.->|"(raises)"| SystemEvent
    Command -.->|scatter| IntegrationEvent
    Command -.- Halted
    Command -.->|invariant| Invariant -.->|reactor| Rejected
    DomainEvent -->|"reactor [orchestrate]"| Command
    Command -->|"[chain]"| Command
    linkStyle 1 stroke:#6b7280,stroke-dasharray:3 3
    linkStyle 2 stroke:#7c3aed,stroke-width:2.5px,stroke-dasharray:8 3
    linkStyle 3 stroke:#9ca3af,stroke-dasharray:3 3
    linkStyle 5,6 stroke:#c2410c,stroke-dasharray:4 2
    linkStyle 7 stroke:#0369a1,stroke-width:3px
    linkStyle 8 stroke:#b91c1c,stroke-width:2px,stroke-dasharray:5 3

Invariants & reactions (Order namespace)

  • Commands: Place, Ship. Outcomes: Placed, Rejected, Shipped.
  • Invariants: CustomerNotBanned, OrderTotalWithinLimit (typed, with pinned reactors turning violations into namespace-level Order.Rejected).
  • State: current_status (ScalarReducer as domain attribute).
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Order["Order namespace"]
        direction LR
        Place{{Place}}:::cmd
        Placed(Placed):::devt
        Rejected(Rejected):::devt
        Ship{{Ship}}:::cmd
        Shipped(Shipped):::devt
        CustomerNotBanned{CustomerNotBanned}:::inv
        OrderTotalWithinLimit{OrderTotalWithinLimit}:::inv
    end
    _e0_[ ]:::entry ==> Place
    _e1_[ ]:::entry ==> Ship
    Place --> Placed
    Ship --> Shipped
    CustomerNotBanned -.->|explain_banned| Rejected
    OrderTotalWithinLimit -.->|explain_over_limit| Rejected
    Place -.->|invariant| CustomerNotBanned
    Place -.->|invariant| OrderTotalWithinLimit
    linkStyle 4,5,6,7 stroke:#c2410c,stroke-dasharray:4 2
Namespaces:
  Order
    Command: Place  (invariant: CustomerNotBanned, OrderTotalWithinLimit)
      β†’ Placed
    Command: Ship
      β†’ Shipped
    Event: Rejected
System events:
  InvariantViolated
Invariants:
  CustomerNotBanned  (on Place; reacted by: explain_banned)
  OrderTotalWithinLimit  (on Place; reacted by: explain_over_limit)
Policies:
  explain_banned  (InvariantViolated β†’ Rejected)
  explain_over_limit  (InvariantViolated β†’ Rejected)
Seed events:
  Place
  Ship

Full code Β· Raw diagrams on GitHub

Domain-named inline handlers

A Command's inline handler can be named after its verb (place, ship, submit, …) instead of the generic handle. The framework picks up the sole public method in the class body; underscore-prefix helpers. Declaring more than one public method on a Command raises TypeError at class creation. See examples/order.py for the canonical form.

Human-in-the-loop approval (Expense namespace)

  • LLM extracts expense data; policy checker auto-approves small expenses or pauses with Interrupted for manager review.
  • Resume with an Approve or Reject command.
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Expense["Expense namespace"]
        direction LR
        Approve{{Approve}}:::cmd
        Approved(Approved):::devt
        Invalidated(Invalidated):::devt
        Reject{{Reject}}:::cmd
        Rejected(Rejected):::devt
        Submit{{Submit}}:::cmd
        Submitted(Submitted):::devt
    end
    ApprovalRequired([ApprovalRequired]):::syst
    _e0_[ ]:::entry ==> Reject
    _e1_[ ]:::entry ==> Submit
    Submit --> Submitted
    Submit --> Invalidated
    Approve --> Approved
    Reject --> Rejected
    Submitted -->|"check_policy [orchestrate]"| Approve
    Submitted -->|check_policy| ApprovalRequired
    linkStyle 6 stroke:#0369a1,stroke-width:3px
Namespaces:
  Expense
    Command: Submit
      β†’ Submitted
      β†’ Invalidated
    Command: Approve
      β†’ Approved
    Command: Reject
      β†’ Rejected
System events:
  ApprovalRequired
Policies:
  check_policy  (Submitted β†’ Approve, ApprovalRequired)
Causal notes:
  Submitted β†’ Approve  via check_policy  [orchestrate]
Seed events:
  Reject
  Submit

Full code Β· Raw diagrams on GitHub

Tool-calling + AG-UI (Conversation namespace)

ReAct tool-calling agent wired end-to-end to AG-UI frontend tools (CopilotKit useFrontendTool).

  • Conversation.Send enforces content moderation before the LLM sees the message.
  • Frontend-declared tools bound to the LLM via build_langchain_tools.
  • Tool calls stream as ToolCallStart/ToolCallArgs/ToolCallEnd; results return via detect_new_tool_results β†’ ToolsExecuted.
  • DomainEvent + MessageEvent mixin with message_reducer().
  • For the handler-initiated FrontendToolCallRequested pattern, see AG-UI.
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Conversation["Conversation namespace"]
        direction LR
        Blocked(Blocked):::devt
        Send{{Send}}:::cmd
        Sent(Sent):::devt
    end
    AnswerProduced[/AnswerProduced/]:::intg
    LLMResponded[/LLMResponded/]:::intg
    ToolsExecuted[/ToolsExecuted/]:::intg
    _e0_[ ]:::entry ==> Send
    _e1_[ ]:::entry ==> ToolsExecuted
    Send --> Sent
    Send --> Blocked
    Sent -->|call_llm| LLMResponded
    ToolsExecuted -->|call_llm| LLMResponded
    LLMResponded -->|finalize_answer| AnswerProduced
%% Side-effect handlers: audit_trail (Auditable)
Namespaces:
  Conversation
    Command: Send
      β†’ Sent
      β†’ Blocked
Integration events:
  ToolsExecuted
  LLMResponded
  AnswerProduced
Policies:
  call_llm  (Sent, ToolsExecuted β†’ LLMResponded)
  finalize_answer  (LLMResponded β†’ AnswerProduced)
  audit_trail  (Auditable)  [side-effect]
Seed events:
  Send
  ToolsExecuted

Full code Β· Raw diagrams on GitHub

Supervisor fan-in (Task namespace)

  • Task.Run kicks off the supervisor loop; supervisor dispatches sub-commands Task.Research / Task.Code or emits the terminal Task.Finalized fact.
  • Custom Reducer folds specialist outputs into shared context.
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Task["Task namespace"]
        direction LR
        Code{{Code}}:::cmd
        Completed(Completed):::devt
        Finalized(Finalized):::devt
        Produced(Produced):::devt
        Research{{Research}}:::cmd
        Run{{Run}}:::cmd
    end
    _e0_[ ]:::entry ==> Run
    Run -->|"supervisor [orchestrate]"| Research
    Run -->|"supervisor [orchestrate]"| Code
    Run -->|supervisor| Finalized
    Completed -->|"supervisor [orchestrate]"| Research
    Completed -->|"supervisor [orchestrate]"| Code
    Completed -->|supervisor| Finalized
    Produced -->|"supervisor [orchestrate]"| Research
    Produced -->|"supervisor [orchestrate]"| Code
    Produced -->|supervisor| Finalized
    Research --> Completed
    Code --> Produced
%% Side-effect handlers: audit_trail (Auditable)
    linkStyle 1,2,4,5,7,8 stroke:#0369a1,stroke-width:3px
Namespaces:
  Task
    Command: Run  (handlers: supervisor)
    Command: Research
      β†’ Completed
    Command: Code
      β†’ Produced
    Event: Finalized
Policies:
  audit_trail  (Auditable)  [side-effect]
Causal notes:
  Run β†’ Research  via supervisor  [orchestrate]
  Run β†’ Code  via supervisor  [orchestrate]
  Completed β†’ Research  via supervisor  [orchestrate]
  Completed β†’ Code  via supervisor  [orchestrate]
  Produced β†’ Research  via supervisor  [orchestrate]
  Produced β†’ Code  via supervisor  [orchestrate]
Seed events:
  Run

Full code Β· Raw diagrams on GitHub

Scatter fan-out (Batch namespace)

  • Batch.Summarize fans out to per-document work via Scatter[DocDispatched].
  • Gather handler uses EventLog.filter() to wait for all DocSummarized facts, then emits Batch.Summarized (namespace-level sibling β€” gather isn't Summarize.handle(), so the outcome can't be Command-private).
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Batch["Batch namespace"]
        direction LR
        DocDispatched(DocDispatched):::devt
        DocSummarized(DocSummarized):::devt
        Summarize{{Summarize}}:::cmd
        Summarized(Summarized):::devt
    end
    _e0_[ ]:::entry ==> Summarize
    Summarize -.->|split_batch| DocDispatched
    DocDispatched -->|summarize_one| DocSummarized
    DocSummarized -->|gather_summaries| Summarized
%% Side-effect handlers: audit_trail (Auditable)
    linkStyle 1 stroke:#7c3aed,stroke-width:2.5px,stroke-dasharray:8 3
Namespaces:
  Batch
    Command: Summarize  (handlers: split_batch; scatters Scatter[DocDispatched])
    Event: DocDispatched
    Event: DocSummarized
    Event: Summarized
Policies:
  summarize_one  (DocDispatched β†’ DocSummarized)
  gather_summaries  (DocSummarized β†’ Summarized)
  audit_trail  (Auditable)  [side-effect]
Seed events:
  Summarize

Full code Β· Raw diagrams on GitHub

Safety gates + streaming (Content namespace)

  • Inline Content.Process.handle classifies text (keyword-based β€” no LLM).
  • External reactors gate approval, emitting Content.Blocked (Halted subtype) or Content.Approved.
  • Live streaming via astream_events().
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Content["Content namespace"]
        direction LR
        Analyzed(Analyzed):::devt
        Approved(Approved):::devt
        Blocked([Blocked]):::halt
        Classified(Classified):::devt
        Process{{Process}}:::cmd
    end
    _e0_[ ]:::entry ==> Process
    Process --> Classified
    Classified -->|gate| Blocked
    Classified -->|gate| Approved
    Approved -->|analyze| Analyzed
Namespaces:
  Content
    Command: Process
      β†’ Classified
    Event: Blocked  [Halted]
    Event: Approved
    Event: Analyzed
Policies:
  gate  (Classified β†’ Blocked, Approved)
  analyze  (Approved β†’ Analyzed)
Seed events:
  Process

Full code Β· Raw diagrams on GitHub

Retries & escalation (Question namespace)

  • Question.Ask declares raises=(RateLimitError,); its inline handle() may raise.
  • A rate-limit catcher re-issues Question.Ask with attempt+1, looping through Ask.handle().
  • Ask.Answered stays Command-private (produced only by Ask.handle()); chained catchers escalate to Question.GaveUp (Halted subtype) after MAX_ATTEMPTS.
graph LR
    classDef entry fill:none,stroke:none,color:none
    classDef cmd fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef devt fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef intg fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
    classDef syst fill:#fef3c7,stroke:#b45309,color:#78350f
    classDef halt fill:#fef3c7,stroke:#b45309,color:#78350f,stroke-width:3px,stroke-dasharray:4 2
    classDef inv fill:#ffedd5,stroke:#c2410c,color:#7c2d12
    subgraph Question["Question namespace"]
        direction LR
        Answered(Answered):::devt
        Ask{{Ask}}:::cmd
        GaveUp([GaveUp]):::halt
    end
    HandlerRaised([HandlerRaised]):::syst
    Ask -.->|"(raises)"| HandlerRaised
    Ask --> Answered
    HandlerRaised -.->|"backoff_and_retry (raises)"| HandlerRaised
    HandlerRaised -->|"backoff_and_retry [orchestrate]"| Ask
    HandlerRaised -->|give_up| GaveUp
    linkStyle 0,2 stroke:#6b7280,stroke-dasharray:3 3
    linkStyle 3 stroke:#0369a1,stroke-width:3px
Namespaces:
  Question
    Command: Ask  (raises RateLimitError)
      β†’ Answered
    Event: GaveUp  [Halted]
System events:
  HandlerRaised
Policies:
  backoff_and_retry  (HandlerRaised β†’ Ask)  [raises QuotaExhaustedError]
  give_up  (HandlerRaised β†’ GaveUp)
Causal notes:
  HandlerRaised β†’ Ask  via backoff_and_retry  [orchestrate]

Full code Β· Raw diagrams on GitHub