Why my agents talk through Postgres, not to each other

Most multi-agent demos I see have agents messaging each other. A planner hands a task to a researcher, the researcher replies, a critic weighs in. It looks good on a slide. I built something different, and it has run 24/7 on my homelab since June.

Switchboard is my multi-agent system for finding and preparing job opportunities. Its agents never talk to each other. They read and write rows in Postgres, and cron decides when each one wakes up. This post covers why I built it that way, how it works, and what broke.

What Switchboard does

The goal is a complete opportunity package for each job. A package holds the role, research on the company, the people worth contacting and a way to reach them.

The pipeline is a chain of agents:

  1. Discovery finds roles, normalises them and works out which application portal each one uses: Greenhouse, Lever, Ashby or Workday.
  2. Opportunity Intel researches the company and the role.
  3. People Research finds who is relevant at that company.
  4. Contact Enrichment finds how to reach them.
  5. Dispatch and the application agents, including Applicator and a dedicated Workday agent, take it from there.

Discovery only looks for junior, graduate and early-career software roles in Dublin, Ireland and remote-Ireland. That narrow scope keeps every later stage small.

Why not let agents talk to each other

I wanted the system to stay deterministic. Which agent runs, when it runs and which job it works on should be decided by code and data. I didn't want that logic living in a system prompt, where a model decides whether to hand off and to whom.

So the handoff isn't a conversation. It's a row changing state.

Postgres is the coordination layer

Each agent owns one stage. It reads the rows that are ready for it, does its work and writes the result to its own table. The core tables are jobs, applications and events. Each newer stage got its own table, so there's opportunity_intel, job_people_research and contact_enrichment.

No agent knows another agent exists. Discovery never calls Opportunity Intel. It writes a job row, and Opportunity Intel picks it up on its next run.

That gives the system a few useful properties.

Crashes are boring. If an agent dies halfway through a task, the job stays in the state it was in and the next run picks it up. Nothing gets lost in a message nobody received.

Every handoff is inspectable. I can query the database and see where each job is and what each agent wrote about it. Debugging means writing SQL, not reading a chat transcript between two models.

Agents are replaceable. Each stage only has to read and write its agreed columns. I can change the model, the prompt or the framework behind one agent without touching the others.

The cost is latency. A job waits for the next cron tick at every stage, so a full package takes somewhere between seven and nine minutes. For job discovery I don't care. Nobody needs a company dossier in under a minute.

The runtime

Switchboard runs in OpenClaw on a Linux Mint box in my homelab, with Hermes handling supporting infrastructure. OpenClaw cron schedules the agents.

Discovery has its own long-running Hermes profile that behaves like a resident sourcing operator. It runs a supervisor loop, works in irregular blocks and only messages me on Telegram when it has something worth saying. Because that bot matters more than the others, a separate cron heartbeat checks its health every 10 minutes and stays quiet unless something is wrong.

Model routing

Most of the work is reading a page and filling in fields. That runs on Gemini 3.5 Flash Lite through OpenRouter.

The work that actually needs judgement, resume tailoring and resume review, goes to frontier models from Anthropic and OpenAI.

The whole thing costs me about 10 to 12 euro a month, which is what I top up on OpenRouter. You could probably push that lower by swapping the cheap tier for one of the Chinese open-weight models, but I haven't needed to.

What broke

An agent cited a Google search as evidence

Contact Enrichment labels every email it finds as verified, public-professional or inferred. Inferred emails need evidence.

At one point it cited a raw Google search results URL as the evidence for an inferred address. That's where it looked, not proof of anything. The output was well formatted and confident, which made it easy to miss.

I tightened its instructions with two rules. A raw search URL on its own no longer counts as evidence. An inferred email now needs either a public email at the same domain showing the pattern, or a reputable email-pattern source.

Structured output tells you the format is right. It tells you nothing about the content, so any field called "evidence" needs its own check.

A crawler was off and nothing complained

HARVESTER was a cron job that crawled a list of companies I track, around the clock. When I went through the system to document it, I found it disabled. I still don't know how long it had been off.

Nothing errored, because a disabled job doesn't fail. It just stops writing rows. Everything downstream kept working on whatever it already had.

This is the real weakness of a state-based design. A missing stage looks exactly like a quiet day. The Discovery bot has a heartbeat, but nothing watches the other stages yet. That's the next thing I need to fix.

Where it started

The first version was built from scratch on OpenClaw, with Stagehand for browser automation. Applications went through a Telegram approval gate. An agent prepared the application, I reviewed it on my phone, and it only got submitted once I approved it.

The early discovery agents, FORGE, HARVESTER, SCOUT and RADAR, posted their results to Discord.

Along the way I looked at LangGraph, CrewAI, PydanticAI and Block's Goosetown. They're good tools, but I kept coming back to the same requirement. I wanted the order of operations to be something I could read in a table, not something a prompt negotiates at runtime.

What I'd tell someone building one

Design the state before the agents. Decide which tables exist and what each stage writes, then pick a framework. Once the state is solid, each agent is a small worker you can throw away.

Watch for silence as well as errors. In a pipeline like this the dangerous failure is the stage that quietly stops.

Treat anything an agent calls evidence as a claim. Check it the way you'd check a new colleague's citations.

Next I'm adding an Outreach Brief agent that turns a finished package into a short note on whom to contact, why, and what to say. It's one more table and one more worker, and nothing upstream has to change.