Most AI-assisted tooling works in a single round trip. You send a request, the model produces an answer, you act on it manually. That model works well for queries. It starts to break down for maintenance.
Imagine you have forty wiki pages. The lint agent has marked twelve of them stale because their source files changed. The right response is to re-ingest each source file, wait for the job to complete, then run lint again to promote the freshly-ingested pages back to active. That's a well-defined sequence of steps. It's also tedious, each step requires a separate command, you have to wait for each job to finish before issuing the next, and you still have to monitor the result and decide if anything needs a follow-up.
An LLM can describe what to do. What you actually want is something that does it.
This is the reasoning behind the Agentic Maintenance Workflows we shipped in Synthadoc v1.2.0.
Static Intent vs. Adaptive Workflow
Synthadoc already had an ActionAgent before v1.2.0. It handled conversational requests like "activate the alan-turing page" or "show the lint report" by extracting structured intent from free text, executing a single action against the wiki, and returning a human-readable result. The agent was good at one-shot operations with a clear endpoint.
What it couldn't handle was anything that required waiting for state to change and branching based on the result. "Re-ingest all stale pages" isn't one action, it's a sequence that depends on knowing which pages are stale, getting user confirmation before touching anything, running one ingest job per page and waiting for each to complete, then running a lint pass on the result. The outcome of each step determines whether the next step should proceed.
The difference between the two cases is: in the first, the model extracts an intent and the code executes it. In the second, the model drives a loop — calling tools, observing results, deciding what to call next. The model isn't just a parser; it's an orchestrator.
Building a robust version of that second pattern is most of what the v1.2.0 agentic work was about.
ActionAgent as the Orchestrator
The ActionAgent already ran a simple agentic loop for single-step intents: system prompt → LLM response → parse action → execute → format result. For workflows, we extended that loop to support tool calls: the model can emit a structured tool_call in its response, the agent executes the named tool, returns the result to the model, and the model decides what to do next. This continues until the model produces a plain-text summary with no pending tool calls, which terminates the loop.
The loop has two hard constraints that we wired in from the start. Maximum 30 tool calls per action, which is enough for re-ingesting a wiki of reasonable size while preventing runaway execution. And a 120-second confirmation timeout, if the user doesn't respond to a confirm_request event within two minutes, the workflow treats it as declined and terminates cleanly. Both of these came from thinking about what failure looks like rather than what success looks like.
A single tool failure doesn't abort the workflow. If one page in a bulk re-ingest fails, the remaining pages still get processed, and the failure is recorded in the final summary. This matters for Workflow A - re-ingesting twelve stale pages and having one fail due to a temporary file lock shouldn't leave the other eleven untouched.
One thing that required careful thought: the action agent needs to route the user's intent to the correct workflow without ambiguity. We built two routing paths. For bulk re-ingest ("re-ingest stale pages", "fix stale pages"), intent extraction goes through the LLM. For by-slug re-ingest ("re-ingest the alan-turing page"), we intercept the request with a compiled regex before the LLM sees it at all. The slug-specific case is common enough, and the phrasing predictable enough, that we didn't want to pay the latency and risk of an LLM classification call for something a regex can handle reliably.
Pluggable Workflow Design
We knew from the start that "re-ingest stale pages" wouldn't be the only workflow we'd want. Lint-based promotion, dead-link cleanup, bulk archiving, all of these follow the same structure: discover scope, confirm, execute steps, summarise. Rather than hardcode each workflow into the action agent, we designed an abstract plugin interface.
The two foundational types are WorkflowContext and AgenticWorkflow. The context is a dataclass that carries everything a workflow needs: the session ID, the wiki root, the job queue, the storage layer, the audit database, an send_sse_event function for real-time progress, and the confirm registry for blocking on user decisions. The AgenticWorkflow base class declares three abstract methods: build_system_prompt(), build_initial_message(), and get_tool_fns(), and that's all a concrete workflow needs to implement. The loop machinery lives in the action agent.
IngestLintWorkflow is the first concrete implementation. It covers both execution paths: Workflow A (bulk stale re-ingest) and Workflow B (per-slug re-ingest). Both paths share the same tool set:
| Tool | What it does |
find_stale_pages | Returns all stale pages with their local source paths |
find_page_source | Looks up any page by slug regardless of lifecycle state |
ingest_source | Force-ingests a source file; blocks until the job reaches a terminal state |
poll_job | Polls a job with exponential backoff until complete |
run_lint | Enqueues a full lint pass; returns a job ID |
confirm | Sends aconfirm_request SSE event; blocks until the user responds |

