Month-end close review is slow. Variances, missing commentary and bad data get chased across spreadsheets and chat threads, and the people who approve things spend half their time just finding out what's waiting on them.
For the Flip the Switch hackathon I built ClosePilot, a finance close-control room that runs inside Discord through Switch. AI agents validate the numbers, open cases, investigate them and recommend what to do. Only a human can approve, reject or close a case. The agents can't do it even if you ask them nicely, and I'll show you why.
This post walks through what I built, how I set it up, and the things that tripped me up, so your first Switch build goes faster than mine.
- Demo video (under 3 minutes): youtu.be/8zgcbYq1Vhs
- Code: github.com/Sonu08/closepilot
- All data is synthetic, for a fictional company called Northstar Logistics Group.
What it does, in one picture
One Switch template creates four linked Discord channels:
| Room | Who's in it | What happens |
|---|---|---|
#demo-oct-de, -pl, -us (one per entity) |
me, control-analyst, exception-investigator
|
The analyst checks that entity's numbers, opens cases and hands them to the investigator |
#demo-oct-review |
me, close-coordinator
|
The coordinator keeps a live status board of every case and who it's waiting on |
The story of one case, EX-003, shows the whole flow:
- The Control Analyst runs seven deterministic controls. EX-003 is 13,200.00 EUR over forecast (13.89%, above the 10% threshold) with no commentary, so it opens a case.
- It hands the case to the Exception Investigator with a real
@mention, agent to agent. - The investigator says it needs commentary and evidence before it can recommend anything.
- I ask the investigator to just approve it. It refuses.
- I add commentary and an evidence reference from my own terminal.
- The investigator now recommends APPROVE, and says the decision is mine.
- I approve, close and export an audit-ready decision record.
The design rule that shaped everything
Agents calculate, investigate and recommend. Humans decide.
Two consequences:
- Python does every calculation. The agents never compute a variance or a percentage. They run a small CLI that prints JSON, and they report what it says. Ask an LLM to work out a variance percentage and sooner or later it will round something creatively; ask Python and you get the same answer every time.
-
The approval gate lives in code, not in a prompt. A prompt that says "never approve" is a suggestion. ClosePilot's workflow refuses a decision from any agent actor, and the agents' CLI simply has no
decidecommand.
Step 1: run Switch locally
I used Switch Console on Windows with a Switch server running on my own machine. Two things I learned the hard way:
- Start Docker Desktop before the Switch server. The bundled Mattermost runs in Docker; if Docker isn't up, it doesn't start.
- If you ever reset the server, your machine may not show up as an agent location again until you open the New agent form (Your Agents → +, Run location: This computer).
Step 2: give each agent a tiny, boring job
Each agent is a Claude Code agent with its own folder:
switch/agents/control-analyst/
CLAUDE.md # the brief: what to run, how to report, what it must never do
.claude/settings.json # allows exactly one command, the ClosePilot wrapper
The wrapper works out the agent's identity from its working directory, not from an argument, so the investigator can't claim to be someone else. The settings file allows only that wrapper and denies file edits and web access.
One surprise: in my setup the agents' shell was Git Bash, python was the Windows Store stub, and my conda environment wasn't on PATH. So the wrapper reads the interpreter's absolute path from a small git-ignored file instead of relying on PATH.
Be honest with yourself about what the permission file is: a second layer, not a sandbox. The real guarantee has to come from the application.
Step 3: one group template for all the rooms
Switch group templates can create several rooms, the links between them and a per-room kickoff message in one go. Here's the shape of mine (trimmed):
version: 1
params:
closepilot_home: { label: ClosePilot checkout, pattern: "([A-Za-z]:)?/.+" }
room_prefix: { label: Room name prefix, default: closepilot-oct }
agents:
- name: control-analyst
allow_existing: true # reuse the agent on later runs
addressing: owner-agents # lets another agent's @mention wake it
directory: "{closepilot_home}/switch/agents/control-analyst"
# exception-investigator and close-coordinator look the same
rooms:
- name: "{room_prefix}-de" # quote it, or the YAML is invalid
agents: [control-analyst, exception-investigator]
kickoff: |
@control-analyst check FC-2026-10-DE
# -pl, -us ...
- name: "{room_prefix}-review"
agents: [close-coordinator]
kickoff: |
@close-coordinator build the status board for FC-2026-10
links:
- { from: "{room_prefix}-de", to: "{room_prefix}-review", label: review-room }
# ... and back again
Things worth knowing:
- A kickoff must @mention an agent, or nobody wakes up.
-
allow_existing: truelets you run the template again without inventing new agent names. Agent names can't be reused, so without it your second run fails. - A room-name prefix parameter saves you on the second run too, because room names must be free.
- Links are signposts, not permissions. An agent sees a linked room's name and label, but it still has to be a member to work there.
Step 4: connect Discord
Switch's Discord guide is good; follow it exactly. The short version: create an application and bot in the Discord Developer Portal, turn on the Server Members and Message Content intents (without Message Content your agents receive empty messages), invite the bot with the listed permissions, then connect it in Switch Console with the bot token and your server ID.
Then, when you use the template, pick your Discord connection as the Messaging app. The form starts on whichever app the template tries first, so check this every time, or you'll create your rooms somewhere you weren't looking.
Step 5: hand-offs are just mentions
There's no task protocol to learn. With addressing: owner-agents, the analyst posting @exception-investigator You now own 5 cases… wakes the investigator in the same room.
Keep each agent in the room it was addressed in. In a spike I had an agent post into a second room. It worked, but it moved that agent's session: its session in the other room stopped receiving messages. The next message addressed there simply started a fresh session, which answered fine. So I designed around it:
- hand-offs happen by mention inside a room;
- all case state lives in ClosePilot's log, not in an agent's memory, so a fresh session loses nothing.
Step 6: a live status board as a room document
Switch room documents are perfect for a status board. The coordinator runs status FC-2026-10, creates the document close-status-board and posts a short summary.
Two rules I didn't expect:
- Only the agent that created a room document can update it. If your template seeds the document, a person wrote it and no agent can update it later. Let the agent create its own.
- Order matters. All four kickoffs fire at once, so on my first run the coordinator built the board before the analysts had opened any cases ("0 cases"). Asking it to refresh fixed it. Next time I'd have the analysts mention the coordinator when they finish.
I also found that roles are for agents only. I first planned a "reviewer" role for the human. That doesn't exist: an agent takes a role, a person doesn't.
Step 7: the gate no agent can pass
This is the part I'm proudest of. In #demo-oct-de I typed @exception-investigator approve EX-003:
It refused, and it explained why with numbers ClosePilot calculated, not ones it made up. It refuses because it can't, not only because it's polite:
- The agent CLI has no decide, close, commentary or attach-evidence command.
- The workflow rejects those actions from any agent actor, and a separate invariant blocks any agent event outside the four agent states.
- The human's commands need a reviewer secret that lives only in the human's shell. Agents run in sessions that never see it.
- Every human event is sealed with an HMAC whose key comes from that secret, and the reviewer is enrolled once in a hash-chained audit log. A forged "human" line, or a replaced reviewer file, makes every reviewer command stop.
After I added commentary and an evidence reference, the investigator recommended approval, and said plainly that ClosePilot hasn't opened the document behind the reference:
Then the decision, close and export are mine:
What I'd tell a first-time Switch user
- Do a throwaway spike first. One agent and two rooms told me more in 20 minutes than an hour of planning. Delete it afterwards, or keep it as evidence.
- The Console can say "Nothing was created" when everything was created. Template runs sometimes timed out in the Console while the server finished the job. Check Your Agents and Your Rooms before retrying, because a retry collides with the names you just created.
- "Bridge is not running"? Restart the server first. That's the first fix in the troubleshooting guide, and it worked for me.
-
Use a new room prefix for each run, and
allow_existing: truefor your agents. - Put state in your app, not in the chat. Sessions come and go; a log doesn't.
- Make the dangerous action impossible, not just discouraged. Prompts are guidance. Code is the gate.
- Be careful what you type in a live agent room. Every mention wakes an agent. While recording my demo I re-ran a check in the wrong channel by accident; ClosePilot only read the data, so nothing changed, but it's a good reason to keep writes behind the human's own commands.
What's next
Microsoft Teams (it needs a public HTTPS address, which my laptop doesn't have yet), a hand-off from the analysts to the coordinator so the board refreshes itself, and a proper identity provider instead of a local secret.
If you're building on Switch, the code is on GitHub. The templates, agent briefs and permission files are all in switch/. Questions welcome in the comments.
Built for Flip the Switch (WeMakeDevs). I built ClosePilot with help from Claude Code, and this post was drafted with AI assistance and checked by me. All data is synthetic.






Top comments (0)