From Manifesto to Code: How argentic-mw Runs Its Agents
In the first post of this series, I laid out a definition and a position: a supervised agent is a scheduled worker that executes a deterministic plan, where the LLM steps in only at extension points where it is useful, and where everything else is explicit, traceable, replayable code. Four pillars followed, determinism, control, auditability and token efficiency, which I presented as properties verifiable on a trace rather than as slogans. In the second post, I proposed a six-criterion decision framework to compare this approach to full-LLM platforms and orchestration frameworks, and I concluded that for scheduled business pipelines with audit and bounded-cost requirements, the custom core remained the coherent answer, even at the price of a real boot cost.
One question remains, and it is the most concrete: do those promises hold in the code, or do they stay an elegant manifesto that fades on contact with implementation? The point of this post is to answer with practice. I will describe the common foundation, the Core, that carries the platform’s three agents, then walk through the three pipelines as so many incarnations of the four pillars. There will be a schema, an extract of the rule table and some pseudo-code, because at this stage of the series, abstraction has done its job and it is the concrete mechanisms that decide whether the vision holds or dissolves.
The Core, the common foundation
The Core establishes the bases needed for everything else, and it is designed to be assimilated in an afternoon. The architectural decision that defines it, ADR 0001, starts from a simple observation: for three cron agents with linear pipelines, a workflow engine would be a disproportionate investment, and an orchestration framework like LangGraph would impose its abstractions without delivering the business core I need. I detailed that reasoning in the decision framework of the second post (see When to choose an all-in-one LLM platform, when to build custom): the middle path reduces the boot cost on orchestration, but traces, replay, the validation queue and the type-instance distinction remain to build on top. Rather than rebuild a supervised core around a framework, I write it directly, in about two hundred lines, and it does exactly what the agents ask, no more, no less.
The heart of the Core is a pipeline executor that fits in one loop. A pipeline is an ordered list of Steps, and execution is a for step in steps that calls each Step in turn, traces its input and output, and persists the whole thing to the database. There is no implicit graph, no hidden node, no LLM-driven loop that would decide to re-iterate. The flow is readable in a file, and that readability is what makes audit possible.
flowchart TD
SCHED[Scheduler<br/>Postgres leader<br/>lease TTL 30s] -->|dispatch run| W1[Worker stateless]
SCHED --> W2[Worker stateless]
SCHED --> W3[Worker stateless]
W1 --> EXEC[Pipeline executor<br/>for step in steps]
W2 --> EXEC
EXEC --> ST1[Step 1<br/>trace input/output]
ST1 --> ST2[Step 2<br/>trace + llm_usage?]
ST2 --> STN[Step N]
STN --> PG[(Postgres<br/>traces, replay cache,<br/>secrets, config, logs)]
The contract of a Step is deliberately minimal and defined by ADR 0002. Concretely, a Step is a typed async function: it takes an input, may produce side effects (HTTP call, database write, email send), then returns a structured output that the Core traces and persists.
The key constraint is idempotence. A Step must be replayable without duplicating its effects. In practice, that means upsert rather than insert, dedup keys for sends, or idempotent API calls (GET, or safe equivalents). It is not guaranteed by the type system, but by code discipline and review, and that is what makes replay reliable.
Replay relies on a cache indexed by run_id + step_index. When you replay a run, the executor consults the cache before calling a Step: if the output is already there, it returns it without re-executing the side effect, and if it is missing, for example after a crash mid-run, it re-executes the Step relying on idempotence to avoid any double effect. The replay endpoint does not create a new run_id, it resets the existing run to pending in place, which preserves trace continuity. A staleness sweep, driven by the scheduler leader, spots runs left at running with a heartbeat too old (beyond sixty seconds) and resets them to pending so they get picked up.
Tracing is the property that turns this loop into an auditable system. Every Step traces its input, output and metadata, and LLM Steps add an llm_usage block with the model name, input tokens, output tokens and latency. The probabilistic part of the system, the one that comes from the LLM, is therefore not diffuse: it is localized in named Steps, and its cost is measured per Step rather than per run. For an auditor, the trace of an execution is an ordered file, where every decision is attachable to a Step, a dated input and a persisted output. The Core does not distinguish LLM Steps from technical Steps at the type level, because they are the same functions, but their trace carries the usage, and that is enough to make the bill readable.
The scheduler is the piece that makes the whole thing operational without becoming a fragility point. It is designed active-passive with leader election via Postgres: several instances run on different nodes, only one is active at a given time, and leadership is acquired through a lease (advisory lock or lease table with a TTL on the order of thirty seconds). If the leader node goes down, another instance takes over at lease expiration, without human intervention. Workers are stateless, and all state (traces, artifacts, replay cache, validation queues) lives in the database. Adding capacity comes down to adding a worker on a new node, and any worker can execute any run. This is classic platform engineering, not AI, and that is precisely the point.
Secrets, finally, are encrypted in the database and injected into Steps via a typed Dependencies container, which also carries HTTP clients and database access. A Step never reads a secret in clear from the environment: it receives it through its dependencies, which makes the exposure surface readable and testable. The configuration of LLM Steps (model, consumption limits, system prompt) also lives in the database, editable via the control plane without redeployment, which separates behavior from code and lets you tune a prompt without opening a PR.
These mechanisms are not exotic. A for step in steps, an indexed cache, a lease-based leader election, encrypted secrets injected by dependency, that is the platform engineering you write when you refuse to buy a framework for three cron pipelines. The value is not in complexity, it is in readability: everything fits in two hundred lines, and everything reads.
Watch Agent, RSS/Atom watch
The Watch Agent is the first agent I wrote, and it is the one where the boundary between determinism and LLM is the most readable. Its pipeline is short: fetch-feeds → dedupe → classify(LLM) → summarize(LLM) → deliver. Five Steps, only two of which call the LLM, and the other three are deterministic code that needs no intelligence to do its job.
The deterministic Steps carry the business logic that does not need a model. fetch-feeds pulls the RSS and Atom feeds configured for the instance, with an async HTTP client and simple retry handling. dedupe compares new articles to those already persisted in the database, by URL or by content hash, and keeps only what has not already been processed. deliver sends the watch mail to the configured recipients and persists the articles for the trace. None of these Steps can surprise: if the feed responds, you get its articles; if the article is already known, you drop it; if the recipient is configured, you send. Behavior is predictable, and that is precisely what you ask of the non-LLM part of a supervised pipeline.
The LLM steps in at two places, where it is useful. classify(LLM) receives each article and assigns it a theme from a closed list configured per instance. summarize(LLM) produces a short summary of the content, with a strict output schema that bounds length and format. Both Steps are LLM Steps in the sense of ADR 0001: a Pydantic AI Agent with a Pydantic output_type, a deps_type for dependency injection, a model chosen for the task, and UsageLimits to bound consumption.
The point worth stressing, because it embodies token efficiency by construction, is that the two Steps do not necessarily share the same model. Theme classification is a bounded categorization task, which a cheap and fast model handles well. Article synthesis asks for a stronger model, able to render the content without impoverishing it. Configuring a cheap model for classification and a stronger model for synthesis is not an after-the-fact optimization, it is a property of the platform: every LLM Step carries its own configuration, and the model choice is local to the Step, not global to the agent. The same pipeline, run today and in six months, consumes the same order of magnitude of tokens, because the structure does not change and the limits are per Step.
The manifesto’s promises read directly in this pipeline. Cost is bounded by the UsageLimits of each LLM Step, and the bill is predictable to the Step. Auditability comes from the per-article trace: for every processed article, you find the classification Step’s input, the produced category, the model used, the tokens consumed, and the summary that followed. Replay comes from the per-Step cache: if the mail send failed, you replay the run, the classification and synthesis Steps return their outputs from the cache, and only the deliver Step re-executes its side effect. Control, finally, is trivial here: there is no risky action, just a mail send, and interrupting a run comes down to stopping execution.
Web Stats Agent, web statistics
The Web Stats Agent is the agent where per-task multi-modeling takes the most relief, because its pipeline chains three consecutive LLM Steps, each on a different register. Its pipeline: fetch-plausible → fetch-search-console → fetch-references-stats → aggregate → compare(LLM) → recommend(LLM) → suggest-references(LLM) → deliver. Eight Steps, three of them LLM Steps, and it is the compare → recommend → suggest-references chain that carries the system’s intelligence.
flowchart LR
A[fetch-plausible] --> B[fetch-search-console]
B --> C[fetch-references-stats]
C --> D[aggregate]
D --> E["compare<br/>(LLM Step)"]
E --> F["recommend<br/>(LLM Step)"]
F --> G["suggest-references<br/>(LLM Step)"]
G --> H[deliver]
The first four Steps are deterministic. fetch-plausible and fetch-search-console query the analytics of the tracked site via their respective APIs, fetch-references-stats pulls the statistics of the configured reference sites (by scraping or by API depending on what they expose), and aggregate consolidates all that into a comparable dataset. None of these Steps asks for judgment: they are typed HTTP calls, data transformations, and database persistence. The LLM part starts when the data is ready.
compare(LLM) receives the site’s statistics and those of the references, and produces a structured comparison: which indicators are below, which are above, which gap is notable. It is a series-reading task, which an intermediate model handles well, able to spot significant gaps without over-interpreting. recommend(LLM) receives the comparison and proposes improvement actions (work on such page, target such keyword, fix such tunnel). It is a synthesis task that asks for a stronger model, able to formulate an actionable, contextualized recommendation. suggest-references(LLM) proposes new reference sites to add to the pool, based on the site’s themes and the references already tracked.
The three Steps are configured independently, each with its model, its UsageLimits and its prompt. Token efficiency is built in that granularity: you do not pay for an expensive model on comparison, which is a reading task, and you do not under-equip recommendation, which is a synthesis task. The system does not rely on a single model for everything, which would be either expensive or underperforming, but on a model per task, calibrated to the task’s difficulty.
The most interesting control point in this agent is the human validation queue for reference suggestions. The suggest-references(LLM) Step produces proposals, but it does not add them to the tracked reference pool. The proposals enter a queue, and a human must approve them via the control plane (REST API or CLI) before they become active. That is the direct incarnation of the control pillar: the LLM proposes, the human decides, and the boundary is explicit in the pipeline. If a suggestion is bad or off-topic, it stays in the queue until rejection, and has no effect on subsequent runs. If it is good, the human approves it, and it joins the pool. The LLM never has the ability to expand its own tracking scope, which would be a control flaw in an autonomous system.
The promises materialize here too. Cost is bounded by the UsageLimits of the three LLM Steps, and multi-modeling avoids paying an expensive model on a trivial task. Auditability comes from the trace: for every recommendation, you find the input statistics, the produced comparison, the model used, and the chain that leads from comparison to recommendation. Replay works at the Step level, and the validation queue is itself persisted in the database, which makes the history of human decisions auditable on the same footing as the LLM’s decisions.
Zendesk Agent, tickets, rule table, auto vs draft
The Zendesk Agent is the longest agent to describe, because it carries the platform’s riskiest action: automatic reply to a client without human validation. Its pipeline: fetch-tickets → classify(LLM) → resolve-response-mode → resolve-routing → [auto] send-auto-response | [draft] draft-response(LLM) → deliver. It is also the agent where the separation between classification and decision is the cleanest, and that is where the rule table takes on its full meaning.
The starting point is a distinction I laid out in ADR 0003 and that I defend: precision comes from classification, which is where natural language needs to be understood, but the decision (auto or draft) comes from a rule, which sits where determinism is required. The LLM classifies the ticket into a category from a closed list, and a human-edited table maps every category to a response mode. The LLM never decides whether to reply automatically, it only produces a category, and that asymmetry is what makes the decision auditable.
flowchart TD
T[fetch-tickets] --> CL["classify(LLM)<br/>category + confidence"]
CL --> RM[resolve-response-mode<br/>lookup rule table]
RM -->|"confidence < threshold"| DR
RM -->|"category → auto + guardrails OK"| AUTO[send-auto-response<br/>interpolated template]
RM -->|"category → draft"| RT[resolve-routing<br/>support / presales]
AUTO --> LG[(auto_response_log<br/>unique dedup_key)]
RT --> DR["draft-response(LLM)"]
DR --> DL[deliver<br/>draft pending validation]
LG --> DL2[(persist trace)]
The rule table is a versioned file, editable by a human, that maps every category to a response_mode (auto or draft) and optionally to a response template. An extract is enough to show the structure:
| category | response_mode | response_template |
|---|---|---|
| prerequisites-missing | auto | templates/prerequisites.md |
| tech-ports | auto | templates/ports.md |
| tech-prerequisites | auto | templates/tech-prereq.md |
| billing-question | draft | n/a |
| bug-report | draft | n/a |
| feature-request | draft | n/a |
| how-to | draft | n/a |
| other | draft | n/a |
Adding a new auto-reply case means adding a row in the table and a template file. No prompt modification, no retraining, no redeployment. The table is the source of truth for the decision, and the LLM never has a say on it. The other category, mapped to draft, is the safety net: anything the LLM cannot classify ends up as a draft for human validation, never as an automatic reply.
The resolve-response-mode Step is the concretization of that logic, and its pseudo-code fits in about ten lines:
async def resolve_response_mode(
classification: TicketClassification,
ctx: StepContext,
) -> ResponseMode:
category = classification.category
confidence = classification.confidence
if confidence < ctx.config.confidence_threshold:
return ResponseMode.draft
row = ctx.rule_table.lookup(category)
if row is None or row.response_mode != "auto":
return ResponseMode.draft
if not ctx.guardrails.all_clear(category, ctx.run_id, ctx.agent_id):
return ResponseMode.draft
return ResponseMode.auto
The logic is deliberately defensive, and it reads in the order of conditions. If confidence is low, fall back to draft. If the category is not in the table or not marked auto, fall back to draft. If any guardrail does not pass, fall back to draft. The default state is draft, which is the safe state, and auto is reached only if all conditions are verified. That is called fail-closed, and for an action that touches a client, it is the only defensible posture.
The guardrails are the subject of the ADR 0003 amendment, and they deserve to be named because auto-response is the system’s riskiest action. The double signal requires a deterministic filter (keyword regex) to confirm the category produced by the LLM: if the regex does not match, fall back to draft. The global kill switch (ARGENTIC_AUTO_RESPONSE_ENABLED as an environment variable, auto_response_enabled as an agent parameter) lets you cut auto-response in one place without touching code. Rate caps limit the number of auto-responses per run (ten by default) and per agent over twenty-four hours (fifty by default), counted via the auto_response_log table. The confidence threshold forces draft below 0.7. The audit log records every auto-response sent, with the ticket, the category, the confidence, the template used, the final render and the dedup key. Dedup, finally, forbids more than one auto-response per ticket and per template, via a sha256(ticket_id + template_path) key with a unique index in the database.
The residual risk is assumed: the regex-based double signal can be bypassed if an attacker injects the keyword into the ticket, and that is explicitly documented in the ADR. Its goal is to improve robustness against non-adversarial LLM classification errors, not to resist a determined attacker. For a system in contact with real clients, that limit must be clearly stated.
Routing is the step that follows classification and completes the decision. A classified ticket is routed to support or presales, and the agent checks the current assignment in Zendesk: if it does not match the expected routing, it corrects it via the API. It is an explicit re-routing, which prevents a presales ticket from staying in the support queue due to an initial mis-assignment. The correction is an idempotent PUT on the Zendesk API, which respects the idempotence constraint of ADR 0002.
The incarnation of the promises is the most complete here. Determinism comes from the rule table, which makes the auto/draft decision a deterministic lookup rather than a model judgment. Control comes from the guardrails and the systematic human validation for drafts, which are the majority of cases. Auditability comes from the auto_response_log, which records every auto-response with its full context, and from the per-Step trace, which makes classification and decision retrievable. Security rests on a fail-closed model: draft is the default state, and the path to auto remains a strictly controlled path.
A technical precision deserves to be made on Zendesk API access, which is the subject of ADR 0004. The agent does not use the Zenpy Python SDK, but httpx.AsyncClient directly. The choice is justified by consistency with the rest of the stack (all other external integrations, Plausible, Search Console, RSS, SMTP, use httpx), by the async-native nature of httpx which avoids wrapping synchronous calls in asyncio.to_thread, and by full control over idempotence (the dedup key is injected into the Zendesk comment metadata without fighting a SDK abstraction). It is an implementation detail, but it is representative of the Core’s philosophy: you keep control over the mechanisms that matter, and you introduce a dependency only if it delivers net value.
What the platform does not do, yet
Honesty requires saying what is not there, because the manifesto can give the impression of a complete system, and it is not yet. The platform is under development, not in production, and real consumption figures, cost per run, latency per Step, will come when it runs for real. What I describe here is the architecture and the mechanisms, not a quantified feedback report.
Three scope limitations are assumed in v1. There are no event triggers: a run is triggered by cron or by manual launch via the control plane, and webhooks or external events are out of scope. Adding an event trigger does not require an architectural change, only a new entry point in the scheduler, but for now the scope is deliberately bounded. There is no Web UI: the control plane is a REST API consumed by a CLI, which covers all operations (configuration, manual launch, draft validation, reference approval, token metrics). A future UI can plug into the same API without rework, but it is not the v1 priority, and metric dashboards can be built with external tools connected to Postgres. Finally, the Web Stats Agent only reads Plausible in v1, not Matomo or GA4: the analytics scope is deliberately reduced, and extension to other sources will happen by adding fetch Steps, without touching the Core.
These limitations are not design defects, they are scope bounds. The Core is designed so that every extension comes down to adding a pipeline of Steps, and the current limitations are the first things to lift once v1 has proven itself in production.
Conclusion, the vision holds
The goal of this series is to expose a philosophy, then to show it verifies concretely in code. The four pillars of the manifesto, determinism, control, auditability and token efficiency, did not stay at the slogan stage: they materialize in the Core and in the three pipelines. Determinism verifies in the for step in steps and in the Zendesk Agent’s rule table. Control verifies in the auto-response guardrails, in the reference validation queue, in the type-instance distinction that lets you configure without redeploying. Auditability verifies in the per-Step trace, in the llm_usage block of LLM Steps, in the auto_response_log that records every risky action. Token efficiency verifies in per-task multi-modeling, in per-Step UsageLimits, in the separation between cheap classification and solid synthesis.
There is no magic in any of it, and that is precisely the point. The Core is two hundred lines, the rule table is a versioned file, the guardrails are defensive code, the scheduler is a Postgres lease-based leader election. All of it is classic, readable platform engineering. The value is not in complexity, it is in the refusal of superfluous complexity.
The platform is extensible by construction. Adding a workflow means adding a pipeline of Steps, and the Core does not change. The three current agents share the same foundation, and the next ones will share it too, whether it is watch, statistics, tickets or something else. That is the property that makes the investment in the Core pay: you pay it once, and every new agent benefits without marginal platform cost.
More workflows will come. v1 sets the frame, and the frame is designed to grow without rewriting. If this approach resonates with the pipelines you have in production, or with those you hesitate to entrust to a full-LLM agent, the supervised path deserves a serious look.