Article
9 min read
Our AI agent turned one ticket into a team of nine agents

Author
Osman Ertem Unat
Last Update
October 30, 2026

Table of Contents
Why we built our own agent platform
Six months in
One pod per session, confined by construction
Multiple providers, one registry, and a fallback plan
Acting as a team, not a service account
Six ways in, one set of gates
Talking to a running agent
Agents that delegate to agents
DCA agent delegation in action
What this adds up to
How we built an internal platform where AI agents run for hours, ask a person when they should, and hand work to other agents.
Why we built our own agent platform
Most AI agent demos last two minutes. Real engineering work doesn't. A useful coding agent clones a repository, waits on CI, needs credentials that belong to a specific team, gets redirected when requirements change, and has to explain what it did and what it cost.
That's why we built Deel Cloud Agents (DCA), our internal platform for running long-lived AI agents as engineering workers. Our engineers already ask for work in Slack and Jira, so DCA meets them there. Mention an agent in a channel, and a session starts in its own Kubernetes pod, posts progress to the thread, takes follow-ups mid-run, and stops to ask when it needs to.
Four decisions shape everything else in the platform:
It runs on Deel's own infrastructure. Agents can work with private resources, like internal services, repositories, and databases, without opening those systems to an outside agent vendor. Credentials stay in our own secrets manager, and each session runs in an isolated pod that exists only for that session.
It's governed like any other production system. Every agent acts with the permissions of the person who started it, and running one requires rights on every step, tool, and sub-agent it depends on. Teams bring their own credentials instead of sharing one bot account, spend has hard limits, any agent can be paused with a kill switch, and every session leaves a full log with its cost.
It's not tied to any one AI provider. DCA runs models from Anthropic, Google, and OpenAI side by side, with Together AI wired in as a fourth option. Models are grouped into classes that span providers, so a provider outage means falling back to a comparable model instead of failing, and teams can pick the right model for each job.
It meets teams on whatever protocol they already use. The same agent can be started from Slack, the web console, a REST API, or an MCP server that plugs into Claude Desktop and Claude Code. It can also run on a schedule or pick up tickets from a Jira board. Every session publishes its lifecycle events over NATS, so other services can react when an agent starts, asks a question, or finishes. Whichever way a request comes in, it passes the same checks and runs the same way.

Six months in
The first DCA session ran on April 20, 2026. About 182,000 sessions have run since, and more than half of them ran in the last 30 days. Between September 26 and October 25, DCA ran 107,691 sessions for 1,064 people across 242 teams, using 880 different agents. Of the sessions that finished, 95.6% succeeded.
The median session takes about eight minutes. Close to one in ten runs for more than an hour, and 1,219 ran for more than three. That long tail is what the rest of this article is built around. On the output side, 3,629 pull requests that DCA agents worked on were merged in those 30 days, and our code-review agent posted 39,800 reviews on 20,953 pull requests.
We don't measure time saved. Nothing in the platform can see what an engineer would have done instead, so we'd rather publish the numbers we can stand behind.
All figures come from production and cover the 30 days to October 25, 2026, unless a different period is named.
One pod per session, confined by construction
Every DCA session gets a dedicated Kubernetes pod that exists for that session alone. It starts when the session starts and goes away when the session ends, so one agent's work can never leak into another's, and there's nothing to clean up afterward.
That isolated environment is what lets an agent do real work instead of just calling APIs. Inside its pod, a session can clone and edit repositories, run scripts, and drive its own headless Chromium through Playwright: opening the app it's working on, clicking through a flow, taking screenshots, and recording video to attach to its report. Tool servers that need their own process run alongside it, each with a minimal, explicit set of environment variables.
Inside the pod, confinement is structural. The agent's bash isn't bash. It's just-bash, a JavaScript shell over a virtual filesystem where only the session's folder exists and no network commands are registered. We used to block dangerous commands with patterns. Now they simply don't exist.
Git is the one real binary, so it's locked down: no shell, no hooks or credential helpers, HTTPS only, no force push, and clones limited to the session's GitHub organization. Several of those rules came from real incidents. Each session authenticates with its own GitHub App token, and every commit carries a trailer naming the session plus a co-author line for the person who asked.

