Multi-Task (DAG) Routing

Most assistants answer a complex request with a single AI call. Synaplan does something different: a small planner model turns the request into a DAG — a directed acyclic graph of tasks — and executes the steps in order, streaming a live task card for each one. Ask for several things at once and you get several real outputs back, on whatever channel the message arrived on.

This is the deep dive. For where it sits in the wider system (the Docker service map, SSE vs WebSocket streaming, the realtime layer) see Architecture & Realtime.


Why a plan instead of one call

A single prompt often hides several distinct jobs. "Summarise this PDF, translate the summary to German, and read it aloud" is three capabilities, not one. A flat chat completion has to fake that in one pass; a plan does each step with the right tool and lets you watch it happen.

Single AI call Multi-task DAG plan
One model, one answer The right capability per step (RAG, search, media, document, calendar, …)
Files? Usually one, or none Multiple generated files from one request
Opaque — you wait, then a wall of text Transparent — a live card per step shows pending → running → done
Hard to extend A typed capability registry you can grow

Simple requests still take the classic fast path — one classifier decision, one handler, no planner overhead. The planner only engages for requests that the AI sorter sees as genuinely multi-step, and only when an administrator has enabled it.


A worked example

Prompt: "Can you create a short paragraph about DAG routing in AI models and create a reminder calendar entry for tomorrow at 10am?"

The planner emits a two-node plan. The chat UI shows a Task plan · 2/2 card while it runs, then leaves both results in place:

  • Answer — a streamed text paragraph (chat capability).
  • Calendar invite — a downloadable .ics meeting file for tomorrow 10:00, with the date resolved to an absolute time (calendar_event capability).
                  ┌──────────────────────────────┐
  user request ──▶│  TaskPlanner (planner model  │
                  │  → validated JSON DAG)        │
                  └───────────────┬──────────────┘
                                  ▼
     ┌──────────────── DAG, executed in topological order ──────────────┐
     │   n1: chat ───────────────┐                                       │
     │                           ▼                                       │
     │   n2: calendar_event ──▶ compose_reply  (terminal reply node)     │
     └───────────────────────────────────────────────────────────────────┘
                  │   live SSE cards: plan · task_update · task_chunk · task_file
                  ▼
          Answer text   +   meeting_YYYYMMDD_HHMMSS.ics

The relative date ("tomorrow at 10am") is resolved by injecting the current time into the planner prompt, so the .ics lands on the right day in the right timezone — no manual date math.


Capabilities

Each DAG node runs exactly one capability. The planner may only emit capabilities from this fixed, validated set — there is no arbitrary code execution.

Capability Does
extract_text Read text from an uploaded attachment (Tika / OCR / Whisper)
chat A normal text answer
summarize Summarise input text
translate Translate input text
rag_query Semantic search over your knowledge base
web_search Live web search with a generated query
url_fetch Read a specific URL named in the message (robots.txt-compliant); with compare: true it keeps one saved copy and returns the diff — see Watched pages below
mcp_fetch Pull live data from your connected MCP servers
mcp_action Call a write-class tool on an MCP server with allow write actions on — creates a ticket or page; waits for your approval when the policy says so
tool_call Call one of your custom HTTP / OpenAPI tools from the registry
email_search Read-only search over your connected IMAP mailboxes and Microsoft 365
file_analysis Vision / OCR / document question-answering
image_generation Generate an image (the /pic path)
video_generation Generate a video (the /vid path)
text2sound Text-to-speech audio
document_generation Build a CSV / XLSX / DOCX / PPTX file
document_export / document_combine Export to PDF or combine several documents into one (needs the office engine)
code_run Short Python / Node file work on copies of chosen files — only with Secure compute on
calendar_event Build an .ics calendar invite
email_me Email the result to the account owner
save_to_folder Upload generated files to a connected Nextcloud / OpenCloud / WebDAV folder
condition / outbound_webhook Saved-task Steps only: branch on a previous result, or POST results to a URL you own

The save_to_folder node routes by channel name: every folder you connect under Manage → Connections → Connected apps gets a prompt-safe name (e.g. nextcloud), and the planner places the node when you say things like "…and save it to my Nextcloud". So "make an image of a cat and save it to Nextcloud" becomes a two-node plan — generate, then deliver into your own storage.

A hidden compose_reply node always terminates the graph — it assembles the final text plus any attachments into the single reply you see.

Steps pass data along the edges with a small reference grammar ($message.text, $n1.text, $n1.file, …), so one node can consume what an earlier node produced.

The data nodes (web_search, url_fetch, mcp_fetch, email_search) share one contract: planner-placed (never speculative), read-only, isolated failure, timeout-bounded, and every outbound fetch goes through one shared SSRF guard. A flag-disabled data node is omitted from the planner's catalog entirely — the planner can't even hallucinate it.


How a plan executes

  1. Plan — the planner model returns JSON; Synaplan validates it (known capabilities only, valid dependencies, no cycles, a sane node cap, a valid reply node). Invalid output safely falls back to a single chat answer.
  2. Execute — nodes run in topological (dependency-first) order. With parallel mode enabled, independent media nodes (image/video/audio) are offloaded to concurrent subprocesses while text nodes stream inline.
  3. Isolate failures — if a node fails, only the steps that depended on it are skipped; the rest of the plan still delivers. If everything fails, the turn falls back to the classic single-handler path.
  4. Assemble & deliver — results (including multiple files) are delivered on the originating channel: chat, widget, WhatsApp, email, or webhook. Web chat additionally persists the card states, so the task plan is still there after a reload.

Live progress events (SSE)