The system prompt for IngestLintWorkflow sets out the rules of operation: discover scope first, always call confirm before touching any page, process pages one at a time, run lint after all ingests complete, and produce a plain-text summary that includes every outcome. The model follows these rules because they're baked into the system prompt it operates under for this workflow, not because of any hardcoded branching in the agent.
Adding a new workflow means implementing AgenticWorkflow, registering it, and writing a system prompt. The routing logic and tool execution loop are inherited for free.
Real-Time Monitoring in the Web UI
A workflow that takes three minutes to complete and only shows output at the end is frustrating to use. The SSE protocol extensions we added in v1.2.0 address this.
Three new event types flow from the action agent to the web UI during a workflow run. tool_progress fires at each tool step, carrying the tool name, an optional job ID, and a short status message, this is what drives the inline progress display in the chat UI. confirm_request carries the payload for the Yes/No confirmation card the UI renders when the workflow calls confirm. And done.pre_prompt is an optional string field in the terminal done event that pre-fills the chat textarea with the logical next action, after a re-ingest completes, for example, the pre-fill suggests "Run lint to promote re-ingested pages to active."
The CLI command line surfaces the same SSE events through the query modal's progress display, so the agentic workflow experience is consistent whether you're working in the web UI or the CLI command line.
The Graph tab in the web UI takes this a step further. Every node in the knowledge graph has a detail panel with a Maintenance section showing two chips: "Check this page for issues" (which routes to a lint analysis) and "Re-ingest this page" (which triggers Workflow B for that node). These chips send a pre-formed chat message with the exact phrase the regex fast-path recognises, so the workflow starts without any typing or intent parsing.

Adding a New Workflow
The architecture is intentionally open. Here's what a new workflow looks like in practice.
class DeadLinkCleanupWorkflow(AgenticWorkflow):
def build_system_prompt(self) -> str:
return DEAD_LINK_SYSTEM_PROMPT
def build_initial_message(self, intent: str) -> str:
return f"Scan the wiki for dead wikilinks and surface broken references."
def get_tool_fns(self) -> list[Callable]:
return [tool_find_dead_links, tool_confirm, tool_archive_page, tool_run_lint]
Register it in the workflow router, add the routing phrase, write the system prompt, and the loop machinery handles execution. The workflow inherits confirm support, SSE progress events, the 30-call limit, and the 120-second confirmation timeout automatically.
The reason this pattern works is that the loop logic — tool dispatch, result injection, termination detection — is completely generic. The only workflow-specific parts are the tools it can call and the instructions it operates under. Everything else is shared infrastructure.
What We Learned, and Where This Goes
Building agentic workflows into a tool-call loop surface area taught us a few things.
Confirmation is load-bearing. The confirm tool isn't a courtesy, it's a safety requirement. Without it, the workflow would discover twelve stale pages and immediately start re-ingesting them without giving the user a chance to review the scope. Adding a user-approval gate before any destructive operation turns the workflow from "something that does things to your wiki" into "something that shows you what it's about to do, then does it with your permission." The 120-second timeout defaults to declined rather than approved, for the same reason.
Per-step tool progress makes everything feel faster. A workflow that takes ninety seconds with no intermediate output feels like it hung. The same workflow with a progress event at each tool step, "Ingesting alan-turing... done. Ingesting ada-lovelace... done." , feels responsive. The actual duration is identical. The perceived latency is completely different.
System prompts are the policy layer. The rules governing how a workflow operates — what order to call tools in, when to stop, how to format the summary — live in the system prompt. This means they're human-readable, version-controlled, and adjustable without touching the execution machinery. When we needed to change the ordering behaviour for Workflow B (look up source path before confirming, not after), we changed four lines of the system prompt and redeployed. No code change.
The natural next extensions are workflows that can operate on a schedule, a nightly stale-page sweep that runs unattended, and workflows that can initiate from lint signals rather than user requests. Both require the same pluggable pattern; they just need a different trigger mechanism. That work is in the roadmap for a future release.
See It Running
The walkthrough below covers the full loop end to end: two stale pages, the confirmation gate, per-page ingest progress, lint auto-run, and the final page-state report showing whether each page reached active.
📺 Synthadoc Agentic Maintenance Workflow — YouTube