I Planned a Multi-Agent Coding System. Here Are the 7 Decisions That Matter An engineer at Zyntari, an Israeli engineering studio, has published the technical specification behind a multi-agent coding system in which AI agents take tasks, write code, run tests and open pull requests under human approval. The design keeps task state in PostgreSQL with transitions gated through a single pure function, so agents can only request state changes, and uses Temporal workflows with side effects confined to Activities so crashed runs resume from history. Agent completion claims are treated as requests for verification, validated against a Zod schema and confirmed by tests, reviews and evidence rather than prose. At Zyntari, my engineering studio in Israel, I am building a system where multiple AI coding agents work on real software projects: taking tasks, writing code, running tests, opening pull requests, and waiting for approvals. Before writing code, I wrote a full technical specification with every architecture decision, every tradeoff, and every thing we deliberately will not build. A spec is usually private. But the decisions in it apply to anyone trying to run more than one coding agent on anything real, so here are the seven that matter most, why we made them, and how to steal them. The short version of the whole philosophy: agents are allowed to reason, but they are not allowed to decide what is true. The system owns the truth. That single rule drives almost everything below. Most agent systems today are a chat that remembers things. The agent narrates what it is doing, and whatever it says becomes the state of the project. That works until the agent gets confused, contradicts itself, or the process dies mid-sentence. Our rule: task state lives in PostgreSQL, and agents can only request state changes. A task moves through explicit states proposed, planned, running, awaiting review, merging, completed , and transitions happen through one pure function that either returns the new state with the events it emits, or returns a rejection. No caller, human or agent, updates state directly in the database. An agent finishing its work does not get to say "done." It must return structured output validated against a schema: js const AgentResultSchema = z.object { status: z.enum "completed", "blocked", "failed", "needs review" , summary: z.string .max 4000 , commitSha: z.string .regex /^ 0-9a-f {40}$/ .optional , changedFiles: z.array z.string .max 500 , acceptanceCriteria: z.array z.object { id: z.string , status: z.enum "passed", "failed", "not tested" , evidenceRefs: z.array z.string , } , } ; The important line is the design rule behind it: a model saying status: "completed" is not a verdict. It is a request to be verified. Tests, reviews, and evidence decide completion. Prose never can. An agent run is not a web request. It can take twenty minutes, wait two days for a human approval, and survive a crashed server. You can hand-roll that, or you can use a workflow engine built for exactly this. We chose Temporal, and Temporal's own documentation describes the same shape we use: the agent loop is a Workflow, each model call and tool call is an Activity, and a crash resumes the conversation instead of restarting it. That architecture comes with one strict rule, and it is the rule people break first: workflow code must be deterministic, because Temporal replays it from history after failures. So no LLM calls, database queries, Git commands, or random IDs inside workflow code. All side effects live in Activities, which are recorded once and replayed from history. The moment you put a direct API call inside a workflow, recovery stops working and you will not notice until the first crash. For long agent runs, Activities heartbeat their progress: which session is running, which phase it is in, and how much it has spent so far. A heartbeat timeout is how you learn that a Claude process or a whole host died, instead of finding out from a silent dashboard. The obvious way to automate a coding agent is to run it in a terminal and script the keystrokes. It is also a dead end: no structured events, no reliable cost data, no way to resume a session after a crash. The Claude Agent SDK //code.claude.com/docs/en/agent-sdk exposes the same agent loop that powers Claude Code programmatically, in TypeScript and Python: messages, session IDs, tool permissions, hooks, budget limits, and structured output. Every run records its session ID before it is allowed to touch anything, so a crashed run can be resumed instead of restarted. Hooks let us enforce policy in the host process before any tool executes, which matters because a policy check does not consume model context and cannot be talked out of by a prompt. One detail worth copying even in small projects: model recommendations in tool metadata, things like "this tool is read-only," are hints for scheduling and display, not security controls. Enforce permissions yourself, in code, before execution. Two agents should never share a writable checkout. Every implementation task gets its own Git worktree, and the system enforces a conservative write set: the paths the task owns, plus the files those paths implicate, like lockfiles and shared configuration. Tasks with overlapping write sets do not run at the same time. Lockfile changes and database migrations take a repository-wide lock, because those are the changes that break everyone else's in-flight work. Around the worker process, a boring security profile on Linux: rootless Docker, non-root user, write access only to its own worktree, no Docker socket, a default-deny network with an allowlist for exactly the endpoints it needs, and a default seccomp profile //docs.docker.com/engine/security/seccomp/ that is never disabled for convenience. Nothing here is exotic. It is the same discipline you would apply to any process that executes machine-generated code, which is what an agent is. When you have many tasks and a few agents, someone has to decide what runs next. The tempting 2026 answer is "let an agent decide." Ours does not. The task graph is validated deterministically: every dependency exists, no cycles Kahn's algorithm, cheap and absolute , every objective maps to an acceptance criterion. Priority is a weighted formula combining business priority, critical path, wait age, and expected cost. Agents may classify inputs, like labeling a task as security-sensitive, but those classifications are constrained enums with a confidence score. The final number comes from the formula. Why so rigid? Because a scheduler that is a formula can be tested, logged, and debugged. A scheduler that is an LLM judgment call cannot answer the question "why didn't my urgent task run" without another LLM judgment call. Blind retry is how a small problem becomes a large bill. We classify every failure first, and each class gets a different response: | Failure class | Example | Response | |---|---|---| | Transient | network reset, provider 5xx | Retry with backoff and jitter | | Rate limit | quota hit | Wait until the supplied reset time | | Worker loss | crashed process | Resume the session, or start a clean attempt | | Validation | malformed output | One repair attempt, then fail | | Deterministic | reproducible compile error | Send back for replanning | | Policy violation | forbidden command | Never retry. Escalate | | Budget reached | cost ceiling hit | Pause and ask for a decision | Backoff is exponential with full jitter, and automatic retries stop at two. If three attempts failed, the task definition is the problem, not luck. Every run reserves predicted cost from its parent budgets before it starts, and reconciles actual cost when it ends. Circuit breakers pause automatic spawning when a project reaches 80% of daily budget, when an attempt costs twice its prediction, or when token burn rate spikes. I wrote about this pattern for regular applications in a previous article about spending limits; in a multi-agent system it stops being a best practice and becomes load-bearing, because the system can spend money while you sleep by design. And one classic distributed-systems lesson that agent systems rediscover the hard way: fencing tokens. Every lease on a task or worktree carries a number that only goes up. If a worker's lease expires and the task is handed to someone else, late results from the old worker are rejected even if it wakes up and politely reports success. Without this, two workers eventually write to the same place, and they will do it while everything looks healthy. The spec has a longer section on non-goals than on most features: no Kubernetes, no Kafka, no Redis, no vector database, no learned routing model in version one, and no agents sharing a writable checkout. Temporal already provides durable queues, timers, and retries. PostgreSQL provides transactional state. Every additional component is a bottleneck you have not measured yet, dressed as preparation. The same discipline applies to agent memory: versioned Markdown files with PostgreSQL full-text indexes, not an embeddings stack, until keyword retrieval demonstrably fails. Code memory is dominated by exact symbols, paths, and commands, where lexical search usually beats semantic similarity anyway. If you are building anything in this space, the order matters more than the parts. Build the deterministic skeleton first, state machines and event logs, and prove that killing every process neither loses nor duplicates a task. Then add the agent runtime. Then the operator surface. Only after one real bug travels from intake to a protected-branch merge with zero manual copying do you earn a second department of agents. Agents reason. The system decides. That is the entire architecture in one sentence. Alon Trifonov is the founder of Zyntari zyntari.com , an independent engineering studio in Israel that builds production-grade software and AI systems. He works in Python, TypeScript, and everything that has to keep running after the demo ends. LinkedIn https://www.linkedin.com/in/alon-trifonov-546061234 Github https://github.com/A-Pex97/