Task plans stream over the same Server-Sent Events channel as normal answers (/api/v1/messages/stream). Alongside the answer tokens you get plan events:

Event (status) Fires when Key fields
plan A multi-node plan starts the node list (node_id, capability, kind), reply_node
task_update A node changes state node_id, state (pendingrunningdone / failed / skipped)
task_chunk A text node streams a token node_id, chunk
task_file A node produced a file node_id, type, url
task_progress A long media render advances node_id, percent, provider status

See Code Examples for the SSE client pattern.


Enabling & configuring

Multi-task routing is controlled per user under Manage → Assistants → Routing (/ai/routing) in the app; administrators set the instance default under Operate → System configuration → Routing and per-group values under Operate → People → Policies. Existing installs keep the classic single-handler fast path until it is switched on, and a shadow mode can plan without executing so operators can review what the planner would do before turning it loose.

Behind the scenes these map to the MULTITASK configuration group (ROUTING_ENABLED, SHADOW_MODE, PARALLEL_ENABLED, MAX_PARALLEL, NODE_TIMEOUT) plus the classifier's fast-path flag. The external data nodes have their own flags: MULTITASK.URL_FETCH_ENABLED and MULTITASK.MCP_FETCH_ENABLED + MCP.CLIENT_ENABLED ship on (seeded on deploy, operator-overridable), MULTITASK.EMAIL_SEARCH_ENABLED ships off. See MCP — Server & Client for the client configuration, and docs/DEVELOPMENT.md in the main repository for developer notes.

The routing cascade

Before the planner runs, the message has to be classified: which topic, which language, does it need web search. That decision used to be one AI call that parsed free-form JSON. It is now a cascade of layers, cheapest first, each of which may answer with confidence or defer to the next:

Layer Flag Default What it does
Fast path CLASSIFIER.FAST_PATH_ENABLED off Heuristic: trivial chat messages skip the AI sorter entirely
Embedding router EMBEDDING_ROUTER.ENABLED (CONFIDENCE_THRESHOLD 0.88) off Compares the message embedding against anchors for the system topics; a confident match skips the sorter
AI sorter, schema-enforced STRUCTURED_OUTPUT.ENABLED on The sorter, planner and extraction prompts send a JSON schema to providers that support it, so the model cannot return a malformed decision
Native tool routing NATIVE_TOOL_ROUTING.ENABLED off Folds "what kind of request is this?" into the answering call as tool choices — one call instead of two for plain chat

The sorter is self-healing: when a provider rejects or garbles the schema response, Synaplan salvages what it can, retries once with a correction, and otherwise falls back to the general topic instead of failing the turn. All new layers ship off; structured output is on and falls back per provider to the previous behaviour. Routing snapshots are unchanged with the defaults.


Saved Tasks — run a plan again, on a schedule

A good plan is worth repeating. Saved Tasks turn a one-off instruction into a reusable, schedulable task:

  • Save from chat — when a multi-step plan finishes, a clock icon appears on the plan card. Click it, give the task a name, and the exact instruction is pinned as a Saved Task.
  • Manage centrally — all tasks live under Manage → Automations → Saved tasks: run them on demand ("Run now"), inspect the run history, or open the task's results.
  • Schedule them — run a task manually, every 15/30/60 minutes, daily, or on selected weekdays. Tasks that write somewhere (email, folders) ask for an explicit unattended-run confirmation before they may run without you.

Each Saved Task gets its own chat: every run appends the incoming instruction and the assistant's reply there, so you can always see what the last run produced. Generated artefacts (images, documents, audio, .ics invites) additionally land in the file manager's Generated gallery, filterable by type. A task that fails three runs in a row is auto-paused and the owner is notified by email.

The feature is gated by the SAVEDTASKS / ENABLED config flag — seeded on for new installs; operators can toggle it in the admin system configuration. The chat widget never runs Saved Tasks. Deleting a task asks for confirmation and removes its shares; only the owner can delete it.

Watched pages — mail me what changed

A scheduled task could read a URL but could not tell whether the page had changed since last time. Watched pages closes that gap with one saved copy per address:

  1. In chat: "get this URL, save it, compare it to the last version and mail me the differences". The planner emits url_fetch with compare: true plus email_me.
  2. Save that chat as a daily Saved Task.
  3. The first run mails "First copy saved."; every later run mails a unified diff, or "No changes since the last check."

The saved copies are managed on the Saved Tasks page under Watched pages: Watch this page, View saved copy, Check now, Stop watching. One row per owner and address — watching the same URL twice reuses it. Deleting a Saved Task does not delete the watch; stopping a watch does not stop the task. No history is kept (the copy is overwritten on every compare), text is capped at 64 000 characters and the diff at 200 lines. Ordinary "summarize this URL" still returns the page, not a diff. The same MULTITASK.URL_FETCH_ENABLED switch turns both off.

Self-hosting? Schedules only fire if the scheduler tick is running — see Quickstart → Scheduled Saved Tasks for the one container (or cron line) you need.


Extending the graph

Capabilities are a typed registry, not a hardcoded prompt: a Capability value plus a tagged task-runner service. Today's set are thin adapters over capabilities Synaplan already runs in production, which keeps the graph safe and predictable.

Now shipping: open DAG endpoints via MCP. The mcp_fetch node hands off to your own MCP serversn8n workflows, internal APIs, any Streamable HTTP MCP endpoint — so the planner can orchestrate the self-hosted stack you already operate. The AI does the planning; your tools do the work, on your infrastructure. Connect servers under Manage → Connections → MCP Servers and allow them per assistant — see MCP — Server & Client. (Reads run on their own; write actions need allow write actions on that server plus your approval; destructive tools are refused.)


See also