Multiple providers, one registry, and a fallback plan
DCA runs on models from Anthropic, Google, and OpenAI, with Together AI wired in as a fourth provider. A typed registry lists each model's context window, reasoning levels, and pricing, which produces the cost line on every session. Models sit in four classes, and each class spans more than one provider.
When a model has to be switched off during an incident, a small table we call the Model Manager takes effect within 30 seconds. Sessions fall back through the rest of their class, then cheaper classes, then more capable ones, because running on a pricier model beats failing. The one exception: when a person names a model, we run it or fail.
Long sessions compress their history at 80% of the context window, keeping the last 20 messages verbatim. As one code comment puts it, a wrong belief about what's already done is worse than a longer summary.

Acting as a team, not a service account
Instead of one shared service account, DCA uses identity profiles: person-owned sets of credential overrides, like a data warehouse key or a GitHub App, that replace what a tool would read from the pod. Each tool declares which variables a profile may override, checked at compile time, and the overlay is built per tool, so a profile's wiki token is unreachable from the database tool. Secrets live in a secrets manager, never in our database. Attaching a profile to an agent requires edit rights on both, and tools that reach production data won't run without one.
Permissions follow the dependency tree. Running an agent requires execute rights on every step, sub-agent, and tool it uses, and a denial lists exactly what's missing. Spend limits apply over rolling one-, seven-, and 30-day windows, any agent can be switched into maintenance mode, and every prompt and follow-up is screened for injection attempts.

Six ways in, one set of gates
Work arrives through Slack, the web console, a REST API that's also exposed as an MCP server for Claude Desktop and Claude Code, cron schedules, a Jira board watcher, and other agents. Every path passes the same checks: permissions, spend limits, and idempotency.
Slack is still the front door. In the last 30 days, 48% of sessions started there, 22% came through the REST API and MCP server, 17% ran on a schedule, 9% were started by another agent, 3% by system events, and 1% from the web console.
In Slack, when a channel has several agents, a model picks one through a tool call that can only return that channel's agents, so it can't invent one. If nothing fits, a lightweight call decides between a short reply and a simple reaction, so the bot doesn't answer every "thanks."

Talking to a running agent
A Slack reply, a console message, an API call, or another agent can reach a running session through one path. Each follow-up is checked atomically against the session's status, saved, and published to a per-session mailbox on NATS.
The core rule of DCA's runtime is that nothing long-running blocks inside a tool call, because a tool that sits silent for minutes gets abandoned by stream timeouts. When an agent asks a person something, ask_user returns immediately, the runner pauses between model steps, and the answer is added before it continues. If nobody answers in time (five minutes by default), the agent wraps up without taking the action it asked about. Unattended runs never get the ask tool at all.
Progress flows back through one Slack card updated in place, since Slack's streaming messages stop after about five minutes and our sessions run for hours.


Agents that delegate to agents
This is the capability we're proudest of: a running agent can start other agents as independent sessions, follow their progress, message them mid-task, and answer their questions.
DCA already had sub-agents, helpers that run as tool calls inside the same pod. But they share the parent's context, turn cap, and lifetime. Some work needs its own agent, its own tools, and room to run for hours.
| Sub-agent | Spawned session | |
|---|---|---|
| Runs in | The parent's pod | Its own pod |
| Definition | A prompt and tools | A complete agent with its own flow, subagents, and output schema |
| Talks to | Only its caller | Any running session in its tree |
Agents get a small toolset: spawn_session to queue a child, status and result tools to follow it, and send_session_message to talk to another session. The tools only appear when a session can actually use them.
The platform decides what a child inherits, never the model. A child runs as the same person, with the same permission checks, spend limits, and credentials. A child of an interactive session can ask questions. A child of an unattended run can't, because an agent can't give its child the ability to interrupt a person. An idempotency key makes retried spawns safe.
The tree is the security boundary. A session can read only the sessions below it, and an id outside that scope gets the same "not found" as one that doesn't exist, so agents can't probe for other sessions. Messages can reach any running session in the same tree, through the same mailbox as a person's follow-up. They arrive as notes from a colleague, never as instructions that override the person's request.
Questions climb the tree. A child has no Slack thread, so its question goes to the nearest running ancestor, which answers it, asks its own person, or tells the child to wrap up. The platform checks that an answer matches a question the recipient really asked, so no agent can unblock another by guessing.
Delegation went live on September 21. In its first five weeks, agents started more than 10,000 child sessions in about 5,200 delegation trees, and weekly volume grew from 1,951 in the first full week to 2,975 in the week of October 19. Most trees are one parent and its children, and 267 reached three or more generations, against a configured cap of four. Children succeed at about the same rate as sessions overall: 96.1%.
The messaging and question paths get real use. Agents exchanged 23,624 messages, and 194 questions climbed a tree to an ancestor, with 161 answers relayed back down. A common handoff is the one in the demo below, a coding agent passing its pull request to a live reviewer agent, which happened 2,596 times from 2,274 coding sessions. QA agents ran about 1,970 test cases, each in its own session. And 461 children ran for more than an hour, the longest for six and a half hours, which is exactly the work that sub-agents inside the parent's pod couldn't take on.
Here is what that looks like in a real run.
DCA agent delegation in action


