Skip to main content
About 5 minutes. This one needs a Pome account: it is the page about the thing that only exists in the cloud. Everything else in Get started runs on your own machine with no account, and you can do that first.
Real work crosses systems. An agent notices something in GitHub and says so in Slack; it takes a payment in Stripe and files the receipt in Gmail. A twin of one service tests half of that. So a sandbox holds up to three digital twins at once. This page starts one with GitHub and Slack in it, lets an agent work across both, and then reads back what each twin recorded.

What you are about to see, both halves

One sandbox is one thing you hold. It has a single id, a single bearer token that opens every twin in it, a single 30-minute clock, and it is one line on the bill no matter how many twins are inside. The twins inside it share nothing else. Each keeps its own state, and each writes its own tape — the record of the calls it served. The GitHub write does not appear on Slack’s tape, and Slack’s does not appear on GitHub’s. Which leaves the interesting question, and it is the one this page exists to answer: if the platform moves nothing between the two twins, what carried the issue number from GitHub into the Slack message?
Your agent did. Pome gives the two twins one address book, one credential and one lifetime. It does not copy a fact out of one and into the other, and there is no setting that makes it. That is not a gap — it is what makes the run a test. An agent that forgets to tell Slack produces a sandbox where GitHub is correct and Slack is silent, and a test that carried the fact for it could never catch that.
Nothing on this page is graded. No task file, no criteria, no score, and no agent eval is charged — an eval is only ever burned when a run is graded, and nothing here is. Grading appears exactly once in this curriculum, at the graded capstone.
Every command below was run against a live sandbox and every output block is pasted back unedited. The ids you see are from that run; yours will differ.

Before you start

  • A Pome account. Sign up, then npx @pome-sh/cli@latest login. A sandbox runs on our infrastructure, so unlike Get started there is no accountless version of this page.
  • Node 24+ for npx, plus curl and jq. Your agent can read the raw JSON without jq; it is here to keep the blocks short.
  • Your own coding agent. No ANTHROPIC_API_KEY and no model inference paid for by Pome: your agent is both the operator and the actor.

Start one sandbox with two twins

Pass --twin once per twin. The order does not matter.
Read the shape of that. One session_id. One expires_at. A per_twin entry per twin, each with its own address — and the same id sitting as a segment inside both. One thing you hold; two places to call.
The jq above is a projection, not the whole response. Unprojected it also carries an mcp_url and an openapi_url per twin, a provider_credentials map of vendor-shaped tokens the twins hand out to code running inside the world, and an agent_token. Drop the pipe to see all of it. None of those is the bearer you call with — that comes from the secrets file below.

One credential, two addresses

--secrets-file writes the values as a shell file. Source it and every twin in the sandbox is reachable from the same shell.
Ten variables, and the count is the lesson. Each twin brought three of its own — a REST address, an MCP address, and a vendor-shaped token for code running inside its world. The other four belong to the sandbox, however many twins you asked for. POME_AUTH_TOKEN is the important one: a single bearer, and it opens both twins. Two helpers keep the rest of the page short, and they differ only in which URL they call — the bearer is the same variable in both.
That one token is what opens both twins, and it opens nothing else. Send it to a different sandbox’s URL and the answer is404 No twin pod for this session. — the same opaque refusal a wrong bearer gets, because the routing layer answers before any twin does. So a 404 on a sandbox you just created is almost always the wrong URL or the wrong token rather than a dead sandbox.

Hand your agent the work

The task crosses both twins on purpose: triage the bug in GitHub, then tell the channel. Paste this as-is.

The GitHub half

One open bug, in acme/api. Triage it:

The Slack half, and the carry

Now the step the whole page is about. acme/api#1 came out of the GitHub response above and goes into the Slack message body here, and the thing that moved it those two lines is the agent:
No join, no shared table, no event bus, no forwarding rule behind that. Slack was told because something outside both twins chose to tell it.

Read the tapes — one per twin

Each twin recorded the calls it served, and only those.
Read those two blocks together. The same ses_L2RwKae6MWKMg1v2 is a segment in every path on both tapes — one sandbox, and both twins know they are inside it. And the two row sets are disjoint. The chat.postMessage that Slack served is nowhere on GitHub’s tape; neither GitHub write appears on Slack’s. Not summarised there, not cross-referenced there — absent. That is mechanical rather than a matter of reading — each tape says so itself:
Reading a tape does not add a row to it, so those blocks are the same however many times you run them. Ask for a whole row and you get what a check reads:
state_mutation: true means the call landed — a write the twin refuses reads false. The tool field is the twin’s own action vocabulary, stamped whether the call arrived over REST or over MCP; the assignees row has none, because only a short list of GitHub actions is stamped by name today.

The other half of isolation: a second sandbox

Two twins in one sandbox do not share state. Neither do two sandboxes — and that is the claim that is easy to fake, because reading a fresh sandbox after stopping the first proves nothing: an empty world is what a reset looks like too. So start a second one and leave the first running.
Both open, both holding the same two twins. Point a second pair of helpers at the new one, keeping the first pair working:
Now ask both sandboxes the same question, in flight at the same moment:
One request, two answers, same instant. The second sandbox’s #general is still sitting on its seeded morning greeting, and its issue #1 is still unassigned with no comments — while the first sandbox, alive on the other side of the &, holds both writes. Sequential reads cannot tell isolation from a reset; this can. The tape says it a second way. The second sandbox’s GitHub tape carries its own id and one row, the read you just did:

What you could assert here

Nothing above was graded, but a tape and a final state are exactly what a graded check reads. Each twin declares its own closed set — GET /v1/checks?twin=<twin> prints it, and so does pome checks. That per-twin split is the point, and it is worth seeing in the vocabulary itself. There is no check that reads both twins. A cross-twin claim is made by conjoining one check on each side, because grading reads what each twin recorded, and each twin recorded only itself:
The negative half above is a demonstration, not something you can assert today. “No new message was posted” is not expressible: slack.no-message-posted counts a channel’s messages including the ones the starting state placed there, and Slack declares no seed-versus-final delta check the way GitHub’s github.no-new-issues does. Tracked as F-1340. Until it lands, isolation between sandboxes is something you observe, as above, rather than something a criterion states.
Pointers, not a task. Write a task is where the grammar lives.

What this costs, and how to stop it

Two sandboxes were open at once above, with four twins running between them. Ask the control plane what that cost — POME_API_KEY is a team key from the dashboard, not the sandbox bearer:
Two, not four. Billing follows the sandbox, not the twin — adding a second twin to a sandbox costs nothing extra, and opening a second sandbox is what counts. No agent eval was charged either: an eval is only ever burned when a run is graded, and nothing here was. Each sandbox expires on its own 30-minute clock — expires_at in the create response — and stopping one takes the twins inside it with it. Stop both rather than waiting them out. $POME_SESSION_ID is whichever file you sourced last, so name each one in turn:
--discard throws the world away instead of keeping it for a report, which is what you want for an ungraded walkthrough like this one. Then the meter is back where it started:

Next

The graded capstone

The same two twins, now an exam: a sealed agent under test, a deliberate failing score, and one sentence of prompt that turns it green.

Cross-twin consistency

A worked example of the failure this page’s shape exists to catch — GitHub correct, Slack silent, and only an exam reading both states at once notices.