Skip to content

Workflow hooks

A great app the tenant agent never invokes is invisible. Workflow hooks let your manifest declare, in plain language, when the orchestrator should reach for your tools — same mechanism the platform’s first-party apps (Office Assistant, Sachverständiger) use internally, now opened up to partner manifests.

This is the missing piece that turns “I shipped a tool with a good description” into “the agent reliably picks my tool when the user asks for X.”

┌─ User: "Wie buche ich diese Rechnung?" ──────────────────────┐
│ │
│ 1. Orchestrator reads system prompt, which includes: │
│ - Custom agent catalog (your app's specialist agent) │
│ - Workflow routing reference (your app's workflows) │
│ │
│ 2. Orchestrator matches user's request against: │
│ - intent: "User forwards an invoice and asks how to │
│ book it" │
│ - triggers: ["wie buche ich diese rechnung", ...] │
│ │
│ 3. Orchestrator delegates to your specialist agent. │
│ │
│ 4. Specialist agent reads its own system prompt, which now │
│ includes the workflow's flow + specifics: │
│ - flow: ["classify_invoice(...)", "email_send(...)"] │
│ - specifics: "Always cite category in plain German..." │
│ │
│ 5. Agent executes the flow. │
└──────────────────────────────────────────────────────────────┘

Two separate audiences read your workflow:

  • The orchestrator sees intent, triggers, non_triggers — routing-only.
  • The specialist agent (your app’s executor) sees flow, specifics — execution-only.

A minimal workflow:

workflows:
- id: classify_inbound_invoice
name: Classify inbound invoice
intent: User forwards or pastes an invoice and asks how to book it
flow:
- "classify_invoice(text=invoice_text) → category"
- "email_send(to=user.email, body='Buchungs-Empfehlung: ...')"

Add routing cues, tool requirements, and channel restrictions as your app matures:

workflows:
- id: classify_inbound_invoice
name: Classify inbound invoice
intent: User forwards or pastes an invoice and asks how to book it
triggers:
- "Inbound email contains invoice attachment or invoice text"
- "User asks 'wie buche ich diese rechnung'"
non_triggers:
- "Just a request for general tax advice with no document"
flow:
- "classify_invoice(text=invoice_text) → category"
- "If category == 'unclear': ask follow-up"
- "Else: email_send(to=user.email, body='Buchungs-Empfehlung: ' + category)"
specifics: |
Keep responses under 100 words. Always cite the chosen category
in plain German. If the invoice has no clear date, ask for it
before booking.
tools_required: [classify_invoice, email_send]
channels: ["*"]

Full field reference: Manifest → workflows.

The platform filters workflows out of the prompt if any declared tools_required isn’t available to the tenant — scope not granted, integration not connected, or the tool simply doesn’t exist on the platform.

This avoids the failure mode where the agent confidently follows your workflow, hits decision='scope_denied' partway through, and leaves the user with a half-finished task. List every tool your flow uses (platform tools and your own app.tool(...) declarations) in tools_required and the orchestrator only sees workflows that’ll actually work for the current tenant.

tools_required:
- email_send # platform tool
- calendar_create_event # platform tool
- my_app_classify # tool declared in this manifest's tools[]

Workflows without tools_required (or with an empty list) pass through unchanged — first-party apps work this way today and continue to.

Some workflows only make sense on specific channels. A voice-driven flow doesn’t fit chat, a long-form drafting flow doesn’t fit WhatsApp’s character limits.

channels: ["whatsapp", "telegram"] # only these
channels: ["*"] # all (default)

Workflows whose channels excludes the current channel never reach the agent’s prompt for that conversation. Default ["*"] means “available everywhere.”

Tenants can override individual workflow fields per-install via the platform’s settings UI. The merge is keyed on id:

  • Override matches a default id → deep-merge, override fields win. Common case: tenant rewrites specifics to match their brand voice, or adds an extra step to flow.
  • Override id has no default match → it’s a tenant-defined workflow added on top of yours.
  • Defaults without an override → pass through unchanged.

You ship sensible defaults; the tenant tweaks what they need. Your upgrade doesn’t clobber their tweaks.

Workflows are part of the manifest, so they version with your app. A tenant on v0.1.0 keeps loading v0.1.0’s workflows until they explicitly upgrade — same semantics as scopes, tools, and bundles.

If a workflow becomes critical to your app’s behaviour, document the version it was introduced in your README. The settings UI shows both default and override side-by-side so tenants can see what changed across upgrades.

- id: send_recap
name: Send recap email
intent: User asks for a daily recap to be emailed
triggers: ["mail me my open items", "send me a recap"]
flow:
- "list_open_items()"
- "email_send(to=user.email, subject='Daily recap', body=summary)"
tools_required: [email_send]

"On schedule, do this even without a user request”

Section titled “"On schedule, do this even without a user request””

Schedule workflows live in lifecycle.schedules[], not in workflows[]. workflows[] is for orchestrator routing on user-driven requests. See Lifecycle hooks for schedule-driven flows.

”Tool X requires preconditions; check first”

Section titled “”Tool X requires preconditions; check first””
- id: book_meeting
name: Book meeting from request
intent: User asks to schedule a meeting
triggers: ["schedule a meeting", "book a call"]
flow:
- "calendar_list_events(from=today, to=today+7) → busy"
- "Find first free 30-min slot"
- "calendar_create_event(start=slot, end=slot+30min, attendees=[...])"
specifics: |
Default duration is 30 minutes. If the user doesn't specify
attendees, ask before scheduling. Avoid 12:00–13:00 for German
tenants (lunch).
tools_required: [calendar_list_events, calendar_create_event]
  • They don’t bypass scope checks. A workflow listing tools_required: [email_send] still fails at runtime if the tenant didn’t grant mail.send. The filter just keeps the agent from seeing the workflow in the first place.
  • They don’t override platform safety gates. risk_level: high tools still gate on the Security Gate regardless of what your workflow says.
  • They don’t replace tool description. The agent still reads every tool’s description. Workflows are additional routing context, not a substitute.