Agents talking to each other

Agents talking to each other

Agents talking to each other

Agents talking to each other
What you're looking at
This is a real run from our console. The engineer gave one agent, Super Eng, a deliberately tiny test ticket: make a homepage background black. Super Eng assembled a team to do the work. The tree shows nine sessions across three generations below the coordinator. Each session ran in its own pod, and the envelope badges count the messages exchanged along each connection. The one-line change isn't the point. A small ticket keeps the diff out of the way so you can see what's new: how the work is split, how the agents talk to each other, and how review and QA loop until the coordinator is satisfied. On a real ticket the tree looks the same. Only the work inside each session gets bigger.
Who does what
Super Eng (center): the coordinator. It takes one engineering request, usually a Jira ticket, and drives it end to end without writing code itself. It spawns the workers, tells each one what to do next, enforces limits (at most 10 review rounds and 3 QA cycles), and shuts everyone down at the end. Its final report covers the PR, the review outcome, the QA verdict, what QA couldn't test, and what a person needs to do next.
Super Eng Coder (right). It implements the change and opens the pull request. When the coordinator sends it review findings or QA failures, it pushes fixes to the same PR.
Super Eng Reviewer (top and bottom). It reviews the PR the same way our code-review agent does, then reviews each new round of commits. There are two reviewer nodes because the first one went idle and exited. The coordinator started a fresh reviewer to look at the latest commits, and it allows only one such restart.
Super Eng QA (left). It designs test cases from the ticket and the PR, then reports pass, fail, or blocked.
QA runner (next ring out). QA spawned it to run the test cases. It sets up the environment and sign-in, then hands each test case to its own session.
Case runners (outer ring). Each one runs a single UI test case end to end in its own browser, so no two cases share a page. It returns a short report with screenshots.
How they coordinate
The workers are long-lived. Each finishes a task, reports back with a short status message (coder done, review done, QA done), and waits in an inbox for its next instruction: fix, review again, run QA again, or shut down. The coordinator runs the loop. The coder opens a PR, review rounds continue until the reviewer approves, then QA runs. If QA fails, the coder fixes the problem and review starts again.
Every worker is an existing standalone agent (our coder, reviewer, and QA agents) with one addition: the inbox phase. No new agent types were needed, only coordination.
What this adds up to
Every section above describes a constraint: one pod per session, a bash with no network, permissions checked on every step, children that inherit exactly what their parent had and nothing more. The takeaway is that those constraints are what make long-running agents possible. An agent that can't leak, can't overspend, and can't grant itself anything is one you can leave running for hours, and one you can let start other agents.
That matters at Deel because our engineering work arrives as Jira tickets and Slack threads, not as tidy prompts, and finishing one means code, review, testing, and a report back. Six months after the first session, DCA runs more than 100,000 sessions a month for 242 teams, and nearly one in ten of those sessions is now started by another agent. We built it on our own infrastructure so agents could be trusted with real work. Now we're finding out how much of it they can take.

Osman Ertem Unat is an AI Automations Team Lead and Generative AI Developer at Deel, where he builds AI-driven automation and agentic systems on top of a full-stack JavaScript foundation. He designs and ships AI agents that help Deelers focus on what really matters.