# Ancha > Ancha runs your AI agent against stateful replicas of Slack, GitHub, Datadog and 35 other tools, grades each attempt with checks, and shows why it failed. > ## Agent Instructions > These docs describe the Ancha CLI, npm package @ancha-labs/cli version 0.1.4, and the hosted workspace at app.ancha.io. Coding agents should start at https://docs.ancha.io/coding-agents. Run the CLI as `ancha` after `npm install -g @ancha-labs/cli`, or as `npx -y @ancha-labs/cli@0.1.4` without installing. Pass --json for machine-readable output, or set ANCHA_JSON=1. Every command ends with a `next:` line naming the next command. Exit codes: 0 pass, 1 a check failed or a comparison regressed, 2 usage, config or authentication error, 3 infrastructure error, 4 unimplemented endpoint with --strict. # Introduction Source: https://docs.ancha.io/ Test an AI agent against stateful replicas of Slack, GitHub, Datadog and 35 other tools, and check what it did. Ancha tests AI agents against stateful replicas of the tools they use. Each replica answers like the real API, starts with fictional company data and changes as your agent acts. The agent reads and writes the replica instead of the real service. You give the agent a task and choose how many attempts to run. Every attempt starts from a fresh copy of the same data. Checks decide whether each attempt passed, and `ancha explain` shows what the agent did when one fails. ## How it works 1. Choose a world: the tools and the starter data the agent works with. The starter data is a fictional company, Acme, and its customers, such as Ridgeway Robotics. 2. Write the task, for example "answer the newest question in #support". 3. Run the test. Ancha starts your agent once per attempt and puts tool URLs and fake credentials in its environment variables. 4. Read the verdict. Checks compare the final tool state and the agent's API calls with what you expected. `ancha explain` shows expected versus actual and the calls behind each failure. ## Tools Ancha replicates 38 tools: Slack, Discord, Twilio, Gmail, Google Drive, Google Calendar, Microsoft Word, Notion, Airtable, Dropbox, Resend, GitHub, Datadog, Sentry, PagerDuty, Better Stack, Linear, Jira, Asana, Okta, Amazon S3, Clerk, HubSpot, Salesforce, Attio, Intercom, Zendesk, Gong, Stripe, PayPal, Shopify, Lob, Google Analytics, Google Ads, LinkedIn Ads, ChatGPT Ads, Open-Meteo and Kalshi. Each replica implements a documented subset of the real API. See [Tools](https://docs.ancha.io/tools/overview) for what each one covers. ## Two ways to run | | Your agent runs | The tools run | | --- | --- | --- | | [CLI](https://docs.ancha.io/quickstart) | On your computer | On a local server that `ancha run` starts | | [Hosted workspace](https://docs.ancha.io/cloud/overview) | On your computer with `ancha run`, or in a container in your workspace for runs you start in the console (private beta) | In your private workspace at app.ancha.io | The CLI needs no account. Signing in at [app.ancha.io](https://app.ancha.io/login) adds a hosted workspace with a browser console. ## Start here - [Quickstart](https://docs.ancha.io/quickstart): Install the CLI, run an agent, break it and read why it failed. About five minutes. - [For coding agents](https://docs.ancha.io/coding-agents): The commands, JSON output and exit codes a coding agent needs, plus a prompt to paste. - [Concepts](https://docs.ancha.io/concepts/overview): Worlds, tests, runs, attempts and checks. - [CLI reference](https://docs.ancha.io/cli/overview): Every `ancha` command and flag in version 0.1.4. ## What to expect - Replicas implement a documented subset of each API. A call outside that subset returns the tool's own error shape, and `ancha run` lists it as a coverage gap. - Your agent must read tool URLs and credentials from the environment variables Ancha sets. Ancha cannot redirect an agent that hardcodes production URLs. - Starter data is fictional. Reporting data in the ads and analytics tools is historical and does not grow when your agent changes a budget. # Quickstart Source: https://docs.ancha.io/quickstart Install the Ancha CLI, run a small agent against four replicated tools, break it, read why it failed and prove the fix. You write a 12-line agent, run it against replicas of Slack, GitHub, Datadog and PagerDuty, then break it on purpose and read how Ancha explains the failure. It takes about five minutes and needs no account and no API keys. ## Before you start - Node.js 24 or later, with npm. Check with `node --version`. - macOS or Linux. ## Run your first test ### Install the CLI ```bash npm install -g @ancha-labs/cli ancha --version ``` The second command prints `0.1.4`. To try Ancha without installing it, replace `ancha` with `npx -y @ancha-labs/cli@0.1.4` in every command on this page. ### Create a project and an agent ```bash mkdir ancha-quickstart && cd ancha-quickstart ``` Save this as `agent.mjs`. It reads one thing from each tool with the URLs and fake credentials Ancha puts in its environment: ```js agent.mjs // A minimal agent: read one thing from each tool Ancha provides. const env = process.env; const calls = [ [env.SLACK_API_URL, 'conversations.list', { Authorization: `Bearer ${env.SLACK_BOT_TOKEN}` }], [env.GITHUB_API_URL, 'repos/acme/starter', { Authorization: `Bearer ${env.GITHUB_TOKEN}` }], [env.DATADOG_API_URL, 'api/v1/dashboard', { 'DD-API-KEY': env.DD_API_KEY, 'DD-APPLICATION-KEY': env.DD_APP_KEY }], [env.PAGERDUTY_API_URL, 'services', { Authorization: `Token token=${env.PAGERDUTY_API_TOKEN}` }], ]; for (const [base, path, headers] of calls) { const res = await fetch(`${base.replace(/\/+$/, '')}/${path}`, { headers }); console.log(res.status, path); } ``` ### Set up the project ```bash ancha init --agent "node agent.mjs" ``` ```text Output Created 7 files; updated 0; preserved 0. agent: node agent.mjs starter: Slack, GitHub, Datadog, PagerDuty (no tool SDK recognized in the dependencies) next: ancha run ``` `init` writes `ancha.yaml`, a starter scene in `scenes/starter/`, instructions for coding agents and `.ancha/` in `.gitignore`. It installs nothing and runs nothing. With a `package.json` that has a `start` script, you can leave out `--agent`. See [`ancha init`](https://docs.ancha.io/cli/init) for the full list of files. ### Check the setup ```bash ancha doctor ``` ```text Output pass workspace: running locally (worlds in a server this command starts) pass node: Node 24.22.2; ancha 0.1.4 pass config: Configuration is valid pass scenes: 1 scenes are valid pass runtime: Backend accepted every scene and released its diagnostic environments pass agent: Agent completed a bounded diagnostic trial (exited) pass native:slack: Observed a successful native slack API request from the configured agent pass native:github: Observed a successful native github API request from the configured agent pass native:datadog: Observed a successful native datadog API request from the configured agent pass native:pagerduty: Observed a successful native pagerduty API request from the configured agent pass business: Diagnostic scene checks passed next: ancha run ``` `doctor` runs your agent once and confirms it reached each tool. ### Run the test ```bash ancha run ``` ```text Output running locally (worlds in a server this command starts) run_started runId=run_20261031-234205_e9f7049f942a39d5 scenes=["starter"] trials=1 parallel=1 workspace=local:/path/to/ancha-quickstart target=local trial_started scene=starter trial=1 envId=0ykgpn5i trial_finished scene=starter trial=1 passed=true valid=true warnings=[] exit=exited exitCode=0 realMs=79 failedChecks=[] ✓ starter 1/1 (100%) agent not sealed: network open; environment limited to 9 variables from this shell (pass more with --pass-env NAME) pass (minimum pass rate 100%) run run_20261031-234205_e9f7049f942a39d5 next: ancha baseline save before ``` The command exits with code `0`. The first line says where the worlds ran, here on a server the command started. The starter scene has four checks, one per tool, and each requires a call to that tool. The environment line says the agent got Ancha's variables plus a small base set from your shell, and nothing else. ## Break it and read why ### Remove the PagerDuty call ```bash sed -i.bak '/PAGERDUTY/d' agent.mjs ancha run ``` ```text Output running locally (worlds in a server this command starts) run_started runId=run_20261031-234205_7570556079db03cd scenes=["starter"] trials=1 parallel=1 workspace=local:/path/to/ancha-quickstart target=local trial_started scene=starter trial=1 envId=bfktffua trial_finished scene=starter trial=1 passed=false valid=true warnings=[] exit=exited exitCode=0 realMs=76 failedChecks=["pagerduty_access"] ✗ starter 0/1 (0%) ✗ pagerduty_access failed in 1/1 trials trial 1: The agent never made a call matching "* /services*". Its calls were: GET /api/conversations.list, GET /repos/acme/starter, GET /api/v1/dashboard. agent not sealed: network open; environment limited to 9 variables from this shell (pass more with --pass-env NAME) fail (minimum pass rate 100%) run run_20261031-234205_7570556079db03cd next: ancha explain run_20261031-234205_7570556079db03cd Patterns are " " matched against the service-relative path, and * is a wildcard. ``` The command exits with code `1`: a check failed. ### Explain the failure ```bash ancha explain latest ``` ```text Output starter trial 1 ✗ pagerduty_access expected: agent calls * /services* actual: never called; agent made 3 call(s) why: The agent never made a call matching "* /services*". Its calls were: GET /api/conversations.list, GET /repos/acme/starter, GET /api/v1/dashboard. trace: #1 agent GET /api/conversations.list -> 200 #2 agent GET /repos/acme/starter -> 200 #3 agent GET /api/v1/dashboard -> 200 environment: 72 variables from this shell were withheld from the agent. If it needs one (a provider key, say), pass it with --pass-env NAME or list it under agent.pass_env in ancha.yaml. Patterns are " " matched against the service-relative path, and * is a wildcard. next: ancha baseline save before --from run_20261031-234205_7570556079db03cd, then fix the agent, then ancha run --compare before ``` Each failed check shows what was expected, what happened and the calls the agent made, in order. The `next` line gives the commands that follow. ### Save the failing run as a baseline ```bash ancha baseline save before ``` ### Fix the agent and prove the fix ```bash mv agent.mjs.bak agent.mjs ancha run --compare before ``` ```text Output running locally (worlds in a server this command starts) run_started runId=run_20261031-234206_09b422aebb589f2b scenes=["starter"] trials=1 parallel=1 workspace=local:/path/to/ancha-quickstart target=local trial_started scene=starter trial=1 envId=qbw16ffq trial_finished scene=starter trial=1 passed=true valid=true warnings=[] exit=exited exitCode=0 realMs=77 failedChecks=[] ✓ starter 1/1 (100%) compared with run_20261031-234205_7570556079db03cd: no regression starter: 0% → 100% (+100 pp); 1 → 1 trials improved: pagerduty_access 0/1 → 1/1 agent not sealed: network open; environment limited to 9 variables from this shell (pass more with --pass-env NAME) pass (minimum pass rate 100%) run run_20261031-234206_09b422aebb589f2b ``` Exit code `0` means every check passed and no scene and no check got worse than the baseline. ## Test your own agent Point `agent.command` in `ancha.yaml` at the command that starts your agent, or let `ancha init` take it from your `package.json`, Procfile, `pyproject.toml` or Dockerfile. Your agent must read tool addresses and credentials from the environment variables Ancha sets. `ancha connect ` prints the SDK settings for a tool: ```bash ancha connect slack --lang ts ``` ```text Output Ancha supplies SLACK_API_URL, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_APP_TOKEN for the selected sandbox or trial. import { WebClient } from '@slack/web-api'; const slack = new WebClient(process.env.SLACK_BOT_TOKEN, { slackApiUrl: process.env.SLACK_API_URL }); await slack.conversations.list(); ``` If your agent needs a key from your shell, such as `OPENAI_API_KEY`, list its name under `agent.pass_env` in `ancha.yaml`. Apart from a base set such as `PATH` and `HOME`, Ancha withholds the rest of your shell from the agent. See [`ancha run`](https://docs.ancha.io/cli/run#what-the-agent-gets). The starter only proves your agent can reach each tool. Replace it with a scene that checks real work. [Test a Slack bot](https://docs.ancha.io/guides/test-a-slack-bot) walks through one. ## Next steps - [For coding agents](https://docs.ancha.io/coding-agents): Hand this loop to Claude Code, Codex or Cursor. - [Write a scene](https://docs.ancha.io/reference/test-file): Tools, starter data, a task and checks in one YAML file. - [Checks](https://docs.ancha.io/concepts/checks): State, trace and judge checks. - [Hosted workspace](https://docs.ancha.io/cloud/overview): Run the same worlds in your private workspace at app.ancha.io. # For coding agents Source: https://docs.ancha.io/coding-agents How a coding agent installs Ancha, runs an agent's tests with init, run and explain, reads JSON output and exit codes, proves a fix, and drives worlds from its own harness. This page tells a coding agent such as Claude Code, Codex or Cursor how to test and fix an AI agent with the Ancha CLI, version 0.1.4. The same text is at [/coding-agents.md](https://docs.ancha.io/coding-agents.md) as Markdown, and the whole site is at [/llms-full.txt](https://docs.ancha.io/llms-full.txt). ## Install Ancha needs Node.js 24 or later. | Option | Command | Then run Ancha as | | --- | --- | --- | | Global install | `npm install -g @ancha-labs/cli` | `ancha` | | Project dev dependency | `npm install --save-dev @ancha-labs/cli` | `npx @ancha-labs/cli` | | No install | nothing | `npx -y @ancha-labs/cli@0.1.4` | Check the install with `ancha --version`. It prints `0.1.4`. > [!WARNING] > > With npx, use the full package name, `@ancha-labs/cli`. There is no `ancha` package on npm. The instruction files `ancha init` writes say the same: run every command as `npx -y @ancha-labs/cli ` until the CLI is installed, then as `ancha `. ## The three commands Run every command from the project root, where `ancha.yaml` lives. | Step | Command | What it does | | --- | --- | --- | | 1. Set up | `ancha init` | Writes `ancha.yaml` with the start command it finds in `package.json`, a Procfile, `pyproject.toml` or the Dockerfile, a starter scene for the tools your dependencies use, agent instructions and `.ancha/` in `.gitignore`. Installs nothing, runs nothing. Pass `--agent ""` when nothing names a command. | | 2. Run | `ancha run --json` | Runs the agent against every scene in `ancha.yaml`. Each trial gets a fresh copy of the tools. | | 3. Explain | `ancha explain latest --json` | For each failed check: expected, actual, and the API calls behind it. Also crashes and timeouts with the end of the agent's log, coverage gaps, and what the agent's environment withheld. | `ancha doctor --json` checks the setup between steps 1 and 2. It runs the agent once and confirms it reached each tool. A tool the agent did not call is a warning, not a failure, and `doctor` exits `0`. Every command ends with `next: ` in text, and with a `next` list in JSON. Follow it. To prove a fix, save the failing run, change the agent, and compare: ```bash ancha baseline save before # edit the agent's code ancha run --compare before --json ``` Exit code `0` means every check passed and no scene and no check got worse than `before`. ## How your agent reaches the tools Ancha starts the agent with tool URLs and fake credentials in environment variables, for example `SLACK_API_URL` and `SLACK_BOT_TOKEN`. The agent must use them instead of real service URLs. `ancha connect --lang ts` (or `--lang py`) prints SDK settings for a tool. See [Environment variables](https://docs.ancha.io/reference/environment-variables) for the full list. The agent runs as a process on this computer with this computer's network. Its environment holds a base set (`PATH`, `HOME`, locale and the like), Ancha's variables and the tools' variables. Anything else from the shell, such as a provider key, reaches it only through `agent.pass_env` in `ancha.yaml` or `--pass-env NAME`. For a sealed run, set `agent.execution: { mode: container, image: node:24-slim }` in `ancha.yaml`; it needs Docker. See [`ancha run`](https://docs.ancha.io/cli/run#what-the-agent-gets). ## JSON output Add `--json` to any command for machine-readable output on stdout, or set `ANCHA_JSON=1`, which acts as `--json` on every command. - `ancha run --json`, `ancha wait --json` and `ancha logs --json` print newline-delimited JSON (NDJSON): one event per line, ending with `run_finished`. Do not parse stdout as a single JSON object. - Every other command prints one JSON object. - Errors print one object with an `error` key, and the process exits with the code in the table below. - `ancha schema --json` prints the JSON Schema of `ancha.yaml`, scene files and these outputs: `run_started`, `run_finished`, `explain`, `world_up`, `check` and `error`. ```json Error {"error":{"code":"USAGE","message":"unknown run \"run_nope\"","hint":"see .ancha/runs/"}} ``` Events from `ancha run --json`, in order: | Event | Fields | | --- | --- | | `run_started` | `runId`, `scenes`, `trials`, `parallel`, `workspace` (the world server, or `local:`), `target` (`local` or `docker`) | | `trial_started` | `scene`, `trial`, `envId` | | `trial_finished` | `scene`, `trial`, `passed`, `valid`, `warnings`, `exit`, `exitCode`, `realMs`, `failedChecks` | | `run_finished` | `summary`: the full result, described below; `next`: the commands to run next; `replayed`: `true` when an idempotency key returned an earlier run | ```json ancha run --json (failing run) {"event":"run_started","runId":"run_20261031-234207_17fd906ac2fe6ce5","scenes":["starter"],"trials":1,"parallel":1,"workspace":"local:/path/to/project","target":"local"} {"event":"trial_started","scene":"starter","trial":1,"envId":"vfbwwx5i"} {"event":"trial_finished","scene":"starter","trial":1,"passed":false,"valid":true,"warnings":[],"exit":"exited","exitCode":0,"realMs":76,"failedChecks":["github_access"]} {"event":"run_finished","summary":{"runId":"run_20261031-234207_17fd906ac2fe6ce5","startedAt":"2026-10-31T23:42:07.666Z","finishedAt":"2026-10-31T23:42:07.755Z","agent":"node agent.mjs","minPassRate":1,"verdict":"fail","scenes":[{"sceneId":"starter","trials":1,"passed":0,"passRate":0,"passAllK":false,"checks":{"slack_access":{"passed":1,"total":1,"required":true},"github_access":{"passed":0,"total":1,"required":true},"datadog_access":{"passed":1,"total":1,"required":true},"pagerduty_access":{"passed":1,"total":1,"required":true}},"agentIssues":[],"harnessIssues":[],"invalidTrials":0,"skippedJudges":0,"warnings":[],"failures":[{"sceneId":"starter","trial":1,"check":"github_access","expected":"agent calls * /*repos*","actual":"never called; agent made 3 call(s)","explanation":"The agent never made a call matching \"* /*repos*\". Its calls were: GET /api/conversations.list, GET /api/v1/dashboard, GET /services. Patterns are \" \" matched against the service-relative path, and * is a wildcard.","traceExcerpt":[{"seq":1,"source":"agent","method":"GET","path":"/api/conversations.list","status":200},{"seq":2,"source":"agent","method":"GET","path":"/api/v1/dashboard","status":200},{"seq":3,"source":"agent","method":"GET","path":"/services","status":200}]}],"coverageGaps":[],"meanRealMs":76,"agentCalls":3,"trialsDetail":[{"trial":1,"passed":false,"valid":true,"exit":"exited","exitCode":0,"realMs":76,"simMs":74,"sessionId":"226d6522146dc2ce4fffc2f6f79d3412","dir":"/path/to/project/.ancha/runs/run_20261031-234207_17fd906ac2fe6ce5/trial-starter-1"}]}],"sceneFingerprints":{"starter":"9502d255c5e452600216c2fba59d25116a5db5694e0678b5ed3122237f9cac3f"},"agentEnvironment":{"target":"host","inherited":9,"withheld":72},"workspace":"local:/path/to/project","target":"local"},"next":["ancha explain run_20261031-234207_17fd906ac2fe6ce5"]} ``` ## Retrying safely If your command can time out, give `ancha run` an `--idempotency-key `. Running it again with the same key returns the same run instead of starting another, with `replayed: true`. While the first run is still going, the retry fails with `SESSION_CONFLICT` and a hint to `ancha wait `. Use a new key for a new run. `ancha run --detach --json` returns a run ID at once; follow it with `ancha logs --follow --json`, which exits with the run's exit code. ## Exit codes | Code | Meaning | What to do | | --- | --- | --- | | `0` | Every required check passed, the pass rate met `min_pass_rate`, and nothing regressed. | Done. | | `1` | A check failed, the pass rate was too low, or a comparison regressed. A single check's drop counts; see `comparison.droppedChecks`. | Run `ancha explain latest --json` and fix the agent. | | `2` | Usage, config, scene or authentication error. `AUTH_INVALID` means `ancha login` again or a valid `ANCHA_API_KEY`. | Read the `hint`. Fix the command or the file named in the error. | | `3` | Infrastructure error: the server, the agent process or a trial's harness failed. | Not an agent failure. Run again, and `ancha doctor --json` if it repeats. | | `4` | The agent called an endpoint the replica does not implement. Only with `--strict`. | `ancha explain` lists the gaps either way. Check `ancha twins coverage ` before changing the agent. | When several apply, `3` wins over `4`, and `4` wins over `1`. `ancha explain` exits `0` even when it lists failures, so read the exit code of `ancha run`. ## Read a verdict The `run_finished` summary holds the verdict. These fields matter most: | Field | Meaning | | --- | --- | | `verdict` | `pass` or `fail` for the whole run. | | `workspace`, `target` | Where the worlds ran, and whether the agent ran as a local process or in a container. | | `scenes[].passed`, `scenes[].trials`, `scenes[].passRate` | Trials that passed out of trials run. A trial passes when every required check passes. | | `scenes[].checks` | Per check: `passed`, `total`, `required`. | | `scenes[].failures` | Failed checks with `expected`, `actual`, `explanation` and `traceExcerpt`. | | `scenes[].agentIssues` | The agent crashed, exited non-zero or timed out. | | `scenes[].harnessIssues`, `scenes[].invalidTrials` | Ancha itself failed during a trial. These trials are not agent failures. | | `scenes[].coverageGaps` | Calls to endpoints the replica does not implement, with counts. | | `scenes[].trialsDetail[].dir` | The trial's folder, with `agent.log`, `trace.jsonl`, `state.json` and `checks.json`. | | `agentEnvironment` | `target`, and how many shell variables the agent `inherited` and how many were `withheld`. | | `comparison.regressed`, `comparison.droppedChecks` | With `--compare`: whether anything got worse, and which checks dropped. | Read it in this order: 1. Exit `2` or `3`: fix the setup or retry. Do not change the agent yet. 2. `agentIssues` is not empty: the agent crashed or timed out. `ancha explain` shows the end of `agent.log`. 3. `failures` is not empty: compare `expected` with `actual`, then read the trace. Many bugs only appear with the world's data, so read the evidence before guessing from the code. 4. `coverageGaps` is not empty: the replica answered with an error because it does not implement that call. Check `ancha twins coverage ` before you change working code. 5. `agentEnvironment.withheld` is not zero and the agent needs one of those variables, such as a provider key: pass it with `--pass-env NAME` or `agent.pass_env`. A clean process exit does not mean the task passed. Only checks decide that. ## Rules for coding agents - Fix the agent's code. Do not edit `ancha.yaml`, the scenes or their checks to make a run pass. The exception is a check that is wrong for the real API, such as a method the official SDK never sends: tell the user, show the evidence from `ancha explain` and the vendor's documentation, and change it only with their agreement. - Never point the agent at real SaaS URLs, and never give it real credentials. - Never give the agent under test the scenes, checks, seeds or reference companions (`oracle.mjs`, `null.mjs`). - Use `--compare` against a saved baseline to prove a fix. Never compare a run with itself. - Keep `--json` on every command, and read the exit code. > [!NOTE] > > The starter scene only proves the agent can reach each tool. Replace it with a scene that checks real work. Its checks accept any HTTP method (`* /api/*.*` for Slack), so an agent built on an official SDK passes them as it is. ## Hosted workspace If the user has an Ancha workspace, `ancha login` asks them to approve this computer once in the browser; ask them to do it. A signed-in `ancha run` still runs the agent and its worlds on this computer, and its first line names the workspace. `ancha run --upload` sends the finished run to that workspace, where the console shows it. `ANCHA_SERVER_URL`, `server.url` in `ancha.yaml` or `--server-url` put the worlds in the workspace instead, and `--local` keeps a command on this computer. `ancha whoami --json` shows which workspace commands use. In CI, `ANCHA_SERVER_URL` and `ANCHA_API_KEY` replace the login, and `ancha run --local --upload` keeps the worlds on the runner and sends the results to the workspace. See [Hosted workspace](https://docs.ancha.io/cloud/overview) and [Results in your workspace](https://docs.ancha.io/cloud/results). ## Your own harness To start the agent yourself, start a world instead of a run: ```bash ancha world up --scene scenes/support-reply/scene.yaml --json # envId, and env: the tools' variables and the task # run the agent with exactly the variables in env ancha check --env --json # the scene's checks; exit 1 when a required one fails ancha world down --env ``` `ancha world trace --env --json` shows the calls so far. See [World lifecycle](https://docs.ancha.io/cli/world-lifecycle). `ancha world gen "" --json` writes a world file from a description; it needs `OPENROUTER_API_KEY`. See [`ancha world gen`](https://docs.ancha.io/cli/world-gen). ## What `ancha init` writes for coding agents | File | Contents | | --- | --- | | `AGENTS.md` | A managed section between `` and `` with the loop and the rules. Existing content is kept. | | `.claude/skills/ancha/SKILL.md` | A Claude Code skill named `ancha` with the same loop, the tools' variables and the exit codes. | See [`ancha init`](https://docs.ancha.io/cli/init). ## Prompt for your coding agent Paste this into Claude Code, Codex, Cursor or another coding agent, in the repo of the agent you want to test: ```text Prompt Use Ancha to test the AI agent in this repository and fix what fails. Docs for you: https://docs.ancha.io/coding-agents.md 1. If `ancha --version` fails, run `npm install -g @ancha-labs/cli`. 2. If there is no ancha.yaml, run `ancha init` (add --agent "" if it finds none), then `ancha doctor --json`. Make the agent read tool URLs and credentials from the environment variables Ancha sets. `ancha connect --lang ts` (or `--lang py`) prints the SDK settings. If the agent needs a provider key, list its name under agent.pass_env in ancha.yaml. 3. Run `ancha run --json`. Exit 0 means every check passed. Exit 1 means a check failed: run `ancha explain latest --json` and read expected, actual and the trace before you change code. 4. Before fixing, save the failing run: `ancha baseline save before`. After fixing, prove it: `ancha run --compare before --json`. Stop when it exits 0. 5. Fix the agent's code, not ancha.yaml, the scenes or their checks. Exit 2 is a setup or authentication error, 3 is infrastructure, 4 means the agent called an endpoint Ancha does not implement. Report those to me instead of working around them. 6. Never use real SaaS URLs or credentials. Never give the agent under test the scenes, checks or reference companions. Finish with what failed, what you changed, and the final `ancha run --compare before --json` result. ``` ## Discover the CLI - `ancha --help` is one page; `ancha advanced` lists the rest; `ancha --help --json` prints every command and flag as one JSON tree. - `ancha --help` prints one command's flags and defaults. - `ancha schema --json` prints every schema; `ancha docs ` prints the bundled guide. - [CLI reference](https://docs.ancha.io/cli/overview) documents every command in 0.1.4. - [Errors](https://docs.ancha.io/reference/errors) lists every error code. # Test a Slack bot Source: https://docs.ancha.io/guides/test-a-slack-bot Write a scene for a Slack support bot built on the official SDK, let Ancha catch a real bug in which thread it answers, read the evidence, and prove the fix. This guide tests a small Slack support bot built with `@slack/web-api`. You write a scene with a #support channel, two questions and checks for the right reply, run the bot three times, find a bug from the evidence, and prove the fix. It takes about ten minutes and needs no Slack workspace and no API keys. ## Before you start - Node.js 24 or later. - The Ancha CLI: `npm install -g @ancha-labs/cli`. See [Quickstart](https://docs.ancha.io/quickstart). ## 1. Create the bot ```bash mkdir slack-bot && cd slack-bot npm init -y npm pkg set type=module npm install @slack/web-api ``` Save this as `bot.mjs`. It reads #support, picks a question, and replies in its thread with an answer from a small playbook. It has a bug that Ancha will find. ```js bot.mjs // A small support bot: answers the newest question in #support, in its thread. import { WebClient } from '@slack/web-api'; const slack = new WebClient(process.env.SLACK_BOT_TOKEN, { slackApiUrl: process.env.SLACK_API_URL }); const playbook = [ { topic: /invite/i, answer: 'An admin can invite a teammate from Settings → Members → Invite.' }, { topic: /csv|export/i, answer: 'Reports → Exports, choose the month, then Download CSV. It can take two minutes to appear.' }, ]; const { channels } = await slack.conversations.list(); const support = channels.find((channel) => channel.name === 'support'); const { messages } = await slack.conversations.history({ channel: support.id }); // newest first const question = messages.findLast((message) => message.text.endsWith('?')); const entry = playbook.find((item) => item.topic.test(question.text)); const answer = entry ? entry.answer : 'Thanks, a teammate will follow up shortly.'; await slack.chat.postMessage({ channel: support.id, thread_ts: question.ts, text: answer }); console.log(`Answered: ${question.text}`); ``` The bot reads `SLACK_API_URL` and `SLACK_BOT_TOKEN`, so the same code runs against real Slack when you give it real values. ## 2. Set up Ancha ```bash ancha init --agent "node bot.mjs" rm -r scenes/starter ``` `init` writes `ancha.yaml` with your command, a starter scene and `.ancha/` in `.gitignore`. Because `package.json` depends on `@slack/web-api`, the starter only checks that the bot reaches Slack. Remove it; you will write a scene for this bot. ## 3. Write the scene Save this as `scenes/support-answer/scene.yaml`: ```yaml scenes/support-answer/scene.yaml id: support-answer title: Answer the newest question in #support twins: [slack] world: static start_time: 2026-10-26T14:00:00Z timeout: 1m task: Answer the newest question in Slack #support, in its thread. seed: slack: team: { name: Acme } bot: $bot users: - { ref: bot, name: ancha, real_name: Ancha assistant, is_bot: true } - { ref: ben, name: ben, real_name: Ben Carter } - { ref: mike, name: mike, real_name: Mike Turner } channels: - { ref: support, name: support, members: [$bot, $ben, $mike] } messages: - ref: csv_question channel: $support user: $mike at: -35m text: "Ridgeway Robotics cannot find last month's CSV export. Where should I point them?" - channel: $support user: $ben at: -32m thread_of: $csv_question text: "Reports → Exports. Choose the month, then Download CSV." - ref: invite_question channel: $support user: $mike at: -2m text: "Ridgeway Robotics wants to add a teammate. How do they invite someone?" checks: - id: answered_newest state: slack.message: channel: $support from: $bot in_thread_of: $invite_question text_contains: "Settings → Members → Invite" - id: posted_a_reply trace: { called: "POST /api/chat.postMessage" } pass: [answered_newest, posted_a_reply] ``` The world has an old question that Ben already answered and a new one from two minutes ago. The checks say what a good outcome looks like: - `answered_newest` looks at the final Slack state: the bot replied in the thread of the newest question, with the right instructions. `from: $bot` keeps Ben's earlier answer from counting. - `posted_a_reply` looks at the bot's calls. It uses `POST` because the SDK calls every Slack method with POST. Check the file: ```bash ancha scene validate scenes/support-answer/scene.yaml ``` ```text Output ok scenes/support-answer/scene.yaml (support-answer) ``` ## 4. Run it three times ```bash ancha run -n 3 ``` ```text Output running locally (worlds in a server this command starts) run_started runId=run_20261031-103344_2137d5d4f255e3a3 scenes=["support-answer"] trials=3 parallel=1 workspace=local:/path/to/slack-bot target=local trial_started scene=support-answer trial=1 envId=imz4odkx trial_finished scene=support-answer trial=1 passed=false valid=true warnings=[] exit=exited exitCode=0 realMs=92 failedChecks=["answered_newest"] trial_started scene=support-answer trial=2 envId=yusbkdtu trial_finished scene=support-answer trial=2 passed=false valid=true warnings=[] exit=exited exitCode=0 realMs=86 failedChecks=["answered_newest"] trial_started scene=support-answer trial=3 envId=jv8tr2mp trial_finished scene=support-answer trial=3 passed=false valid=true warnings=[] exit=exited exitCode=0 realMs=82 failedChecks=["answered_newest"] ✗ support-answer 0/3 (0%) ✗ answered_newest failed in 3/3 trials trial 1: The check wanted ≥1 message in #support from @ancha in reply to @mike: "Ridgeway Robotics wants to add a teammate. How…" containing "Settings → Members → Invite" but found 0 matching; @ancha has 1 message(s) in #support, 0 in that thread. agent not sealed: network open; environment limited to 9 variables from this shell (pass more with --pass-env NAME) fail (minimum pass rate 100%) run run_20261031-103344_2137d5d4f255e3a3 next: ancha explain run_20261031-103344_2137d5d4f255e3a3 ``` The command exits with code `1`. Each trial ran on a fresh copy of the channel, and all three failed the same way. The bot got only Ancha's variables and a small base set from your shell, which is all it needs. ## 5. Find out why ```bash ancha explain latest --trial 1 ``` ```text Output support-answer trial 1 ✗ answered_newest expected: ≥1 message in #support from @ancha in reply to @mike: "Ridgeway Robotics wants to add a teammate. How…" containing "Settings → Members → Invite" actual: 0 matching; @ancha has 1 message(s) in #support, 0 in that thread why: The check wanted ≥1 message in #support from @ancha in reply to @mike: "Ridgeway Robotics wants to add a teammate. How…" containing "Settings → Members → Invite" but found 0 matching; @ancha has 1 message(s) in #support, 0 in that thread. trace: #1 agent POST /api/conversations.list -> 200 #2 agent POST /api/conversations.history -> 200 #3 agent POST /api/chat.postMessage -> 200 environment: 70 variables from this shell were withheld from the agent. If it needs one (a provider key, say), pass it with --pass-env NAME or list it under agent.pass_env in ancha.yaml. next: ancha baseline save before --from run_20261031-103344_2137d5d4f255e3a3, then fix the agent, then ancha run --compare before ``` Without `--trial 1`, `explain` prints all three trials. The bot did post one message, just not in the newest question's thread. The trial's files show where it went: ```bash RUN=$(cat .ancha/runs/latest) cat .ancha/runs/$RUN/trial-support-answer-1/agent.log grep chat.postMessage .ancha/runs/$RUN/trial-support-answer-1/trace.jsonl ``` `agent.log` says `Answered: Ridgeway Robotics cannot find last month's CSV export. Where should I point them?`, and the request in `trace.jsonl` carries that question's `thread_ts`. The bot answered the oldest question. `conversations.history` returns messages newest first, and `findLast` takes the last match, which is the oldest. ## 6. Fix it and prove the fix Save the failing run as the reference, then fix the bot: ```bash ancha baseline save before ``` In `bot.mjs`, change `messages.findLast(` to `messages.find(` so the bot takes the newest question. Then run again and compare: ```bash ancha run -n 3 --compare before ``` ```text Output ✓ support-answer 3/3 (100%) compared with run_20261031-103344_2137d5d4f255e3a3: no regression support-answer: 0% → 100% (+100 pp); 3 → 3 trials improved: answered_newest 0/3 → 3/3 agent not sealed: network open; environment limited to 9 variables from this shell (pass more with --pass-env NAME) pass (minimum pass rate 100%) run run_20261031-103345_81849c0329dfa0fd ``` Exit code `0`: every check passed and no scene and no check got worse than `before`. ## Next steps - **Grade the wording.** Add a judge check, and keep it out of `pass` while you tune it. Without `OPENROUTER_API_KEY` it is skipped with a warning and does not change the verdict: ```yaml - id: helpful judge: "The reply tells the person exactly where in the product to invite a teammate." ``` - **Cover more cases.** Add scenes for a question the playbook does not know, a question in a thread, or no question at all. Each scene is a file under `scenes/`. - **Test a model-based bot.** Its answers vary, so run more trials and set `min_pass_rate` in `ancha.yaml`. List its provider key under `agent.pass_env`; Ancha withholds the rest of your shell from the bot. See [ancha.yaml](https://docs.ancha.io/reference/ancha-yaml). - **React to events.** A bot that listens for messages reads Slack events from the trial's event feed, or by HTTP or Socket Mode. See [Slack](https://docs.ancha.io/tools/slack#events). - **Run it in CI.** See [CI](https://docs.ancha.io/cloud/ci). - **Hand it to a coding agent.** See [For coding agents](https://docs.ancha.io/coding-agents). # How Ancha works Source: https://docs.ancha.io/concepts/overview The objects Ancha uses (agent, tool, world, test, run, attempt and check) and how the CLI's scene and trial vocabulary maps onto them. Ancha runs your agent against replicas of the tools it uses, many times, from the same starting point. ## Objects | Object | What it is | | --- | --- | | Agent | Your code, how it starts (a command or an HTTP endpoint) and the model provider it uses. | | Tool | A stateful replica of a service such as Slack or GitHub. It answers the real API's endpoints with realistic IDs, and its state changes as the agent acts. CLI output calls a tool a *twin*. | | World | A set of tools with starter data: channels and messages, repositories and issues, monitors, incidents, contacts and deals. See [Worlds](https://docs.ancha.io/concepts/worlds). | | Test | A world plus tasks and how many times to try each one, saved so it runs again against new agent code. See [Tests](https://docs.ancha.io/concepts/tests). | | Run | One execution of a test. | | Attempt | One independent try at one task, on its own fresh copy of the world. See [Attempts and runs](https://docs.ancha.io/concepts/attempts-and-runs). | | Check | A condition on the final world state or on the agent's API calls that decides whether an attempt passed. See [Checks](https://docs.ancha.io/concepts/checks). | ## What happens in a run ```mermaid flowchart LR W["Fresh world"] --> A["Agent works"] --> C["State and calls captured"] --> V["Checks decide"] ``` 1. Ancha creates a fresh copy of the world for each attempt, from the same seed. 2. It starts your agent with the task and the tool addresses and fake credentials in environment variables. 3. The agent calls the tools. Every call is recorded, and tool state changes as it would in the real service. 4. When the agent exits or times out, Ancha captures the final state and the call trace. 5. Checks run against what was captured. The attempt passes when every required check passes. ## Two vocabularies The hosted console and the CLI's playground commands use agents, worlds, tests, runs and attempts. `ancha run` is file-based and uses scenes and trials. They describe the same ideas: | Idea | Hosted console and `ancha playground` | `ancha run` | | --- | --- | --- | | World | A saved, versioned world | `twins`, `seed` and `world_rules` in a scene file | | Task | A task on a test | `task` in a scene file | | Checks | A JSON list on each task | `checks` in a scene file | | Test | A saved test | `ancha.yaml` plus the scene files it lists | | Attempt | Attempt | Trial | | Live copy of a world | Playground | Environment (`envId`) | ## Determinism The same world and seed always produce the same starting data, including record IDs. Tools run on a simulated clock that starts at a fixed time, so timestamps and time-based behavior repeat across attempts. A difference between two attempts comes from your agent. ## Where results live | You run | Results are stored in | | --- | --- | | `ancha run` | `.ancha/runs/` in your project. See [Attempts and runs](https://docs.ancha.io/concepts/attempts-and-runs). | | `ancha playground` | `.ancha/playgrounds/` in your project. | | Runs in the hosted console | Your hosted workspace's disk. | # Worlds Source: https://docs.ancha.io/concepts/worlds A world is the set of tools and starter data an attempt begins with. Every attempt gets its own copy, built from the same seed. A world is the starting point for an attempt: which tools exist and what data they hold. Ancha builds a fresh copy of the world for every attempt, so one attempt's writes never reach another. ## Starter data Every tool ships with versioned starter data for one fictional company, Acme, which sells warehouse analytics, and its customers: Ridgeway Robotics, whose pilot starts this week, Pinecrest Health, Bluefield Logistics, Kestrel Credit Union, Summit Freight, Harbor Foods and Helix Manufacturing. The records are linked across tools: the Ridgeway Robotics account in Salesforce, the deal in HubSpot and the discovery call in Gong describe the same customer, and the same people, Ben Carter, Rachel Moore, Mike Turner and their colleagues, appear in Slack, GitHub and PagerDuty. | Tool | Starter data (dataset `-starter`, version 1.1.0) | | --- | --- | | [Slack](https://docs.ancha.io/tools/slack) | Workspace "Acme", a bot and nine people, ten channels (#general, #support, #growth, #orders-incident, #product, #sales, #customer-success, #data-platform, #random and a private #leadership), 52 messages with threads. | | [GitHub](https://docs.ancha.io/tools/github) | Three repositories: `acme/orders` with seven commits, six pull requests and seven issues, plus `acme/web` and `acme/warehouse-sync`. | | [Datadog](https://docs.ancha.io/tools/datadog) | Seven monitors, three dashboards, five events, eight metric series and seventeen log lines across six services. | | [PagerDuty](https://docs.ancha.io/tools/pagerduty) | Four users, two escalation policies, four services and five resolved incidents. | | [HubSpot](https://docs.ancha.io/tools/hubspot) | Nine companies, thirteen contacts, eight deals and the default sales pipeline. | | [Salesforce](https://docs.ancha.io/tools/salesforce) | Seven accounts, six contacts, four leads, six opportunities, two campaigns and two campaign members. | | [Gong](https://docs.ancha.io/tools/gong) | Two users, two workspaces and seven calls, six with transcripts. | | [Google Analytics](https://docs.ancha.io/tools/google-analytics) | Two properties and nineteen sessions of historical traffic. | | [Google Ads](https://docs.ancha.io/tools/google-ads) | A manager account with one client account, a separate restricted account, and a search campaign with a budget, ad group, ad, keyword and two days of reports. | | [LinkedIn Ads](https://docs.ancha.io/tools/linkedin-ads) | Two ad accounts, a campaign group, two campaigns, two creatives and daily analytics. | | [ChatGPT Ads](https://docs.ancha.io/tools/chatgpt-ads) | Two ad accounts, two campaigns, an ad group, two ads and hourly insights. | | [Open-Meteo](https://docs.ancha.io/tools/open-meteo) | Ten places (San Francisco, New York, London, Berlin, Tokyo, Sydney, Portland, Chicago, Toronto and Singapore) with a synthetic climate. | | [Gmail](https://docs.ancha.io/tools/gmail) | The support mailbox with four labels, nineteen messages in threads, a draft and an attachment, plus Laura Kim's mailbox. | | [Google Drive](https://docs.ancha.io/tools/google-drive) | Thirty files in company, customer, engineering, finance and leadership folders, with permissions, comments and a trashed copy. | | [Microsoft Word](https://docs.ancha.io/tools/microsoft-word) | Contracts, policies and templates as `.docx` files, one with an earlier version and one shared with a customer. | | [Notion](https://docs.ancha.io/tools/notion) | Engineering and product pages with blocks and comments, a customer feedback database and meeting notes. | | [Dropbox](https://docs.ancha.io/tools/dropbox) | Sales, marketing, customer upload and team folders with a revision, a deleted file, a public link and a shared folder. | | [Resend](https://docs.ancha.io/tools/resend) | Two domains, two segments, six contacts, five sent emails with outcomes and two broadcasts. | | [Better Stack](https://docs.ancha.io/tools/better-stack) | Six monitors, two heartbeats, four incidents, two on-call rotations and a status page. | | [Linear](https://docs.ancha.io/tools/linear) | Two teams, two projects, five labels and seventeen issues. | | [Jira](https://docs.ancha.io/tools/jira) | Three projects, thirteen issues, three sprints and two issue links. | | [Okta](https://docs.ancha.io/tools/okta) | Twelve users across every lifecycle state, nine groups, eight SAML apps and ten System Log events. | | [Amazon S3](https://docs.ancha.io/tools/amazon-s3) | Four buckets with read-write, read-only and no access, holding customer exports, a sync staging area, backups and web assets. | | [Clerk](https://docs.ancha.io/tools/clerk) | Twelve users in five customer organizations, with pending invitations and sessions. | | [Attio](https://docs.ancha.io/tools/attio) | Seven companies, nine people, five deals, a renewals list, two notes and three tasks. | | [Stripe](https://docs.ancha.io/tools/stripe) | Three products, five customers with test cards, four subscriptions, six invoices, two payments and a refund. | | [PayPal](https://docs.ancha.io/tools/paypal) | A product with three plans, four subscriptions and five checkout orders. | | [Shopify](https://docs.ancha.io/tools/shopify) | A store with two locations, five products, seven customers and eight orders. | | [Lob](https://docs.ancha.io/tools/lob) | Nine addresses, two bank accounts, three templates, two letters, two postcards and a check. | | [Kalshi](https://docs.ancha.io/tools/kalshi) | Three series with twelve markets and an account with five orders. | Some starter records exist to test boundaries. For example, Slack's #leadership channel is private and the bot is not a member, and Salesforce and HubSpot each hold one account, Helix Manufacturing, that the agent's token cannot read. ## Presets A preset is a ready-made set of tools whose data fits together. List them with `ancha sandbox presets`. | Preset | Name | Tools | | --- | --- | --- | | `operations` | Engineering workspace | Slack, GitHub, Datadog, PagerDuty | | `b2b-salesforce` | Salesforce revenue workspace | LinkedIn Ads, Salesforce, Gong, Google Ads, ChatGPT Ads, Google Analytics | | `b2b-hubspot` | HubSpot revenue workspace | LinkedIn Ads, HubSpot, Gong, Google Ads, ChatGPT Ads, Google Analytics | A new hosted workspace also starts with a **Support workspace** world: Slack and GitHub with the starter data above. ## Naming records with refs Starter records have refs, short names that start with `$`. Checks and scene files use refs instead of generated IDs, for example `$slack_support` for the #support channel or `$github_orders` for `acme/orders`. Each [tool page](https://docs.ancha.io/tools/overview) lists the refs its starter data declares. In a scene file, you declare your own refs with `ref:` and use them with `$`. See [Test file](https://docs.ancha.io/reference/test-file). ## Time Each world runs on a simulated clock. A scene's `start_time` sets when the world begins, and `time_scale` sets how fast simulated time runs. Starter history sits before the start, for example a Slack message posted 35 minutes earlier. Because the clock is simulated, the same world gives the same timestamps on every attempt. ## Static and living worlds | Kind | Behavior | | --- | --- | | `static` | Nothing happens unless the agent acts. The starter scene is static. | | `living` | The world has rules that act on their own while the agent works: a person posts in Slack after two minutes, a fault keeps a metric high until a fix is deployed, a monitor alerts and pages PagerDuty. This is the default for scene files. | Living-world rules are written in a scene's `world_rules`. See [Test file](https://docs.ancha.io/reference/test-file#world-rules). ## Versions Worlds you save in the hosted console or with `ancha world create` are versioned. Editing a world creates a new revision. A test keeps the revision it was saved with until you choose a newer one, so old results stay comparable. Archiving hides a world from normal lists and keeps its history. ## Where worlds come from | You use | Define the world with | | --- | --- | | `ancha run` | A scene file: `twins`, `seed` and optional `world_rules`. See [Test file](https://docs.ancha.io/reference/test-file). | | `ancha playground` | `ancha world create --tools slack,github`. See [`ancha world`](https://docs.ancha.io/cli/world). | | Hosted console | The **Worlds** page. | | Exploring by hand | `ancha sandbox create --preset operations`. See [`ancha sandbox`](https://docs.ancha.io/cli/sandbox). | # Tests Source: https://docs.ancha.io/concepts/tests A test is a world plus tasks and how many times to try each one, saved so the same test runs again against new agent code. A test fixes everything except your agent: the world, the tasks, the checks and how many attempts each task gets. When you change the agent and run the test again, any difference in the results comes from the agent. ## What a test holds | Part | Meaning | | --- | --- | | World | The tools and starter data each attempt begins with. See [Worlds](https://docs.ancha.io/concepts/worlds). | | Tasks | What the agent should do, in plain language. | | Attempts | How many independent tries each task gets. | | Checks | Conditions that decide whether an attempt passed. Optional; without them attempts are ungraded. See [Checks](https://docs.ancha.io/concepts/checks). | | Time limit | How long an attempt may run before Ancha stops the agent. | ## Tests in `ancha run` `ancha run` reads its test from files in your repository, so the test is versioned with your code. - `ancha.yaml` lists scene files with globs, sets how many trials each scene gets and the minimum pass rate. See [ancha.yaml](https://docs.ancha.io/reference/ancha-yaml). - Each scene file holds one task with its world and checks. See [Test file](https://docs.ancha.io/reference/test-file). ```yaml ancha.yaml agent: command: node agent.mjs scenes: [scenes/**/scene.yaml] trials: 5 parallel: 2 min_pass_rate: 0.8 ``` A run passes when every scene's pass rate reaches `min_pass_rate` and no comparison regressed. ## Tests in the hosted console In the hosted console, open an agent and choose **Add tasks**. Pick a world, write the tasks, set how many times each repeats, add checks, then **Run tasks**. The test is saved, so you can run it again after you change the agent. See [Hosted workspace](https://docs.ancha.io/cloud/overview). ## Tests with `ancha playground` `ancha playground test create` saves the same kind of test locally: ```bash ancha playground test create --name "Support checks" \ --agent --world --tasks tasks.json --concurrency 2 --json ``` `tasks.json` is an array of tasks. Each task has a `prompt`, `attempts`, and optionally a `title`, `timeoutMs` and `checks`: ```json tasks.json [ { "title": "Answer the newest support question", "prompt": "Answer the newest unanswered question in Slack #support in its thread.", "attempts": 3, "timeoutMs": 120000, "checks": [ { "id": "replied", "trace": { "called": "POST /api/chat.postMessage" } } ] } ] ``` A test holds up to 20 tasks and 100 attempts in total. A task holds up to 20 checks. `timeoutMs` ranges from 1,000 to 3,600,000 and defaults to 300,000 (five minutes). Task checks use the scene check format but only `state` and `trace` checks; `judge` checks run only in `ancha run`. See [`ancha playground`](https://docs.ancha.io/cli/playground). ## Pinned inputs A saved test pins the world revision and its starter data. Starting it later uses the same data, with your current agent code. If you edit the world, the test keeps its revision until you choose the newer one. `ancha test save ` saves the setup of a completed `ancha agent run` as a reusable test. `ancha test run ` refuses to start when the saved scenario or data definitions have changed. See [`ancha test`](https://docs.ancha.io/cli/test). ## Baselines A baseline freezes the summary of a completed `ancha run` so later runs can be compared with it: ```bash ancha baseline save before ancha run --compare before ``` A comparison fails when the pass rate of any scene or any required check drops. It requires the same set of scenes with the same content, including their checks. Trial counts may differ. See [`ancha baseline`](https://docs.ancha.io/cli/baseline). # Attempts and runs Source: https://docs.ancha.io/concepts/attempts-and-runs A run tries every task of a test several times. Each attempt gets a fresh world and a fresh agent process, and Ancha records everything it did. A run is one execution of a test. It tries each task several times. Each try is an attempt. `ancha run` output calls an attempt a trial. Running the same task many times tells you how reliable the agent is. An agent that passes 3 of 5 attempts has a problem that a single run can hide. ## What each attempt gets | Fresh per attempt | Detail | | --- | --- | | World | Its own copy of the tools, built from the same seed. Writes in one attempt never appear in another. | | Tool addresses and credentials | Its own URLs and fake credentials, passed in environment variables. See [Environment variables](https://docs.ancha.io/reference/environment-variables). | | Agent process | Ancha starts the agent for the attempt and stops it at the end. | | State directory | `ANCHA_STATE_DIR` points to an empty directory for the agent's own files and memory. | The seed and starting data stay the same across attempts, so every attempt starts from the same state. ## Where the agent runs | You start the run with | The agent runs | Isolation | | --- | --- | --- | | `ancha run` | As an ordinary process on your computer, in the project directory, with a base set of your shell's variables, the names you allow in `agent.pass_env`, and Ancha's variables | Tool state is separate per attempt. The process itself is not sealed: it shares your files and network. Your shell's other variables, such as real credentials, are withheld unless you pass them. See [`ancha run`](https://docs.ancha.io/cli/run#what-the-agent-gets). | | `ancha run` with `agent.execution.mode: container` | In a local Linux container from an image you provide | Read-only source without Ancha's own files, only the environment variables you list, no network except the tools and the model hosts you allow. See [ancha.yaml](https://docs.ancha.io/reference/ancha-yaml#agent-execution). | | Your own harness, after `ancha world up` | Wherever your harness starts it | Your harness decides. The agent gets the world's tool variables and nothing that controls the world. `ancha check` grades the result. See [World lifecycle](https://docs.ancha.io/cli/world-lifecycle). | | `ancha playground start` | In a local container built from your project | A private network per attempt with its own tools. Needs Docker. See [`ancha playground`](https://docs.ancha.io/cli/playground). | | **Run tasks** in the hosted console | In a container in your hosted workspace | Reaches the tools and the model provider you chose, nothing else. See [Hosted workspace](https://docs.ancha.io/cloud/overview). | The worlds themselves run on a server `ancha run` starts, or in your hosted workspace after `ancha login`. The first line of every run says which, and the run's JSON carries `workspace` and `target`. See [CLI overview](https://docs.ancha.io/cli/overview#which-server-a-command-uses). ## Outcomes | Outcome | Meaning | | --- | --- | | Passed | Every required check passed. | | Failed | At least one required check failed. | | Agent issue | The agent crashed, exited with an error or ran past the time limit. Its checks are still evaluated against what it did. | | Invalid | Ancha itself failed during the attempt. The attempt does not count as an agent failure, and the run exits with code `3`. | | Ungraded | Its task has no checks, so the attempt finished without a verdict. | Coverage gaps are reported alongside the outcome. A gap means the agent called an endpoint the replica does not implement. It does not fail the attempt unless you pass `--strict`. ## Time limits Each attempt has a time limit: `timeout` in a scene file (default `5m`), or `timeoutMs` on a playground task (default 300,000 ms). When it runs out, Ancha stops the agent and records a timeout. `ancha doctor` uses a 10-second limit by default. Results report both real time (`realMs`) and simulated time (`simMs`). See [Worlds](https://docs.ancha.io/concepts/worlds#time). ## Parallel attempts `parallel` in `ancha.yaml`, or `--parallel`, sets how many trials of `ancha run` run at once. The default is 2 for command agents and 1 for HTTP endpoint agents. Playground runs use `--concurrency` (at most 10). The local container runtime admits at most three attempts of one run at a time and can run fewer when the machine is busy. ## What `ancha run` records Each run gets a directory under `.ancha/runs/`, named by its run ID, for example `run_20261031-071656_195dc8894e8b58a1`. | Path | Contents | | --- | --- | | `summary.json` | The run's summary: verdict, pass rates, check tallies, failures, issues, coverage gaps, where the worlds ran and what the agent's environment held. | | `inputs.json` | The exact scenes and seeds the run used, and how the agent was executed. | | `trial--/agent.log` | Everything the agent printed. | | `trial--/trace.jsonl` | Every API call in order, with method, path, status and response. | | `trial--/state.json` | The final state of every tool. | | `trial--/checks.json` | Each check's result with expected, actual and the trace entries behind it. | | `trial--/result.json` | The trial's outcome, exit code and timings. | | `.ancha/runs/latest` | The ID of the most recent completed run. Commands accept `latest` in place of a run ID. | `ancha explain`, `ancha report`, `ancha logs` and `ancha run --compare` read these files. `ancha report --html` writes a self-contained `report.html` into the run's directory. ## Long runs `ancha run --detach` keeps the run going in a background worker after the command returns. Follow it with `ancha logs --follow`, `ancha wait ` or `ancha status `. `logs --follow` and `wait` exit with the run's original exit code. A command that may time out can pass `--idempotency-key` so a retry returns the same run. See [`ancha run`](https://docs.ancha.io/cli/run#detached-runs). # Checks Source: https://docs.ancha.io/concepts/checks Checks decide whether an attempt passed. State checks read the final world, trace checks read the agent's API calls, process checks read what happened between the agent and the people in the world, and judge checks ask a model to grade text. A check is a named condition that Ancha evaluates when an attempt ends. An attempt passes when every required check passes. Without checks, an attempt is ungraded: it ran, but nothing says whether it did the right thing. ## Four kinds | Kind | Reads | Use it for | | --- | --- | --- | | `state` | The final state of the tools | Outcomes: a reply exists in the right thread, a file has the right content, an issue carries the right label, an incident is resolved. | | `trace` | The agent's API calls, in order | Behavior: the agent filed an issue, or never wrote to the CRM. | | Process checks | What the world's engine recorded between the agent and the people in it | Conversations: the agent asked the customer the right question, told the engineer the details, never leaked a confidential fact, and did things in the right order. `ancha run` and `ancha check` only. | | `judge` | The agent's API calls and final Slack messages, graded by a model | Qualities that exact conditions cannot express, such as whether a reply was correct and polite. `ancha run` and `ancha check` only. | Prefer state checks for outcomes. They pass however the agent got there, while trace checks depend on which endpoint it chose. ## Examples In a scene file, checks are YAML: ```yaml scene.yaml checks: - id: answered state: slack.message: channel: $slack_support from: $slack_bot in_thread_of: $slack_question text_contains: "Reports" - id: filed trace: { called: "POST /repos/acme/orders/issues" } - id: read_only_crm trace: { not_called: ["POST /crm/*", "PATCH /crm/*", "DELETE /crm/*"] } - id: asked_first before: [{ revealed: [laura.workspace] }, { called: "POST /repos/*/issues" }] - id: helpful judge: "The reply tells the customer where to find the CSV export and how long it takes." pass: [answered, filed, read_only_crm, asked_first] ``` In the hosted console and in playground task files, the same checks are a JSON list on each task: ```json [ {"id": "filed", "trace": {"called": "POST /repos/acme/orders/issues"}}, {"id": "answered", "state": {"slack.message": {"channel": "$slack_support", "from": "$slack_bot", "in_thread_of": "$slack_question", "text_contains": "issues/"}}} ] ``` ## Required checks `pass` lists the IDs of the required checks. It defaults to every check. A check outside `pass` is still evaluated and reported, but it does not decide the outcome. Use that for advisory checks, such as a judge rubric you are still tuning. ## Trace patterns A trace pattern is `" "`, for example `"POST /api/chat.postMessage"`. - The path is relative to the tool's base URL and has no query string: `/repos/acme/starter`, not the full URL. - The method is compared without regard to case. `*` as the method accepts any method. The path must match exactly, including case. - `*` in the path matches any run of characters, including `/`. `"POST /crm/*"` matches every POST under `/crm/`. The pattern `"*"` matches any call. - Only the agent's own calls count. Actions taken by the world, such as a person posting in Slack, never match. | Field | Passes when | | --- | --- | | `called` | Every listed pattern matches at least one call. | | `not_called` | No call matches any listed pattern. | With both fields set, both must hold. > [!TIP] > > Use `*` as the method when the verb does not matter. Slack's official SDKs call every Web API method with `POST`, while plain `fetch` code often uses `GET`, and Slack accepts both. The starter scene's checks are written this way, for example `* /api/*.*`. Check the agent's calls with `ancha explain` or in `trace.jsonl` when a trace check surprises you. ## State checks | Check | Passes when | | --- | --- | | `slack.message` | At least `count_gte` messages (default 1) in a channel match all the conditions given: author, a thread, a case-insensitive substring. | | `github.file_at` | A file at a branch, commit or `production` contains, or equals, the given text. | | `github.pull` | A pull request is merged, or not. | | `github.production` | Production is exactly a given commit. | | `github.issue` | Issues in a repository match a title, body, labels, state and count. | | `datadog.monitor` | A monitor's state is `OK` or `Alert`. | | `datadog.downtime` | An active downtime covers a monitor with enough time left and a bounded total length. | | `pagerduty.incident` | An incident has a status, an exact set of assignees, or was acknowledged or resolved within a time. | | `fault_resolved` | A living-world fault is resolved. | State checks count every record in the final state, including starter data. Set `from` on a `slack.message` check so a message from the starter history cannot satisfy it. See [Checks reference](https://docs.ancha.io/reference/checks) for every field. ## Process checks A scene can hold [people](https://docs.ancha.io/reference/test-file#people): a customer who holds back details until asked, an engineer who knows the workaround, a manager who decides the priority. A model writes what they say, but the world's engine decides who receives a message, which facts a person may share, what they decide and when they answer, and it records every one of those events. Process checks read those records. | Check | Passes when | | --- | --- | | `revealed` | The agent asked, and the owner of each fact shared it. | | `knows`, `told` | A person knows each fact, from the agent or from someone else. | | `decided` | A person made a decision, with a given option. | | `message` | At least one message matching a selector reached a person or a place. | | `never` | No message matched the selector, for example a confidential fact never reached the customer. | | `before` | Milestones happened in order: a call, a message, or the first time a predicate held. | | `within` | A milestone happened within a time limit of the start or of another milestone. | Because the checks read the engine's records and never the model's text, a person who writes something unexpected cannot pass or fail an attempt by wording alone. A milestone that never happened fails `before` and `within`, so an attempt that did nothing cannot pass them. See [Process checks](https://docs.ancha.io/reference/checks#process-checks). ## Judge checks A judge check is a rubric in plain language. After the attempt, Ancha sends a model a bounded copy of the evidence: the agent's API calls to Slack, GitHub, Datadog and PagerDuty with their requests and responses, and the final Slack messages, channels and users. The model returns passed or failed with a reason and the trace entries it relied on. Credentials, the scene's seed and the other checks are never sent. - Judge checks need `OPENROUTER_API_KEY` in the environment of the `ancha run` or `ancha check` command. Ancha calls the model through OpenRouter. The agent does not get the key unless you list it in `agent.pass_env`. - Set `judge.model` in `ancha.yaml` to the OpenRouter model you want the judge to use. - Without a key, judge checks are skipped. A skipped check never passes, so a required judge check fails the attempt. Keep advisory rubrics out of `pass`. - If the model call fails, the trial is marked invalid instead of failed. - Judge checks run in `ancha run` and `ancha check` only. The hosted console and playground tasks accept `state` and `trace` checks. ## Refs Checks name records with refs such as `$slack_support`, not with generated IDs. Refs come from the world's starter data or from your scene's `seed`, and people from its `people`. Saving a task in the hosted console checks every ref against its world. See [Worlds](https://docs.ancha.io/concepts/worlds#naming-records-with-refs). ## Results Every check result has the same fields: | Field | Meaning | | --- | --- | | `id` | The check's ID. | | `required` | Whether it is in `pass`. | | `passed` | The result. | | `expected` | What the check wanted, in words. | | `actual` | What the world held or what the agent did. | | `explanation` | One sentence combining the two. | | `evidence` | Sequence numbers of the trace entries behind the result. | `ancha explain` prints these for failed checks, `ancha check --json` prints them all for a live world, and each trial's `checks.json` holds them all. # Tools Source: https://docs.ancha.io/tools/overview The 38 tools Ancha replicates, which API version each one follows, how many endpoints it implements, and how your agent connects to it. Each tool is a stateful replica of a real service's API. It answers the same endpoints with the same response shapes, keeps state that changes as your agent acts, and starts from fictional starter data about one company, Acme, and its customers. Your agent talks to it with its normal SDK or HTTP client, pointed at the URL Ancha provides. ## The 38 tools Use the key in scene files (`twins: [slack, github]`), in `--tools` lists and in commands such as `ancha twins coverage slack`. ### Communication and documents | Tool | Key | API | Endpoints | Starter data | | --- | --- | --- | --- | --- | | [Slack](https://docs.ancha.io/tools/slack) | `slack` | Web API subset, bot token | 22 | A workspace with ten channels, nine people and 52 messages | | [Gmail](https://docs.ancha.io/tools/gmail) | `gmail` | v1 subset | 26 | A support mailbox with labels, threads, a draft and an attachment | | [Google Drive](https://docs.ancha.io/tools/google-drive) | `google-drive` | Drive API v3 subset | 26 | Folders of Docs, Sheets, Slides, PDFs and CSVs with permissions and comments | | [Microsoft Word](https://docs.ancha.io/tools/microsoft-word) | `microsoft-word` | Graph v1.0 drive items | 21 | Contracts, policies and templates as `.docx` files in one OneDrive | | [Notion](https://docs.ancha.io/tools/notion) | `notion` | Notion-Version 2025-09-03 subset | 19 | Engineering and product pages, a feedback database and meeting notes | | [Dropbox](https://docs.ancha.io/tools/dropbox) | `dropbox` | API v2 subset | 24 | Sales, marketing and customer folders with revisions, shared links and a shared folder | | [Resend](https://docs.ancha.io/tools/resend) | `resend` | REST subset | 40 | Domains, segments, contacts, sent emails and broadcasts | ### Engineering and operations | Tool | Key | API | Endpoints | Starter data | | --- | --- | --- | --- | --- | | [GitHub](https://docs.ancha.io/tools/github) | `github` | REST subset | 71 | Three repositories with commits, pull requests, labeled issues and check runs | | [Datadog](https://docs.ancha.io/tools/datadog) | `datadog` | API v1 and v2 subset | 26 | Seven monitors, three dashboards, metrics, logs and events | | [PagerDuty](https://docs.ancha.io/tools/pagerduty) | `pagerduty` | REST v2 and Events v2 subset | 17 | Four services, two escalation policies and five resolved incidents | | [Better Stack](https://docs.ancha.io/tools/better-stack) | `better-stack` | Uptime API v2 and v3 subset | 29 | Monitors, heartbeats, incidents, on-call and a status page | | [Linear](https://docs.ancha.io/tools/linear) | `linear` | GraphQL subset | 27 | Two teams, two projects and seventeen issues | | [Jira](https://docs.ancha.io/tools/jira) | `jira` | REST v3 and Agile 1.0 subset | 57 | Three projects, thirteen issues and three sprints | | [Okta](https://docs.ancha.io/tools/okta) | `okta` | Management API v1 subset | 31 | Twelve users in every lifecycle state, nine groups, eight apps and the System Log | | [Amazon S3](https://docs.ancha.io/tools/amazon-s3) | `amazon-s3` | REST (SigV4) subset | 25 | Four buckets with different access, customer exports and web assets | | [Clerk](https://docs.ancha.io/tools/clerk) | `clerk` | Backend API v1 subset | 32 | Customers' users, five organizations, invitations and sessions | ### Revenue and commerce | Tool | Key | API | Endpoints | Starter data | | --- | --- | --- | --- | --- | | [HubSpot](https://docs.ancha.io/tools/hubspot) | `hubspot` | CRM v3 | 18 | Nine companies, thirteen contacts, eight deals and a pipeline | | [Salesforce](https://docs.ancha.io/tools/salesforce) | `salesforce` | REST v67.0 | 14 | Seven accounts, contacts, leads, six opportunities and two campaigns | | [Attio](https://docs.ancha.io/tools/attio) | `attio` | REST v2 subset | 30 | People, companies, deals, a renewals list, notes and tasks | | [Gong](https://docs.ancha.io/tools/gong) | `gong` | v2 subset, read-only | 5 | Two workspaces and seven calls, six with transcripts | | [Stripe](https://docs.ancha.io/tools/stripe) | `stripe` | API 2026-10-28.endive subset, test mode | 34 | Products and prices, five customers, subscriptions, invoices, payments and a refund | | [PayPal](https://docs.ancha.io/tools/paypal) | `paypal` | REST subset | 35 | Plans and subscriptions, checkout orders and reports | | [Shopify](https://docs.ancha.io/tools/shopify) | `shopify` | Admin GraphQL 2026-10 subset | 32 | A store with products, inventory at two locations, customers and eight orders | | [Lob](https://docs.ancha.io/tools/lob) | `lob` | Print & Mail API v1 subset | 28 | Addresses, letters, postcards, a check, bank accounts and templates | ### Marketing and analytics | Tool | Key | API | Endpoints | Starter data | | --- | --- | --- | --- | --- | | [Google Analytics](https://docs.ancha.io/tools/google-analytics) | `ga4` | Data API v1beta, REST | 4 | Two properties with historical sessions | | [Google Ads](https://docs.ancha.io/tools/google-ads) | `google-ads` | v26, REST and gRPC | 9 | Accounts, a search campaign, budget, ad group, ad and reports | | [LinkedIn Ads](https://docs.ancha.io/tools/linkedin-ads) | `linkedin-ads` | 202610, Rest.li 2.0 | 17 | Ad accounts, campaigns, creatives and analytics | | [ChatGPT Ads](https://docs.ancha.io/tools/chatgpt-ads) | `chatgpt-ads` | Advertiser API v1 | 26 | An ad account, campaigns, an ad group, ads and insights | ### Other | Tool | Key | API | Endpoints | Starter data | | --- | --- | --- | --- | --- | | [Open-Meteo](https://docs.ancha.io/tools/open-meteo) | `open-meteo` | v1 subset, no key | 2 | Ten places with a synthetic climate | | [Kalshi](https://docs.ancha.io/tools/kalshi) | `kalshi` | Trade API v2 subset | 25 | Three series of markets with order books and an account with positions | `ancha twins list` prints the same keys and counts. Eight more tools ship in 0.1.3: Twilio (`twilio`), Google Calendar (`google-calendar`), Intercom (`intercom`), Zendesk (`zendesk`), Asana (`asana`), Discord (`discord`), Sentry (`sentry`) and Airtable (`airtable`). `ancha twins` lists them with their endpoint counts, and `ancha twins coverage ` lists the endpoints each one implements. ## How your agent connects For every tool in the world, Ancha sets environment variables with the tool's base URL and fake credentials before it starts your agent. Point your SDK or HTTP client at the URL and send the credentials the way the real service expects them. The fake credentials work only against this attempt's tools. ```bash ancha connect github --lang ts ``` ```text Output Ancha supplies GITHUB_API_URL, GITHUB_TOKEN, GITHUB_WEBHOOK_SECRET for the selected sandbox or trial. import { Octokit } from '@octokit/rest'; const github = new Octokit({ auth: process.env.GITHUB_TOKEN, baseUrl: process.env.GITHUB_API_URL }); await github.repos.get({ owner: 'acme', repo: 'starter' }); ``` `ancha connect ` prints settings for Node (`--lang ts`) for every tool, and for Python (`--lang py`) for Slack, GitHub and Google Ads. Where an official SDK cannot take a base URL, the snippet shows the custom fetch or adapter that sends its requests to the replica, as for Stripe, Dropbox, Shopify and PayPal. See [`ancha connect`](https://docs.ancha.io/cli/connect) and [Environment variables](https://docs.ancha.io/reference/environment-variables) for every variable. Locally, tool URLs look like `http://127.0.0.1:/e//github`. In a hosted workspace they look like `https://app.ancha.io/t//e//github`. Read them from the environment; never build them yourself. ## Coverage Every replica implements a documented subset of the real API, not the whole service. `ancha twins coverage ` lists each implemented endpoint with its status and notes: ```bash ancha twins coverage slack ``` | Status | Meaning | | --- | --- | | `supported` | The endpoint behaves like the real one for the documented request shapes. | | `partial` | The endpoint works for the common cases. Its notes list the parameters and behaviors it does not model. | A call to anything else fails visibly. The replica answers in the tool's native error shape, with a message that starts with `ancha: not implemented:` and the response header `x-ancha-gap: 1`: | Tool | Response to an unimplemented call | | --- | --- | | Slack | HTTP 200 with `{"ok":false,"error":"unknown_method","ancha_gap":"ancha: not implemented: POST /api/chat.scheduleMessage"}` | | GitHub | HTTP 501 with `{"message":"ancha: not implemented: GET /repos/acme/orders/releases", ...}` | | HubSpot, Salesforce | HTTP 400 with the tool's validation error and an `ancha: not implemented:` message | | Linear, Shopify | A GraphQL error that names the unsupported field or operation | | Others | HTTP 501 with the tool's error shape | `ancha run` and `ancha explain` report these calls as coverage gaps. With `--strict`, any gap makes the run exit with code `4`. ## Fidelity Responses follow the real APIs' documented shapes and were tested with the official clients where a tool page names one. They have not been compared call by call with the live services. Behavior that depends on the real world is not simulated: nothing is probed, printed, mailed, delivered or traded for real, reporting data is historical, and mail, invitations and notifications are recorded rather than sent. Each tool page lists its limits. # Slack Source: https://docs.ancha.io/tools/slack A stateful replica of Slack's Web API for bot tokens: channels, direct messages, messages, threads, reactions and users, with Events API delivery and an MCP server. The Slack replica implements 22 Web API methods for a bot token, with Events API delivery for messages and reactions. Its key is `slack`. It was tested with `@slack/web-api` and `@slack/bolt`. ## Connect | Variable | Value | | --- | --- | | `SLACK_API_URL` | Base URL of the Web API. It ends in `/api/`, so append the method name: `SLACK_API_URL + "chat.postMessage"`. | | `SLACK_BOT_TOKEN` | Fake bot token. Send it as `Authorization: Bearer `. | | `SLACK_SIGNING_SECRET` | Signs HTTP event deliveries. Verify signatures against the raw request body. | | `SLACK_APP_TOKEN` | Fake app-level token for Socket Mode. | ```js Node import { WebClient } from '@slack/web-api'; const slack = new WebClient(process.env.SLACK_BOT_TOKEN, { slackApiUrl: process.env.SLACK_API_URL }); await slack.conversations.list(); ``` ```python Python import os from slack_sdk import WebClient slack = WebClient(token=os.environ['SLACK_BOT_TOKEN'], base_url=os.environ['SLACK_API_URL'].rstrip('/') + '/') slack.conversations_list() ``` These are the snippets `ancha connect slack --lang ts` and `--lang py` print. The SDKs call every method with `POST`, which matters for [trace checks](https://docs.ancha.io/concepts/checks#trace-patterns). For Bolt in Socket Mode, set the local URL on both the app's `clientOptions` and the receiver's `installerOptions.clientOptions`. Otherwise the receiver opens its socket against real Slack. ## Endpoints Every method is called with `POST` at `/api/`, so trace checks look like `POST /api/chat.postMessage`. | Method | Support | Notes | | --- | --- | --- | | `auth.test` | supported | Also allowed while an SDK starts up, before the attempt's clock starts. | | `conversations.list` | partial | `types=im` lists the bot's direct messages. `mpim` is accepted and matches nothing: multi-person direct messages are not modeled. | | `conversations.info` | supported | | | `conversations.history` | supported | Newest first, with cursors. | | `conversations.replies` | supported | Parent first, then replies oldest first. | | `conversations.join` | supported | | | `conversations.create` | supported | | | `conversations.members` | supported | | | `conversations.open` | partial | One user opens a direct message, created on first use, or returns an existing one; `return_im` and `prevent_creation`. Multi-person direct messages are a gap. | | `chat.postMessage` | partial | Text and a subset of blocks. A user ID as `channel` posts in the direct message with that user. Emits `message` events in channels where the bot is a member and in its direct messages. | | `chat.update` | partial | Text and blocks; `metadata` and `attachments` are coverage gaps. Emits `message_changed`. | | `chat.delete` | partial | The bot's own messages. Emits `message_deleted`. Deleting a thread parent leaves its replies in place. | | `chat.getPermalink` | supported | Returns a fictional workspace address. | | `reactions.add` | supported | Emits `reaction_added`. | | `reactions.remove` | partial | Emits `reaction_removed`. | | `reactions.get` | supported | | | `search.messages` | partial | Needs a user token. The bot token, the only token Ancha provides, is rejected. | | `users.list` | supported | | | `users.info` | supported | | | `users.lookupByEmail` | supported | | | `users.conversations` | partial | `types=im` lists the bot's direct messages; `mpim` matches nothing. | | `apps.connections.open` | partial | Socket Mode only, with `SLACK_APP_TOKEN`. One live socket per attempt. | Any other method returns HTTP 200 with `{"ok": false, "error": "unknown_method"}`, an `ancha_gap` message and the header `x-ancha-gap: 1`. ## Events Posting, editing and deleting messages and adding or removing reactions emit Events API envelopes for channels where the bot is a member. A person in the world who mentions the bot also emits `app_mention`. Starter history does not emit events. `ancha run` delivers events in one of three ways, set by `agent.slack_transport` in `ancha.yaml`: | Transport | How the agent receives events | | --- | --- | | `feed` | Polls `ANCHA_EVENTS_URL` with `ANCHA_EVENTS_TOKEN`. The default for command agents. | | `http` | Ancha POSTs signed requests to the agent's URL, after the Slack `url_verification` handshake. The default for endpoint agents. | | `socket` | Socket Mode through `apps.connections.open`. Command agents only. | See [ancha.yaml](https://docs.ancha.io/reference/ancha-yaml) and [Environment variables](https://docs.ancha.io/reference/environment-variables). ## MCP server `ancha mcp serve --sandbox ` runs a Slack MCP server over stdio, profile `ancha-slack-v1`, against a running sandbox. `ancha mcp config --sandbox ` prints the `mcpServers` entry to add to your MCP host. It exposes 19 tools that call the same Web API and appear in the same trace: `slack_auth_test`, `slack_conversations_list`, `slack_conversations_info`, `slack_conversations_history`, `slack_conversations_replies`, `slack_conversations_members`, `slack_conversations_join`, `slack_conversations_create`, `slack_chat_postMessage`, `slack_chat_update`, `slack_chat_delete`, `slack_chat_getPermalink`, `slack_reactions_add`, `slack_reactions_remove`, `slack_reactions_get`, `slack_users_list`, `slack_users_info`, `slack_users_lookupByEmail`, `slack_users_conversations`. The MCP server writes plain-text messages only and has no reset or admin tools. See [`ancha mcp`](https://docs.ancha.io/cli/mcp). ## Starter data The `slack-starter` dataset is the workspace **Acme**: a bot, nine people and ten channels, with 52 messages of history. Worlds, sandboxes and the hosted console use it. Scene files carry their own `seed`. | Ref | User | | --- | --- | | `$slack_bot` | `@ancha` ("Ancha assistant"), a bot. Your agent acts as this user. | | `$slack_ben` | Ben Carter, `ben@acme.example` | | `$slack_rachel` | Rachel Moore, `rachel@acme.example` | | `$slack_mike` | Mike Turner, `mike@acme.example` | | `$slack_greg` | Greg Wilson, `greg@acme.example` | | `$slack_laura` | Laura Kim, `laura@acme.example` | | `$slack_tom` | Tom Hughes, `tom@acme.example` | | `$slack_ellen` | Ellen Price, `ellen@acme.example` | | `$slack_victor` | Victor Lane, `victor@acme.example` | | `$slack_anna` | Anna Fischer, `anna@acme.example` | | Ref | Channel | Members | | --- | --- | --- | | `$slack_general` | `#general`, "Company updates" | Everyone, including the bot | | `$slack_support` | `#support`, "Customer questions and follow-ups" | Bot, Ben, Mike, Greg, Laura, Anna | | `$slack_growth` | `#growth`, "Ridgeway Robotics pilot" | Bot, Ben, Mike, Tom, Ellen | | `$slack_orders` | `#orders-incident`, "Orders API operations" | Bot, Ben, Rachel, Greg | | `$slack_leadership` | `#leadership`, private, "Private planning" | Ben and Mike. The bot is not a member. | | `$slack_product` | `#product`, "Roadmap and customer feedback" | Bot, Ben, Ellen, Greg, Laura | | `$slack_sales` | `#sales`, "Deals and pipeline" | Bot, Tom, Mike, Anna, Ben | | `$slack_success` | `#customer-success`, "Renewals and onboarding" | Bot, Laura, Mike, Tom | | `$slack_data` | `#data-platform`, "Warehouse sync and pipelines" | Bot, Victor, Greg, Rachel | | `$slack_random` | `#random`, "Anything else" | Everyone except the bot | The messages tell one story: a support channel with customer questions and answers, an Orders API incident that was rolled back two hours ago, a sales pipeline, a product roadmap and a data platform team. The ones with refs, which checks can name: | Ref | Message | | --- | --- | | `$slack_question` | Mike in #support, 35 minutes before start: "Ridgeway Robotics cannot find last month's CSV export. Where should I point them?" | | `$slack_answer`, `$slack_followup` | Ben's answer in that thread ("Reports → Exports. Choose the month, then Download CSV. The export can take two minutes to appear.") and Mike's thanks. | | `$slack_access` | Ben in #support, 20 minutes before start: a second customer needs help inviting a teammate; an admin can use Settings → Members → Invite. | | `$slack_sso`, `$slack_double`, `$slack_seats` | Three more support threads: Bluefield Logistics asking about SSO with Okta and SCIM, Kestrel Credit Union's dashboard showing yesterday's numbers twice (fixed in 4.12), and Pinecrest Health adding 20 seats mid-contract (prorated). | | `$slack_brief`, `$slack_brief_reply` | Ben in #growth: the Ridgeway Robotics pilot is this week's focus; Mike's reply about the discovery call. | | `$slack_case_study` | Ellen in #growth: the warehouse-to-pipeline case study draft is ready for review, with a thread. | | `$slack_ops`, `$slack_docs` | Rachel in #orders-incident: the Orders API is healthy and the slow-request incident is resolved. Ben: a draft runbook update is in GitHub. | | `$slack_latency` | Rachel in #orders-incident, 3 hours before start: p95 latency above 700 ms, with a thread about the 4.12.1 deploy and the rollback. | | `$slack_welcome`, `$slack_roadmap`, `$slack_signed`, `$slack_hello_victor` | #general: a welcome, Thursday's roadmap review, Pinecrest Health signing for 120 seats, and Victor joining the data platform team. | | `$slack_feedback`, `$slack_spec` | #product: this month's top feedback and the scheduled reports spec. | | `$slack_renewal` | Mike in #sales: the Kestrel Credit Union renewal moves to contract next week. | | `$slack_connector` | Victor in #data-platform: the Snowflake connector upgrade is tomorrow at 9am. | | `$slack_private` | Ben in #leadership: a draft planning note. | | `$slack_sourdough` | Greg in #random, about a sourdough starter. | `ancha sandbox configuration --tools slack --json` prints every message with its text and time. ## Checks Use `slack.message` to check what ended up in a channel: ```yaml - id: answered_in_thread state: slack.message: channel: $slack_support from: $slack_bot in_thread_of: $slack_question text_contains: "Exports" ``` It counts messages in `channel` that match every condition you set and passes when there are at least `count_gte` (default 1). `text_contains` ignores case. `in_thread_of` counts replies, not the parent. Set `from`, or a message from the starter history can satisfy the check. See [Checks reference](https://docs.ancha.io/reference/checks#slack-message). ## Limits - Bot token only: no user OAuth and no user-token search. The bot can open and post in a direct message with one user; multi-person direct messages are not modeled. - No file uploads, no Slack UI and no rate-limit model. - Unsupported message arguments, such as `attachments`, `metadata` and broadcast replies, return an error and change nothing. # Gmail Source: https://docs.ancha.io/tools/gmail A stateful replica of the Gmail API v1: mailboxes, messages, threads, labels, drafts, attachments and history, where mail moves only between the mailboxes in the world. The Gmail replica implements 26 endpoints of the Gmail API v1 for the mailboxes in the world. Sending a message delivers it to a seeded mailbox or records it in Sent; nothing leaves the world. Its key is `gmail`. It was tested with `@googleapis/gmail`. ## Connect | Variable | Value | | --- | --- | | `GMAIL_API_URL` | Base URL. Paths start with `/gmail/v1/`. | | `GMAIL_ACCESS_TOKEN` | Fake OAuth access token for the agent's mailbox. Send `Authorization: Bearer `. | ```js Node import { auth, gmail } from '@googleapis/gmail'; const oauth = new auth.OAuth2(); oauth.setCredentials({ access_token: process.env.GMAIL_ACCESS_TOKEN }); const mail = gmail({ version: 'v1', auth: oauth }); const options = { rootUrl: process.env.GMAIL_API_URL + '/', retry: false }; const { data } = await mail.users.messages.list({ userId: 'me', q: 'is:unread' }, options); ``` This is what `ancha connect gmail --lang ts` prints. Google's client takes the replica as a per-request `rootUrl` ending in `/`; a global `rootUrl` keeps only the origin and drops the path. Use `me` as the user ID. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `GET` | `/gmail/v1/users/:userId/profile` | supported | | `GET` | `/gmail/v1/users/:userId/messages` | partial | | `GET` | `/gmail/v1/users/:userId/messages/:id` | supported | | `POST` | `/gmail/v1/users/:userId/messages/send` | partial | | `POST` | `/gmail/v1/users/:userId/messages/batchModify` | supported | | `POST` | `/gmail/v1/users/:userId/messages/:id/modify` | supported | | `POST` | `/gmail/v1/users/:userId/messages/:id/trash` | supported | | `POST` | `/gmail/v1/users/:userId/messages/:id/untrash` | supported | | `GET` | `/gmail/v1/users/:userId/messages/:messageId/attachments/:id` | supported | | `GET` | `/gmail/v1/users/:userId/threads` | partial | | `GET` | `/gmail/v1/users/:userId/threads/:id` | supported | | `POST` | `/gmail/v1/users/:userId/threads/:id/modify` | supported | | `POST` | `/gmail/v1/users/:userId/threads/:id/trash` | supported | | `POST` | `/gmail/v1/users/:userId/threads/:id/untrash` | supported | | `GET` | `/gmail/v1/users/:userId/labels` | supported | | `GET` | `/gmail/v1/users/:userId/labels/:id` | supported | | `POST` | `/gmail/v1/users/:userId/labels` | partial | | `PATCH` | `/gmail/v1/users/:userId/labels/:id` | supported | | `DELETE` | `/gmail/v1/users/:userId/labels/:id` | supported | | `GET` | `/gmail/v1/users/:userId/drafts` | supported | | `GET` | `/gmail/v1/users/:userId/drafts/:id` | supported | | `POST` | `/gmail/v1/users/:userId/drafts` | partial | | `PUT` | `/gmail/v1/users/:userId/drafts/:id` | partial | | `POST` | `/gmail/v1/users/:userId/drafts/send` | supported | | `DELETE` | `/gmail/v1/users/:userId/drafts/:id` | supported | | `GET` | `/gmail/v1/users/:userId/history` | partial | Anything else returns HTTP 501 with Google's error shape and `x-ancha-gap: 1`. ## Behavior - **Mail stays in the world.** A message sent to a seeded mailbox is delivered there. Mail to any other address is recorded in Sent and goes nowhere. An unauthorized `From` becomes the account's own address. - **Search.** `q` supports words, phrases, `from:`, `to:`, `cc:`, `bcc:`, `subject:`, `label:`, `in:`, `is:`, `has:`, `category:`, `filename:`, dates, sizes, `rfc822msgid:`, `OR`, `-` and grouping. Other operators are coverage gaps. A thread matches when any of its messages matches. - **Sending** takes `raw` only; the `/upload` media form is a gap. Sending a draft gives the message a new ID and removes the draft. Replacing a draft creates a new draft message, as Gmail does. - **Labels** take colors as `#rrggbb`; the replica does not check them against Gmail's palette. - **History** starts after seeding, with one record per message change. ## Starter data The `gmail-starter` dataset has two mailboxes at `acme.example`. The agent's token belongs to **Acme Support**, `support@acme.example`, with the labels Customers, Billing, Follow up and Sales, nineteen messages and one draft. | Ref | Message | | --- | --- | | `$sso_question`, `$sso_answer`, `$soc2` | Grace Lin at Bluefield Logistics asks about SSO with Okta and SCIM; Laura Kim answers; Grace asks for the SOC 2 Type II report (labels Customers and Follow up). | | `$double_count`, `$double_count_answer`, `$double_count_thanks` | Carla Diaz at Kestrel Credit Union reports a dashboard counting yesterday twice; Mike Turner points at the 4.12 fix; Carla confirms. | | `$pricing`, `$pricing_draft` | Lucy Grant at Harbor Foods asks about Team plan pricing (label Sales). An unsent draft reply is in Drafts. | | `$seats`, `$seats_answer`, `$seats_followup` | Nora Bell at Pinecrest Health adds 20 seats; Laura confirms proration; Nora asks for the order form (unread, Billing). | | `$csv_export` | Erin Walsh at Ridgeway Robotics cannot find last month's CSV export, 36 minutes before start. Unread, important. | | `$summit_training` | Megan Shaw at Summit Freight asks for the admin training recording. Unread. | | `$renewal_po` | Peter Novak at Kestrel Credit Union sends the renewal purchase order, with a CSV attachment. Unread. | | `$ridgeway_onboarding` | Mike Turner: Ridgeway Robotics onboards its warehouse team this week. | | `$datadog_recovered`, `$github_rate_limit` | Notifications: a recovered Datadog monitor and a GitHub pull request. | | `$ridgeway_autoreply`, `$conference`, `$domain_spam` | An out-of-office reply in the trash, a promotion, and a message in Spam. | The second mailbox, `laura@acme.example` (Laura Kim), holds one sent message about the Helix Manufacturing quarterly review. ## Checks Gmail has no state checks. Use trace checks, for example `{ called: "POST /gmail/v1/users/me/messages/send" }` or `{ not_called: "POST /gmail/v1/users/me/messages/*/trash" }`. ## Limits - Mail moves only inside the world: delivery to seeded mailboxes, Sent for everything else. - Uploads, batch requests, push notifications, filters, settings and delegation are not modeled. # Google Drive Source: https://docs.ancha.io/tools/google-drive A stateful replica of the Google Drive API v3: files and folders in one My Drive, uploads and exports, permissions, comments, revisions and changes. The Google Drive replica implements 26 endpoints of the Drive API v3 for one signed-in account's My Drive, with Docs, Sheets and Slides stored as text. Its key is `google-drive`. It was tested with `@googleapis/drive`. ## Connect | Variable | Value | | --- | --- | | `GOOGLE_DRIVE_API_URL` | Base URL. Paths start with `/drive/v3/` and `/upload/drive/v3/`. | | `GOOGLE_DRIVE_ACCESS_TOKEN` | Fake OAuth access token for the agent's account. Send `Authorization: Bearer `. | ```js Node import { auth, drive } from '@googleapis/drive'; const oauth = new auth.OAuth2(); oauth.setCredentials({ access_token: process.env.GOOGLE_DRIVE_ACCESS_TOKEN }); const files = drive({ version: 'v3', auth: oauth }); const options = { rootUrl: process.env.GOOGLE_DRIVE_API_URL + '/', retry: false }; const { data } = await files.files.list({ q: "trashed = false", fields: 'files(id, name, mimeType)' }, options); ``` This is what `ancha connect google-drive --lang ts` prints. Google's client takes the replica as a per-request `rootUrl` ending in `/`; a global `rootUrl` keeps only the origin and drops the path. ## Endpoints | Method | Path | Notes | | --- | --- | --- | | `GET` | `/drive/v3/about` | `fields` is required, as in Drive. | | `GET` | `/drive/v3/files` | `q` supports `name`, `fullText`, `mimeType`, `parents`, `owners`, `writers`, `readers`, times, `trashed`, `starred`, `sharedWithMe` and `visibility`. `properties has` is a gap. | | `POST` | `/drive/v3/files` | Metadata only: folders and empty files. | | `POST` | `/upload/drive/v3/files` | `uploadType=multipart` or `media`. Text converts to Docs, Sheets and Slides. Resumable uploads are a gap. | | `DELETE` | `/drive/v3/files/trash` | Empties the trash. | | `GET` | `/drive/v3/files/{fileId}` | `alt=media` downloads binary content. `root` is the account's My Drive. | | `PATCH` | `/drive/v3/files/{fileId}` | Rename, describe, star, trash (owner only), properties, and moves with one `addParents` and `removeParents`. | | `PATCH` | `/upload/drive/v3/files/{fileId}` | Replaces content and keeps a revision. | | `DELETE` | `/drive/v3/files/{fileId}` | Owner only; skips the trash. | | `POST` | `/drive/v3/files/{fileId}/copy` | | | `GET` | `/drive/v3/files/{fileId}/export` | Docs to text, Markdown or HTML; Sheets to CSV or TSV; Slides to text. PDF and Office formats are gaps. | | `GET`, `POST` | `/drive/v3/files/{fileId}/permissions` | Lists include the owner and inherited grants. Grants to users, groups, a domain and anyone. Notification emails are recorded, not sent; ownership transfer and expirations are gaps. | | `GET`, `PATCH`, `DELETE` | `/drive/v3/files/{fileId}/permissions/{permissionId}` | Role changes on the file's own grants. | | `GET`, `POST` | `/drive/v3/files/{fileId}/comments` | `fields` is required. Comments are unanchored, optionally quoting text. | | `GET`, `PATCH`, `DELETE` | `/drive/v3/files/{fileId}/comments/{commentId}` | | | `GET`, `POST` | `/drive/v3/files/{fileId}/comments/{commentId}/replies` | A reply with `action` `resolve` or `reopen` changes the comment's state. | | `GET` | `/drive/v3/files/{fileId}/revisions` | Writers and owners only. | | `GET` | `/drive/v3/changes/startPageToken` | | | `GET` | `/drive/v3/changes` | Changes the account can see, one entry per file per page. Push notifications are a gap. | All 26 endpoints are partial. Anything else returns HTTP 501 with Google's error shape and `x-ancha-gap: 1`. ## Starter data The `google-drive-starter` dataset is the My Drive of `ancha@acme.example` in the domain `acme.example`, with eleven users and thirty files. The agent sees what is shared with it: the company folder **Acme**, which everyone in the domain can edit, and a few files shared directly. | Folder | Contents | | --- | --- | | `Acme/Product` (Ellen Price) | "Scheduled reports spec", a Doc with three comments from Laura and Greg; "Roadmap review: Thursday topics"; the Sheet "Customer feedback: October". | | `Acme/Marketing` (Tom Hughes) | "Ridgeway Robotics case study (draft)" with a resolved comment; "Acme product overview.pdf", shared with anyone who has the link. | | `Acme/Customers/Ridgeway Robotics` (Laura Kim) | "Ridgeway Robotics pilot plan" and `ridgeway-warehouse-tables.csv`. Erin Walsh at Ridgeway can comment on this folder. | | `Acme/Customers/Pinecrest Health` | "Pinecrest Health order form: 20 additional seats" (a draft) and the Sheet "Pinecrest Health onboarding checklist". | | `Acme/Customers/Bluefield Logistics` | "Bluefield Logistics security questionnaire" with a comment from Greg, and "Acme SOC 2 Type II report 2026.pdf", described as "Share only under NDA, as view-only". | | `Acme/Engineering` (Ben Carter) | "Orders API runbook" and "Postmortem: Orders API latency after 4.12.1", a draft with a comment. | | `Finance` (Anna Fischer) | The Sheets "Purchase orders 2026" and "October expense reports". Mike can edit; Ben and the agent can read. | | `Leadership` (Mike Turner) | "Board deck draft: Q4", a presentation. Shared with Ben only; the agent cannot see it. | | My Drive root | "Weekly support digest: Oct 24", owned by the agent and shared with the domain, plus a trashed copy. Ben's private "Ben notes" is not shared. | `ancha sandbox configuration --tools google-drive --json` prints every file with its ID, owner, permissions and content. ## Checks Google Drive has no state checks. Use trace checks, for example `{ called: "POST /drive/v3/files/*/permissions" }` or `{ not_called: "DELETE /drive/v3/files/*" }`. ## Limits - My Drive for one signed-in account: no shared drives, push notifications or resumable uploads. - Exports cover text, Markdown, HTML, CSV and TSV; PDF and Office exports are coverage gaps. - Sharing notifications are recorded in state, never emailed. # Microsoft Word Source: https://docs.ancha.io/tools/microsoft-word A stateful replica of Microsoft Graph drive items for Word documents in one OneDrive: folders, .docx files built from paragraphs, versions, sharing links and invitations. The Microsoft Word replica implements 21 endpoints of Microsoft Graph v1.0 for one OneDrive. Word files are real `.docx` packages built from paragraphs, with PDF conversion on download. Its key is `microsoft-word`. It was tested with `@microsoft/microsoft-graph-client`. ## Connect | Variable | Value | | --- | --- | | `MICROSOFT_GRAPH_API_URL` | Base URL, standing in for `https://graph.microsoft.com`. Paths start with `/v1.0/`. | | `MICROSOFT_GRAPH_ACCESS_TOKEN` | Fake access token for the signed-in user. Send `Authorization: Bearer `. | ```js Node import { Client } from '@microsoft/microsoft-graph-client'; const token = process.env.MICROSOFT_GRAPH_ACCESS_TOKEN!; const graph = Client.init({ baseUrl: process.env.MICROSOFT_GRAPH_API_URL + '/', defaultVersion: 'v1.0', authProvider: (done) => done(null, token), fetchOptions: { headers: { authorization: 'Bearer ' + token } } }); const files = await graph.api('/me/drive/root/children').get(); // Next page: graph.api(files['@odata.nextLink'].slice(files['@odata.nextLink'].indexOf('/v1.0') + 5)).get() ``` This is what `ancha connect microsoft-word --lang ts` prints. The Graph client signs only Microsoft's HTTPS hosts and drops a capitalized `Authorization` header elsewhere, so the token also goes in `fetchOptions` as a lower-case header. Its `api()` takes paths, so follow an `@odata.nextLink` with the part after `/v1.0`. ## Endpoints All 21 endpoints are partial. Anything else returns HTTP 501 with Graph's error shape and `x-ancha-gap: 1`. | Method | Path | Notes | | --- | --- | --- | | `GET` | `/v1.0/me` | The signed-in user. | | `GET` | `/v1.0/me/drive` | Quota and owner. | | `GET` | `/v1.0/me/drive/recent` | | | `GET` | `/v1.0/me/drive/items/{item-id}` | Also by path, `/me/drive/root:/{path}:`, and under `/drives/{drive-id}/`. `$select` and `$expand=children`. | | `GET` | `/v1.0/me/drive/items/{item-id}/children` | `$top`, `$skiptoken` and `$orderby` (`name`, `lastModifiedDateTime`, `size`). | | `POST` | `/v1.0/me/drive/items/{item-id}/children` | Folders, with `@microsoft.graph.conflictBehavior`. | | `PATCH` | `/v1.0/me/drive/items/{item-id}` | Rename, move (`parentReference`) and description. `If-Match`. | | `DELETE` | `/v1.0/me/drive/items/{item-id}` | `If-Match`. No recycle bin. | | `GET` | `/v1.0/me/drive/items/{item-id}/content` | 302 to a pre-authenticated URL. `format=pdf` converts Word documents. | | `PUT` | `/v1.0/me/drive/items/{parent-id}:/{filename}:/content` | Simple upload up to 250 MB, also by path (folders are created). A new version on replace. Upload sessions are a gap. | | `POST` | `/v1.0/me/drive/items/{item-id}/copy` | 202 with a monitor URL that reports completion at once. | | `GET` | `/v1.0/me/drive/root/search(q='{text}')` | Names and the text of Word and text files. | | `POST` | `/v1.0/me/drive/items/{item-id}/createLink` | View and edit links, anonymous or organization scope. An existing matching link is returned. | | `POST` | `/v1.0/me/drive/items/{item-id}/invite` | Recorded, never emailed. | | `GET` | `/v1.0/me/drive/items/{item-id}/permissions` | Owner, own and inherited permissions. | | `DELETE` | `/v1.0/me/drive/items/{item-id}/permissions/{perm-id}` | | | `GET` | `/v1.0/me/drive/items/{item-id}/versions` | | | `GET` | `/v1.0/me/drive/items/{item-id}/versions/{version-id}/content` | 302 to a pre-authenticated URL. | | `POST` | `/v1.0/me/drive/items/{item-id}/versions/{version-id}/restoreVersion` | | | `GET` | `/download/{token}`, `/monitor/{token}` | The pre-authenticated download URL and the copy monitor URL. | ## Starter data The `microsoft-word-starter` dataset is the OneDrive of `ancha@acme.example` in the tenant **Acme**. The users are the automation account, Anna Fischer, Tom Hughes, Mike Turner and the external "Pinecrest Health Legal". | Folder | Documents | | --- | --- | | `/Contracts` | "Bluefield Logistics - Mutual NDA" (a draft that has not been sent for signature), "Pinecrest Health - MSA (signed)", "Pinecrest Health - Order form - 20 additional seats" (out for review, shared with Pinecrest Health Legal, with an earlier version), "Kestrel Credit Union - Renewal order form 2027" (awaiting countersignature) and "Ridgeway Robotics - Pilot agreement" (signed). | | `/Policies` | "Information security policy" (with an organization-wide view link) and "Data processing addendum v2". | | `/Templates` | "Mutual NDA template" and "Order form template". | Every document is a `.docx` whose paragraphs `ancha sandbox configuration --tools microsoft-word --json` prints. ## Checks Microsoft Word has no state checks. Use trace checks, for example `{ called: "POST /v1.0/me/drive/items/*/invite" }` or `{ not_called: "DELETE /v1.0/me/drive/items/*" }`. ## Limits - One OneDrive. Word files are real `.docx` packages built from paragraphs, with PDF conversion. No Word document object model, comments or co-authoring. - The Graph client signs only HTTPS Graph hosts: set `baseUrl` to `MICROSOFT_GRAPH_API_URL` plus `/` and pass the token as a lower-case `authorization` header in `fetchOptions.headers`. Follow `@odata.nextLink` with the part after `/v1.0`. - Invitations and sharing are recorded, never emailed; no upload sessions, delta queries or shared-with-me. # Notion Source: https://docs.ancha.io/tools/notion A stateful replica of the Notion API: pages, blocks, databases and data sources, comments, users and search, as an internal integration sees them. The Notion replica implements 19 endpoints of the Notion API, version `2025-09-03`, for an internal integration: it sees only the pages shared with it, as Notion does. Its key is `notion`. It was tested with `@notionhq/client`. ## Connect | Variable | Value | | --- | --- | | `NOTION_API_URL` | Base URL. Paths start with `/v1/`. | | `NOTION_TOKEN` | Fake internal integration token. Send `Authorization: Bearer `. | ```js Node import { Client } from '@notionhq/client'; const notion = new Client({ auth: process.env.NOTION_TOKEN, baseUrl: process.env.NOTION_API_URL }); const { results } = await notion.search({ query: 'runbook' }); ``` This is what `ancha connect notion --lang ts` prints. Send `Notion-Version: 2025-09-03`, as the current client does; requests with an older version are coverage gaps, because databases are served as data sources. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `GET` | `/v1/users/me`, `/v1/users`, `/v1/users/:user_id` | supported | | `POST` | `/v1/search` | partial: matches titles; filter by object (`page` or `data_source`); sorted by `last_edited_time` | | `POST` | `/v1/pages` | partial: parents `page_id`, `data_source_id` or `database_id`; emoji icons; covers, templates and markdown content are gaps | | `GET` | `/v1/pages/:page_id` | supported | | `PATCH` | `/v1/pages/:page_id` | partial: properties, trash and emoji icons; locking, covers and templates are gaps | | `GET` | `/v1/pages/:page_id/properties/:property_id` | supported | | `GET` | `/v1/blocks/:block_id` | supported | | `PATCH` | `/v1/blocks/:block_id` | partial: text, checked state, language and trash for the supported block types | | `DELETE` | `/v1/blocks/:block_id` | supported: moves the block, or child page, to the trash | | `GET` | `/v1/blocks/:block_id/children` | supported: child pages and databases appear as `child_page` and `child_database` blocks | | `PATCH` | `/v1/blocks/:block_id/children` | partial: appends at the end or after a sibling; paragraph, headings, lists, to-dos, toggles, quotes, callouts, code, dividers and bookmarks; other block types are gaps | | `GET` | `/v1/databases/:database_id` | supported | | `GET` | `/v1/data_sources/:data_source_id` | supported | | `POST` | `/v1/data_sources/:data_source_id/query` | partial: property filters for text, number, checkbox, select, status, multi-select, people and date, timestamp filters, `and`/`or`, sorts | | `POST` | `/v1/comments` | partial: comments on pages and replies to discussions; attachments and markdown are gaps | | `GET` | `/v1/comments`, `/v1/comments/:comment_id` | supported | Anything else returns HTTP 501 with Notion's error shape and `x-ancha-gap: 1`. ## Starter data The `notion-starter` dataset is the workspace **Acme**, with the integration **Ancha** and eight users: Ben Carter, Ellen Price, Greg Wilson, Rachel Moore, Laura Kim, Mike Turner, Tom Hughes and Victor Lane. | Page | Contents | | --- | --- | | Engineering | A top-level page shared with the integration, with two children: "Orders API runbook" (headings, a numbered list, a callout about rolling back 4.12.1 and a bookmark) and "On-call handbook". | | Product | A top-level page with "Scheduled reports spec" (requirements as to-dos, open questions, and two comments) and "Roadmap review: Thursday", plus two databases. | | Leadership | Not shared with the integration. Its child "Board deck draft" is shared. | | Database | Properties | Rows | | --- | --- | --- | | Customer feedback | Feedback (title), Customer (select), Theme (multi-select), Status, Votes (number), Owner (people), Received (date), Link (URL) | Six requests: weekly scheduled reports (Pinecrest Health, in progress, 12 votes), SCIM provisioning with Okta (Bluefield Logistics), exports that take minutes to appear (Ridgeway Robotics), daily totals counted twice (Kestrel Credit Union, done), dark mode (Summit Freight) and a usage report for a renewal review (Helix Manufacturing). | | Meeting notes | Name (title), Date, Type (select), Attendees (people) | Three: the Ridgeway Robotics pilot kickoff, the Helix Manufacturing quarterly review prep, and the 4.12.1 rollback retro with two open to-dos. | `ancha sandbox configuration --tools notion --json` prints every block and row. ## Checks Notion has no state checks. Use trace checks, for example `{ called: "POST /v1/pages" }` or `{ not_called: "DELETE /v1/blocks/*" }`. ## Limits - An internal integration: it sees only pages shared with it, as Notion does. - Text, heading, list, to-do, toggle, quote, callout, code, divider and bookmark blocks; images, files, tables, synced blocks, relations, rollups, formulas and markdown endpoints are gaps. - Requests with a `Notion-Version` before `2025-09-03` are coverage gaps: databases are served as data sources. # Dropbox Source: https://docs.ancha.io/tools/dropbox A stateful replica of the Dropbox API v2 for one account: files and folders, uploads and downloads, search, revisions, shared links and shared folders. The Dropbox replica implements 24 endpoints of the Dropbox API v2 for one account: files and folders, uploads up to 150 MB, downloads, search, revisions, shared links and shared folders. Its key is `dropbox`. It was tested with the `dropbox` SDK. ## Connect | Variable | Value | | --- | --- | | `DROPBOX_API_URL` | Base URL, standing in for both `https://api.dropboxapi.com` and `https://content.dropboxapi.com`. Paths start with `/2/`. | | `DROPBOX_ACCESS_TOKEN` | Fake access token. Send `Authorization: Bearer `. | ```js Node import { Dropbox } from 'dropbox'; const dbx = new Dropbox({ accessToken: process.env.DROPBOX_ACCESS_TOKEN, fetch: (url: string, init?: RequestInit) => fetch(url.replace(/^https:\/\/(api|content)\.dropboxapi\.com/, process.env.DROPBOX_API_URL!), init) }); const { result } = await dbx.filesListFolder({ path: '' }); ``` This is what `ancha connect dropbox --lang ts` prints. The SDK has no base-URL option, so give it a `fetch` that sends both Dropbox hosts to the replica. ## Endpoints All 24 endpoints are partial. Anything else returns HTTP 501 with Dropbox's error shape and `x-ancha-gap: 1`. | Method | Path | Notes | | --- | --- | --- | | `POST` | `/2/files/list_folder` | `recursive`, `include_deleted` and `limit`. Shared links and property groups are gaps. | | `POST` | `/2/files/list_folder/continue` | Pages through a listing, then reports changes since it. | | `POST` | `/2/files/get_metadata`, `/2/files/create_folder_v2`, `/2/files/delete_v2`, `/2/files/copy_v2` | | | `POST` | `/2/files/move_v2` | Folders move with everything under them; missing folders are created. | | `POST` | `/2/files/upload` | `add`, `overwrite` and `update` modes, `autorename` and `content_hash`. Up to 150 MB; upload sessions are a gap. | | `POST` | `/2/files/download` | The current or an earlier `rev`. | | `POST` | `/2/files/search_v2` | File names and text contents; `path`, `max_results`, `file_status`, `filename_only` and `file_extensions`. | | `POST` | `/2/files/search/continue_v2`, `/2/files/list_revisions`, `/2/files/restore` | | | `POST` | `/2/files/get_temporary_link` | A download link valid for four hours of world time. | | `GET` | `/temporary/{token}` | The link `get_temporary_link` returns. | | `POST` | `/2/sharing/create_shared_link_with_settings` | One link per path: a second request answers `shared_link_already_exists` with the existing link. | | `POST` | `/2/sharing/list_shared_links`, `/2/sharing/modify_shared_link_settings`, `/2/sharing/revoke_shared_link` | | | `POST` | `/2/sharing/share_folder` | Completes immediately. | | `POST` | `/2/sharing/add_folder_member` | Recorded, never emailed; unknown addresses become invitees. | | `POST` | `/2/sharing/list_folder_members` | | | `POST` | `/2/users/get_current_account`, `/2/users/get_space_usage` | | ## Starter data The `dropbox-starter` dataset is the account of `ancha@acme.example` in the team **Acme**, with 25 entries. | Folder | Contents | | --- | --- | | `/Sales/Bluefield Logistics` | "Bluefield proposal v2.pdf" with an earlier revision, a deleted "Bluefield proposal v1.pdf", and a `Security review` folder with "Acme SOC 2 Type II report 2026.pdf" and the security questionnaire answers. | | `/Sales/Pinecrest Health` | The signed MSA and a draft order form for 20 additional seats. | | `/Sales/Kestrel Credit Union` | "Renewal quote 2027.pdf". | | `/Marketing` | "Acme product overview.pdf" with a public shared link, a `Case studies` folder with the Ridgeway Robotics draft, and a `Logos` folder. | | `/Customer uploads/Ridgeway Robotics` | A shared folder that Erin Walsh at Ridgeway Robotics and Laura Kim can edit, holding `warehouse-schema.csv` and `pick_lists_sample.csv` uploaded by Erin. | | `/Team` | "Holiday schedule 2026.txt". | The people the account can share with are Tom Hughes, Ellen Price, Anna Fischer and Laura Kim at Acme, and Erin Walsh outside it. ## Checks Dropbox has no state checks. Use trace checks, for example `{ called: "POST /2/sharing/create_shared_link_with_settings" }` or `{ not_called: "POST /2/files/delete_v2" }`. ## Limits - The SDK has no base-URL option: give it a fetch that sends `https://api.dropboxapi.com` and `https://content.dropboxapi.com` to `DROPBOX_API_URL`. - One account; uploads up to 150 MB with no upload sessions; no file requests, Paper or team-space administration. - Sharing records links and members; nothing is emailed. # Resend Source: https://docs.ancha.io/tools/resend A stateful replica of Resend's REST API: transactional emails and batches, broadcasts, domains, contacts and segments, where mail is recorded rather than delivered. The Resend replica implements 40 endpoints of Resend's REST API. Mail is recorded, not delivered: each email gets the outcome its recipient implies, two simulated seconds after sending. Its key is `resend`. It was tested with the `resend` SDK. ## Connect | Variable | Value | | --- | --- | | `RESEND_BASE_URL` | Base URL. The SDK reads it by itself. | | `RESEND_API_KEY` | Fake API key with full access. Send `Authorization: Bearer `. | ```js Node import { Resend } from 'resend'; // The SDK also reads RESEND_BASE_URL by itself. const resend = new Resend(process.env.RESEND_API_KEY, { baseUrl: process.env.RESEND_BASE_URL }); const { data, error } = await resend.emails.list(); ``` This is what `ancha connect resend --lang ts` prints. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `POST` | `/emails` | partial: from a verified domain or `resend.dev`; `bounced@resend.dev` and `complained@resend.dev` give those outcomes; templates, topics and URL attachments are gaps | | `POST` | `/emails/batch` | partial: up to 100 emails, all or none; permissive mode is a gap | | `GET` | `/emails`, `/emails/:id` | supported | | `PATCH` | `/emails/:id` | supported: reschedules a scheduled email | | `POST` | `/emails/:id/cancel` | supported | | `POST` | `/domains` | partial: custom return paths, click and open tracking and TLS settings are gaps | | `GET` | `/domains`, `/domains/:id` | supported | | `POST` | `/domains/:id/verify` | partial: verification completes two simulated minutes after it is asked for | | `DELETE` | `/domains/:id` | supported | | `POST` | `/contacts` | partial: contact properties and topics are gaps | | `GET` | `/contacts` | supported | | `GET`, `PATCH`, `DELETE` | `/contacts/:id` | supported, by ID or email | | `GET` | `/contacts/:id/segments` | supported | | `POST`, `DELETE` | `/contacts/:id/segments/:segment_id` | supported | | `POST`, `GET` | `/segments` | supported | | `GET`, `PATCH`, `DELETE` | `/segments/:id` | supported | | `GET` | `/segments/:id/contacts` | supported | | `POST` | `/broadcasts` | partial: a draft, or sent now or at `scheduled_at` to the segment's subscribed contacts as they are when the send is asked for; topics are a gap | | `GET` | `/broadcasts`, `/broadcasts/:id` | supported | | `PATCH` | `/broadcasts/:id` | partial: drafts only | | `POST` | `/broadcasts/:id/send` | supported: now or at `scheduled_at` | | `POST` | `/broadcasts/:id/cancel` | supported: a scheduled broadcast goes back to draft | | `DELETE` | `/broadcasts/:id` | supported: drafts and scheduled broadcasts | | `POST`, `GET` | `/audiences` | supported: deprecated aliases of `/segments` | | `GET`, `DELETE` | `/audiences/:id` | supported: deprecated aliases | | `POST`, `GET` | `/audiences/:id/contacts` | supported: deprecated, the segment's contacts | | `GET`, `PATCH` | `/audiences/:id/contacts/:contact` | supported: deprecated, by ID or email | Anything else returns HTTP 501 with Resend's error shape and `x-ancha-gap: 1`. ## Starter data The `resend-starter` dataset is Acme's account with an API key named "Support agent". | Record | Detail | | --- | --- | | Domains | `acme.example`, verified. `mail.acme.example`, added three hours ago and unverified. | | Segments | "Customers" and "Product updates". | | Contacts | Erin Walsh, Nora Bell, Grace Lin, Carla Diaz, Megan Shaw and Lucy Grant (unsubscribed), each in one or both segments. | | Emails | Five sent: an overdue-invoice reminder to Helix Manufacturing that bounced, a welcome to Nora Bell (opened), "Your CSV export is ready" to Erin Walsh (clicked), a training recording to Megan Shaw (delivered), and a scheduled reports beta invite to three customers (delivered). | | Broadcasts | "October product update", sent to Product updates ten days ago, and "November product update", a draft about the scheduled reports beta. | ## Checks Resend has no state checks. Use trace checks, for example `{ called: "POST /emails" }` or `{ not_called: "POST /broadcasts/*/send" }`. ## Limits - Mail is recorded, not delivered: `bounced@` and `complained@resend.dev` give those outcomes, other recipients are delivered two simulated seconds after sending. - Broadcasts go to the segment's subscribed contacts as they are when the send is requested. - Domain verification completes two simulated minutes after it is requested; templates, webhooks, topics and contact properties are gaps. # GitHub Source: https://docs.ancha.io/tools/github A stateful replica of GitHub's REST API: repositories, commits and Git data, file contents, pull requests and reviews, issues and labels, check runs, Actions runs and deployments. The GitHub replica implements 71 REST endpoints over a real commit graph: branches, commits, trees and file contents change as your agent pushes, opens pull requests and merges. Its key is `github`. It was tested with `@octokit/rest`. ## Connect | Variable | Value | | --- | --- | | `GITHUB_API_URL` | Base URL of the REST API. | | `GITHUB_TOKEN` | Fake token. Send it in the `Authorization` header with the `Bearer` or `token` scheme. | | `GITHUB_WEBHOOK_SECRET` | Signs webhook deliveries in `X-Hub-Signature-256`. Verify against the raw request body. | ```js Node import { Octokit } from '@octokit/rest'; const github = new Octokit({ auth: process.env.GITHUB_TOKEN, baseUrl: process.env.GITHUB_API_URL }); await github.repos.get({ owner: 'acme', repo: 'starter' }); ``` ```python Python import os from github import Github, Auth github = Github(base_url=os.environ['GITHUB_API_URL'].rstrip('/'), auth=Auth.Token(os.environ['GITHUB_TOKEN'])) github.get_repo('acme/starter') ``` These are the snippets `ancha connect github` prints. `acme/starter` is the repository in the starter scene that `ancha init` writes. Worlds use `acme/orders`. ## Endpoints All 71 endpoints are partial: they implement the common request shapes, and `ancha twins coverage github` lists what each one leaves out. Any other route returns HTTP 501 with GitHub's error shape and `x-ancha-gap: 1`. ### Account and repositories | Method | Path | | --- | --- | | `GET` | `/user` | | `GET` | `/user/repos` | | `GET` | `/orgs/:org/repos` | | `GET` | `/repos/:owner/:repo` | | `GET` | `/repos/:owner/:repo/branches` | | `GET` | `/repos/:owner/:repo/branches/:branch` | ### Commits and Git data | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/commits` | | `GET` | `/repos/:owner/:repo/commits/:ref` | | `GET` | `/repos/:owner/:repo/compare/:base...:head` | | `GET` | `/repos/:owner/:repo/git/blobs/:sha` | | `POST` | `/repos/:owner/:repo/git/blobs` | | `GET` | `/repos/:owner/:repo/git/trees/:sha` | | `POST` | `/repos/:owner/:repo/git/trees` | | `GET` | `/repos/:owner/:repo/git/commits/:sha` | | `POST` | `/repos/:owner/:repo/git/commits` | | `GET` | `/repos/:owner/:repo/git/ref/:ref` | | `GET` | `/repos/:owner/:repo/git/matching-refs/:ref?` | | `POST` | `/repos/:owner/:repo/git/refs` | | `PATCH` | `/repos/:owner/:repo/git/refs/:ref` | | `DELETE` | `/repos/:owner/:repo/git/refs/:ref` | Object SHAs are deterministic identities, not real Git content hashes. Commits are immutable, and new ones take their date from the simulated clock. ### File contents | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/contents/:path?` | | `PUT` | `/repos/:owner/:repo/contents/:path` | A file write creates a commit on an existing branch. Updates need the file's current blob SHA, as on GitHub. ### Pull requests and reviews | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/pulls/:number/reviews` | | `POST` | `/repos/:owner/:repo/pulls/:number/reviews` | | `GET` | `/repos/:owner/:repo/pulls/:number/reviews/:id` | | `GET` | `/repos/:owner/:repo/pulls/comments` | | `GET` | `/repos/:owner/:repo/pulls/comments/:id` | | `PATCH` | `/repos/:owner/:repo/pulls/comments/:id` | | `DELETE` | `/repos/:owner/:repo/pulls/comments/:id` | | `GET` | `/repos/:owner/:repo/pulls/:number/comments` | | `POST` | `/repos/:owner/:repo/pulls/:number/comments` | | `GET` | `/repos/:owner/:repo/pulls` | | `POST` | `/repos/:owner/:repo/pulls` | | `GET` | `/repos/:owner/:repo/pulls/:number` | | `PATCH` | `/repos/:owner/:repo/pulls/:number` | | `GET` | `/repos/:owner/:repo/pulls/:number/merge` | | `PUT` | `/repos/:owner/:repo/pulls/:number/merge` | | `GET` | `/repos/:owner/:repo/pulls/:number/files` | | `GET` | `/repos/:owner/:repo/pulls/:number/commits` | Pull requests are between branches of the same repository. A merge makes a two-parent merge commit when the base is an ancestor of the head. A diverged or stale head returns 409, and a closed or draft pull request returns 405. Squash, rebase and cross-repository pull requests are not modeled. ### Issues and comments | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/issues` | | `POST` | `/repos/:owner/:repo/issues` | | `GET` | `/repos/:owner/:repo/issues/comments` | | `GET` | `/repos/:owner/:repo/issues/comments/:id` | | `PATCH` | `/repos/:owner/:repo/issues/comments/:id` | | `DELETE` | `/repos/:owner/:repo/issues/comments/:id` | | `GET` | `/repos/:owner/:repo/issues/:number` | | `PATCH` | `/repos/:owner/:repo/issues/:number` | | `GET` | `/repos/:owner/:repo/issues/:number/comments` | | `POST` | `/repos/:owner/:repo/issues/:number/comments` | Issues and pull requests share one numbering sequence. Labels apply on create and update, and the `labels` filter matches issues that carry every listed label, ignoring case. Assignees and milestones are not modeled. ### Labels | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/labels` | | `POST` | `/repos/:owner/:repo/labels` | | `GET` | `/repos/:owner/:repo/labels/:name` | | `PATCH` | `/repos/:owner/:repo/labels/:name` | | `DELETE` | `/repos/:owner/:repo/labels/:name` | | `GET` | `/repos/:owner/:repo/issues/:number/labels` | | `POST` | `/repos/:owner/:repo/issues/:number/labels` | | `PUT` | `/repos/:owner/:repo/issues/:number/labels` | | `DELETE` | `/repos/:owner/:repo/issues/:number/labels` | | `DELETE` | `/repos/:owner/:repo/issues/:number/labels/:name` | Repository labels have a name, color and description. Giving an issue a label the repository does not declare creates it, as GitHub does. Names match without regard to case. Label suggestions are a gap. ### Check runs and Actions | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/commits/:ref/check-runs` | | `GET` | `/repos/:owner/:repo/check-runs/:id` | | `GET` | `/repos/:owner/:repo/actions/runs` | | `GET` | `/repos/:owner/:repo/actions/workflows/:workflow/runs` | | `GET` | `/repos/:owner/:repo/actions/runs/:id` | | `POST` | `/repos/:owner/:repo/actions/runs/:id/rerun` | Check runs are read-only starter data. Actions runs exist only for the simulated pipelines a living world defines; no workflow actually executes. ### Deployments | Method | Path | | --- | --- | | `GET` | `/repos/:owner/:repo/deployments` | | `POST` | `/repos/:owner/:repo/deployments` | | `GET` | `/repos/:owner/:repo/deployments/:id` | | `GET` | `/repos/:owner/:repo/deployments/:id/statuses` | | `POST` | `/repos/:owner/:repo/deployments/:id/statuses` | | `GET` | `/repos/:owner/:repo/deployments/:id/statuses/:statusId` | A successful deployment status for the `production` environment moves production to that deployment's commit. ## Events Changes emit GitHub webhook events, signed with `GITHUB_WEBHOOK_SECRET`: pushes and branch updates emit `push`, pull request changes and merges emit `pull_request`, comments emit `issue_comment`, issues emit `issues` with the actions `opened`, `edited`, `closed`, `reopened`, `labeled` and `unlabeled`, and deployment statuses emit `deployment_status`. Agents receive them through the trial event feed or by HTTP. See [ancha.yaml](https://docs.ancha.io/reference/ancha-yaml). ## Starter data The `github-starter` dataset holds three private repositories of the organization `acme`. | Ref | Record | | --- | --- | | `$github_bot` | User `ancha` ("Ancha assistant"). The token authenticates as this user. | | `$github_ben`, `$github_greg`, `$github_rachel`, `$github_victor` | Users `ben` (Ben Carter), `greg` (Greg Wilson), `rachel` (Rachel Moore) and `victor` (Victor Lane). | | `$github_orders` | Repository `acme/orders`, default branch `main`: the Orders API. | | `$github_web` | Repository `acme/web`: the dashboard app. | | `$github_sync` | Repository `acme/warehouse-sync`: copies customer warehouse tables into Acme. | ### acme/orders | Ref | Record | | --- | --- | | `$github_pagination`, `$github_node24`, `$github_idempotency`, `$github_retries` | Four merged commits on `main`, from 50 hours to 8 hours before start: pagination for `GET /orders`, the Node 24 upgrade, idempotency keys for order creation, and webhook retries with backoff. | | `$github_healthy` | Commit "Pin worker concurrency to 32 after the rollback" on `main`, 2 hours before start, with `config/runtime.ts`, `README.md` and `docs/runbook.md`. Production points here. | | `$github_docs` | Commit "Document deployment checks" on branch `runbook-update`. Its "Documentation checks" check run succeeded. | | `$github_rate_limit` | Commit "Rate-limit order creation per API key" on branch `rate-limit-orders`, 40 minutes before start. Its CI check run succeeded. | | `$github_runbook_pr` | Draft pull request #12, "Clarify recovery checks in the Orders runbook", from `runbook-update` into `main`. | | `$github_rate_limit_pr` | Open pull request #11, "Rate-limit order creation per API key", from `rate-limit-orders`. | | `$github_pagination_pr`, `$github_node24_pr`, `$github_idempotency_pr`, `$github_retries_pr` | Merged pull requests #3, #5, #8 and #10 for the four merged commits. | | `$github_export_issue` | Open issue #13, "Explain delayed CSV exports", with one comment from Ben. | | `$github_cancel_issue`, `$github_states_issue` | Open issues #6, "Return 409 when an order is cancelled twice", and #9, "Document the order states". | | `$github_cents_issue`, `$github_page_issue`, `$github_timeout_issue`, `$github_rollup_issue` | Closed issues #1, #2, #4 and #7, each fixed by one of the merged pull requests. | ### acme/web and acme/warehouse-sync | Ref | Record | | --- | --- | | `$github_web_main` | Commit "Release 2026.10.29" on `main` of `acme/web`. Production points here. | | `$github_web_schedule`, `$github_web_schedule_pr` | Commit "Add the report schedule page" on branch `report-schedules`, and its open pull request #3. | | `$github_web_dark`, `$github_web_tooltips` | Open issue #1, "Dark mode for dashboards", and closed issue #2, "Chart tooltips overflow on mobile". | | `$github_sync_main` | Commit "Stream large tables in batches" on `main` of `acme/warehouse-sync`. Production points here. | | `$github_sync_retry`, `$github_sync_stall` | Open issue #1, "Retry Snowflake connections on 503", and closed issue #2, "Sync stalls on tables over 50M rows". | The starter scene that `ancha init` writes uses a different, smaller repository, `acme/starter`. ## Checks | Check | Example | | --- | --- | | `github.file_at` | `{ github.file_at: [$github_orders, main, docs/runbook.md], contains: "health check" }` | | `github.pull` | `{ github.pull: { pull: $github_runbook_pr, merged: true } }` | | `github.production` | `{ github.production: { repo: $github_orders, commit: $github_docs } }` | | `github.issue` | `{ github.issue: { repo: $github_orders, body_contains: [CSV, export], labels: [bug], state: open, count: 1 } }` | The second element of `github.file_at` is a branch name, a commit ref, `default` or `production`. `contains` and `equals` compare exact bytes and are case-sensitive. To check that the agent opened an issue, use a `github.issue` state check, or a trace check such as `{ called: "POST /repos/acme/orders/issues" }`. See [Checks reference](https://docs.ancha.io/reference/checks#github). ## Limits - No branch protection, permissions model, assignees, milestones or releases. - No real Actions execution, jobs, artifacts or logs. - Annotated tags, symlinks and submodules are not modeled. # Datadog Source: https://docs.ancha.io/tools/datadog A stateful replica of Datadog's API v1 and v2: metric queries, log search, monitors and mutes, downtimes, dashboards, events and incidents. The Datadog replica implements 26 endpoints across metrics, logs, monitors, downtimes, dashboards, events and incidents. Its key is `datadog`. It was tested with `@datadog/datadog-api-client`. ## Connect | Variable | Value | | --- | --- | | `DATADOG_API_URL` | Base URL of the API. | | `DD_API_KEY` | Fake API key, sent as `DD-API-KEY`. | | `DD_APP_KEY` | Fake application key, sent as `DD-APPLICATION-KEY`. | | `DATADOG_WEBHOOK_TOKEN` | Bearer token on webhook deliveries to your agent. | ```js Node import { client, v1 } from '@datadog/datadog-api-client'; const configuration = client.createConfiguration({ baseServer: new client.BaseServerConfiguration(process.env.DATADOG_API_URL!, {}), enableRetry: false, }); // The SDK reads DD_API_KEY and DD_APP_KEY. v2 incident operations require explicit unstableOperations flags. const dashboards = new v1.DashboardsApi(configuration); await dashboards.listDashboards(); ``` This is what `ancha connect datadog --lang ts` prints. Turn SDK retries off so a failing call does not hide behind retries. `ancha connect` has no Python snippet for Datadog. ## Endpoints Any other route returns HTTP 501 with `{"errors": ["ancha: not implemented: ..."]}` and `x-ancha-gap: 1`. `ancha twins coverage datadog` prints each endpoint's full notes. ### Metrics and logs | Method | Path | Support | | --- | --- | --- | | `GET` | `/api/v1/query` | partial | | `POST` | `/api/v2/logs/events/search` | partial | Metric queries support `avg`, `sum`, `min` and `max`, literal tag filters joined with AND, and grouping. Functions, arithmetic and boolean filters are coverage gaps. Both metrics and logs answer windows of at most 24 hours. Log search matches literal service, status and tag filters and quoted message substrings. ### Monitors | Method | Path | Support | | --- | --- | --- | | `POST` | `/api/v1/monitor/:id/mute` | partial | | `POST` | `/api/v1/monitor/:id/unmute` | partial | | `GET` | `/api/v1/monitor` | partial | | `GET` | `/api/v1/monitor/:id` | partial | Monitor states are `OK` and `Alert`. Mutes are global: they suppress notifications, not evaluation. In a living world, monitors evaluate generated metrics every 15 seconds of simulated time. ### Downtimes | Method | Path | Support | | --- | --- | --- | | `POST` | `/api/v1/downtime` | partial | | `GET` | `/api/v1/downtime` | partial | | `GET` | `/api/v1/downtime/:id` | partial | | `PUT` | `/api/v1/downtime/:id` | partial | | `DELETE` | `/api/v1/downtime/:id` | partial | | `POST` | `/api/v2/downtime` | partial | | `GET` | `/api/v2/downtime` | partial | | `GET` | `/api/v2/downtime/:id` | partial | | `PATCH` | `/api/v2/downtime/:id` | partial | | `DELETE` | `/api/v2/downtime/:id` | partial | | `GET` | `/api/v1/monitor/:monitor_id/downtimes` | partial | | `GET` | `/api/v2/monitor/:monitor_id/downtime_matches` | partial | Downtimes are one-time windows for one monitor or all monitors. Recurrence and scoped or tag-targeted downtimes are coverage gaps. v1 and v2 downtimes use their own ID formats, which are not interchangeable. ### Dashboards | Method | Path | Support | | --- | --- | --- | | `GET` | `/api/v1/dashboard` | partial | | `GET` | `/api/v1/dashboard/:id` | supported | Dashboards are read-only. Their widgets are stored, never executed. ### Events | Method | Path | Support | | --- | --- | --- | | `GET` | `/api/v1/events` | partial | | `POST` | `/api/v1/events` | partial | ### Incidents | Method | Path | Support | | --- | --- | --- | | `GET` | `/api/v2/incidents` | partial | | `POST` | `/api/v2/incidents` | partial | | `GET` | `/api/v2/incidents/:id` | partial | | `PATCH` | `/api/v2/incidents/:id` | partial | Incidents are JSON:API resources with `state` (`active`, `stable`, `resolved`) and `severity` (`SEV-1` to `SEV-5` or `UNKNOWN`). They do not affect faults or PagerDuty. ## Starter data The `datadog-starter` dataset describes Acme's production services two hours after an Orders API deploy was rolled back, with a warehouse sync that is falling behind. | Ref | Record | | --- | --- | | `$datadog_orders` | Monitor "Orders request failures": `avg(last_5m):avg:orders.errors{service:orders} > 1`, state `OK`. Its message notifies `@pagerduty-orders`. | | `$datadog_checkout` | Monitor "Checkout p95 latency", `OK`. | | `$datadog_payments` | Monitor "Payment webhook failures", `OK`. | | `$datadog_sync_lag` | Monitor "Warehouse sync lag": `avg(last_15m):avg:warehouse.sync.lag_seconds{service:warehouse-sync} > 900`, state `Alert`. | | `$datadog_web_5xx` | Monitor "Web 5xx responses", `OK`. | | `$datadog_db_disk` | Monitor "db-primary disk usage", `OK`. | | `$datadog_queue` | Monitor "Fulfillment queue depth", `OK`. | | `$datadog_dashboard` | Dashboard "Orders API health". | | `$datadog_checkout_board`, `$datadog_sync_board` | Dashboards "Checkout" and "Warehouse sync". | | `$datadog_recovery` | Event "Orders recovery verified", 10 minutes before start. | Four more events tell the deploy story: orders 4.12.1 deployed 3 hours 10 minutes before start, rolled back to 4.12.0 twenty minutes later, web 2026.10.29 deployed 47 minutes before start, and a Snowflake maintenance window 42 minutes before start. Eight metric series carry the last hour: `orders.errors` and `orders.latency` (a spike 15 minutes before start, 12 errors and 780 ms, then normal), `checkout.latency.p95`, `payments.webhook.errors`, `warehouse.sync.lag_seconds` (climbing from 420 to 1,230 seconds), `web.requests.5xx`, `system.disk.used_pct` for `db-primary` (around 72%) and `fulfillment.queue.depth`. Seventeen log lines from the services `orders`, `warehouse-sync`, `checkout`, `payments`, `web` and `fulfillment` match them, for example the Snowflake 503 retries and "Sync for ridgeway-robotics is 17 minutes behind". All are tagged `env:production`. ## Checks | Check | Example | | --- | --- | | `datadog.monitor` | `{ datadog.monitor: $datadog_orders, is: OK }` | | `datadog.downtime` | `{ datadog.downtime: { monitor: $datadog_orders, remaining_gte: 1m, duration_lte: 10m } }` | `datadog.downtime` passes when an active, uncanceled downtime targets that exact monitor, has at least `remaining_gte` left and lasts no longer than `duration_lte` in total. Add `reject_active_unsafe: true` to also fail when any active all-monitor or overlong downtime exists. See [Checks reference](https://docs.ancha.io/reference/checks#datadog). ## Limits - A bounded metric query grammar and log search syntax. - One-time global downtimes only; no recurrence or scopes. - No SLOs, notebooks, synthetics or APM. # PagerDuty Source: https://docs.ancha.io/tools/pagerduty A stateful replica of PagerDuty's REST API v2 and Events API v2: users, services, escalation policies, schedules, on-call, incidents, notes and alert events. The PagerDuty replica implements 17 endpoints of REST API v2 and the Events API v2 `enqueue` endpoint. Incidents move through triggered, acknowledged and resolved, and escalation policies assign them over simulated time. Its key is `pagerduty`. It was tested with `@pagerduty/pdjs`. ## Connect | Variable | Value | | --- | --- | | `PAGERDUTY_API_URL` | Base URL of the REST API and of `/v2/enqueue`. | | `PAGERDUTY_API_TOKEN` | Fake REST token. Send `Authorization: Token token=`. | | `PAGERDUTY_FROM_EMAIL` | The email to send in the `From` header on writes, such as acknowledging an incident. | | `PAGERDUTY_ROUTING_KEY` | Events API routing key of the default service. | | `PAGERDUTY_ROUTING_KEYS` | JSON object of routing keys by service ref, for example `{"pagerduty_orders": "..."}`. | | `PAGERDUTY_SIGNING_SECRET` | Signs webhook deliveries to your agent. | ```js Node import { api } from '@pagerduty/pdjs'; const pd = api({ token: process.env.PAGERDUTY_API_TOKEN! }); const base = process.env.PAGERDUTY_API_URL!; const result = await pd.get('/services', { url: `${base}/services` }); // Use result.data and result.next(); every REST call requires the complete local url. // Attributed writes require headers: { From: process.env.PAGERDUTY_FROM_EMAIL! }. // Native Events v2: fetch(`${base}/v2/enqueue`, ...) with PAGERDUTY_ROUTING_KEY. // PDJS server, resource/all and Events convenience helpers assume production HTTPS. ``` This is what `ancha connect pagerduty --lang ts` prints. Pass the full local URL on every PDJS call; its `server` option, `all` helpers and Events helpers assume production HTTPS. Send events with `fetch` instead. `ancha connect` has no Python snippet for PagerDuty. ## Endpoints All endpoints are partial. Any other route returns HTTP 501 with PagerDuty's error shape and `x-ancha-gap: 1`. ### Users and services | Method | Path | | --- | --- | | `GET` | `/users` | | `GET` | `/users/:id` | | `GET` | `/services` | | `GET` | `/services/:id` | ### Escalation, schedules and on-call | Method | Path | | --- | --- | | `GET` | `/escalation_policies` | | `GET` | `/escalation_policies/:id` | | `GET` | `/schedules` | | `GET` | `/schedules/:id` | | `GET` | `/schedules/:id/users` | | `GET` | `/oncalls` | Schedules hold explicit on-call intervals. Rotations, layers, restrictions and overrides are not modeled. ### Incidents | Method | Path | | --- | --- | | `GET` | `/incidents` | | `GET` | `/incidents/:id` | | `PUT` | `/incidents/:id` | | `GET` | `/incidents/:id/notes` | | `POST` | `/incidents/:id/notes` | | `GET` | `/incidents/:id/log_entries` | `PUT /incidents/:id` acknowledges, resolves, reassigns to a user or hands the incident to an escalation policy or level. It needs the `From` header. Reopening, urgency changes, snoozing and bulk updates are coverage gaps. Incident lists cover the previous 30 days by default. ### Events | Method | Path | | --- | --- | | `POST` | `/v2/enqueue` | `POST /v2/enqueue` takes Events API v2 `trigger`, `acknowledge` and `resolve` payloads with a service's routing key. Each incident holds one alert, and open events with the same dedup key and routing key update the same incident. ## Starter data The `pagerduty-starter` dataset is Acme's on-call setup: four services, two escalation policies and five resolved incidents. | Ref | Record | | --- | --- | | `$pagerduty_bot` | User "Ancha assistant", `ancha@acme.example`. Writes are attributed to this user. | | `$pagerduty_rachel`, `$pagerduty_greg`, `$pagerduty_victor` | Users Rachel Moore, Greg Wilson and Victor Lane, at `acme.example`. | | `$pagerduty_policy` | Escalation policy "Orders on-call": the bot first, then Rachel after 5 minutes. | | `$pagerduty_platform` | Escalation policy "Platform on-call": Greg first, then Victor after 10 minutes. | | `$pagerduty_orders` | Service "Orders API", using "Orders on-call". The default service for `PAGERDUTY_ROUTING_KEY`. | | `$pagerduty_checkout`, `$pagerduty_payments`, `$pagerduty_sync` | Services "Checkout", "Payments" and "Warehouse sync", using "Platform on-call". | | `$pagerduty_previous` | Incident "Orders API slow requests", resolved, created 20 minutes before start, assigned to Rachel. | | `$pagerduty_rollback` | Incident "Orders API latency after the 4.12.1 deploy", resolved, 3 hours before start, assigned to Rachel. | | `$pagerduty_checkout_slow`, `$pagerduty_webhooks`, `$pagerduty_sync_stall` | Resolved incidents on Checkout (50 hours before start, Greg), Payments (30 hours, Greg) and Warehouse sync (20 hours, Victor). | ## Checks ```yaml - id: incident_resolved state: pagerduty.incident: { service: $pagerduty_orders, first: true } status: resolved assigned_to: [$pagerduty_rachel] resolved_within: 5m ``` `pagerduty.incident` selects an incident by ref (`{ ref: $pagerduty_previous }`) or the first one created for a service, including incidents created during the attempt. It needs at least one of `status`, `assigned_to`, `acknowledged_within` or `resolved_within`. `assigned_to` is the exact final set of assignees. The `_within` durations run from creation to the first real transition. See [Checks reference](https://docs.ancha.io/reference/checks#pagerduty). ## Limits - No contact methods, notification rules, teams, rotations or overrides. - One alert per incident; no alert grouping or event orchestration. - Escalation timers advance with the simulated clock. # Better Stack Source: https://docs.ancha.io/tools/better-stack A stateful replica of Better Stack's Uptime API: monitors, heartbeats, incidents, on-call and status pages, where nothing is probed. The Better Stack replica implements 29 endpoints of the Uptime API, versions v2 and v3. Nothing is probed: a monitor is down while it has an open incident, and a heartbeat recovers when it is pinged. Its key is `better-stack`. Better Stack publishes no official SDK, so the replica was tested over HTTP against the documented shapes. ## Connect | Variable | Value | | --- | --- | | `BETTER_STACK_API_URL` | Base URL, standing in for `https://uptime.betterstack.com`. Paths start with `/api/v2/`, `/api/v3/` and `/api/v1/heartbeat/`. | | `BETTER_STACK_API_TOKEN` | Fake team API token. Send `Authorization: Bearer `. | ```js Node const response = await fetch(process.env.BETTER_STACK_API_URL + '/api/v2/monitors', { headers: { Authorization: 'Bearer ' + process.env.BETTER_STACK_API_TOKEN } }); const monitors = await response.json(); ``` This is what `ancha connect better-stack --lang ts` prints. ## Endpoints All 29 endpoints are partial. Anything else returns HTTP 501 with `x-ancha-gap: 1`. | Method | Path | Notes | | --- | --- | --- | | `GET`, `POST` | `/api/v2/monitors` | `url` and `pronounceable_name` filters; `page` and `per_page`. Creates `status`, `keyword`, `ping`, `tcp` and other HTTP-level types; nothing is probed. | | `GET`, `PATCH`, `DELETE` | `/api/v2/monitors/{id}` | Rename, retarget, change frequency or keyword, pause and resume. | | `GET` | `/api/v2/monitors/{id}/response-times` | Hourly per region for the last day, slow while an incident was open. | | `GET` | `/api/v2/monitors/{id}/sla` | Availability from the monitor's incidents. | | `GET`, `POST` | `/api/v2/heartbeats` | | | `GET`, `PATCH`, `DELETE` | `/api/v2/heartbeats/{id}` | | | `GET`, `POST` | `/api/v1/heartbeat/{token}` | The public ping URL. A ping resolves the heartbeat's open incident. | | `GET`, `POST` | `/api/v3/incidents` | `from`, `to`, `monitor_id`, `heartbeat_id`, `resolved` and `acknowledged` filters. Manual incidents; nobody is paged. | | `GET`, `DELETE` | `/api/v3/incidents/{id}` | | | `POST` | `/api/v3/incidents/{id}/acknowledge`, `/api/v3/incidents/{id}/resolve` | | | `GET` | `/api/v2/on-calls` | Who is on call now, with users included. | | `GET` | `/api/v2/on-calls/{id}` | An ID, or `default`. | | `GET` | `/api/v2/status-pages`, `/api/v2/status-pages/{id}`, `/api/v2/status-pages/{id}/resources` | | | `GET`, `POST` | `/api/v2/status-pages/{id}/status-reports` | Manual and maintenance reports with their first update. Subscribers are not notified. | | `GET` | `/api/v2/status-pages/{id}/status-reports/{report_id}` | | | `POST` | `/api/v2/status-pages/{id}/status-reports/{report_id}/status-updates` | A report ends when an update resolves every affected resource. | ## Starter data The `better-stack-starter` dataset is the team **Acme**, 20 minutes after an Orders API incident was resolved and with a warehouse sync heartbeat that missed its check-in. | Record | Detail | | --- | --- | | Monitors | "Orders API" (every 30 s), "Acme dashboard", "Checkout", "Payment webhooks" (a keyword monitor), "Status page" and a paused "Staging dashboard", in the groups Production, Public and Staging. | | Heartbeats | "Warehouse sync: Ridgeway Robotics" (every 15 minutes, 5 minutes grace; last ping 38 minutes ago) and "Nightly database backup" (daily). | | Incidents | Resolved: Checkout 502s two days ago (Greg Wilson), an Orders API timeout 3 hours ago and Orders API 504s 35 minutes ago (both Rachel Moore). Open: the missed warehouse sync heartbeat, 20 minutes ago. | | On-call | "Orders on-call" (default): Rachel Moore. "Platform on-call": Greg Wilson and Victor Lane. | | Status page | "Acme" at `status.acme.example`, with the resources Dashboard, Orders API, Checkout and Warehouse sync, and one resolved report, "Slow order processing". | Users: Rachel Moore, Greg Wilson, Victor Lane and Ben Carter. ## Checks Better Stack has no state checks. Use trace checks, for example `{ called: "POST /api/v3/incidents/*/acknowledge" }`. ## Limits - Nothing is probed: a monitor is down while it has an open incident, and heartbeats recover when pinged. - No escalation policies, paging, telemetry or status page subscribers. - No official SDK exists; tested against the documented REST shapes. # Linear Source: https://docs.ancha.io/tools/linear A stateful replica of Linear's GraphQL API: issues, comments, labels, workflow states, teams, projects and users, pruned to the operations it serves. The Linear replica serves 27 GraphQL operations from Linear's published schema: 17 queries and 10 mutations over issues, comments, labels, workflow states, teams, projects and users. Its key is `linear`. It was tested with `@linear/sdk`. ## Connect | Variable | Value | | --- | --- | | `LINEAR_API_URL` | The GraphQL endpoint. | | `LINEAR_API_KEY` | Fake personal API key. Send it in the `Authorization` header, as the SDK does. | ```js Node import { LinearClient } from '@linear/sdk'; const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY, apiUrl: process.env.LINEAR_API_URL }); const issues = await linear.issues({ first: 10 }); ``` This is what `ancha connect linear --lang ts` prints. `@linear/sdk` accepts a plain-http `apiUrl` only on `localhost`, `127.0.0.1` or `[::1]`, which is where a local world runs. A hosted workspace serves the replica over HTTPS. ## Operations | Kind | Operation | Support | | --- | --- | --- | | query | `viewer`, `organization`, `teams`, `team`, `users`, `user` | supported | | query | `issues` | partial: `filter` supports `and`/`or`, string, ID, number, date and boolean comparators, relation and collection filters; `sort` is a gap | | query | `issue` | supported | | query | `searchIssues` | partial: every term must appear in the identifier, title or description, and in comments with `includeComments` | | query | `workflowStates`, `workflowState`, `issueLabels`, `issueLabel`, `comments`, `comment`, `projects`, `project` | supported | | mutation | `issueCreate` | partial: cycles, templates, subscribers, milestones, releases and SLAs are gaps | | mutation | `issueUpdate` | partial: moving an issue to another team is a gap | | mutation | `issueArchive`, `issueAddLabel`, `issueRemoveLabel`, `commentUpdate`, `commentDelete`, `issueLabelCreate` | supported | | mutation | `issueDelete` | partial: trashes the issue; `permanentlyDelete` is a gap | | mutation | `commentCreate` | partial: comments on issues only | Fields and types outside these operations answer as coverage gaps in GraphQL's error shape, with `x-ancha-gap: 1`. ## Starter data The `linear-starter` dataset is the workspace **Acme** (`acme`). The token belongs to the user `ancha`. | Record | Detail | | --- | --- | | Users | Ancha assistant, Ben Carter (admin, Engineering lead), Greg Wilson, Rachel Moore, Victor Lane, Ellen Price, Laura Kim, Mike Turner (admin). | | Team `ENG` | Engineering: Orders API, web app and dashboards. Members: the bot, Ben, Greg, Rachel, Ellen. Team labels Incident and Orders API. | | Team `DATA` | Data Platform: warehouse sync and pipelines. Members: the bot, Victor, Greg, Rachel. Team label Warehouse sync. | | Workspace labels | Bug, Feature, Improvement, Customer, Documentation. | | Project "Scheduled reports" | Led by Ellen, started 2026-10-19, target 2026-12-11. | | Project "Orders API reliability" | Led by Rachel, started 2026-10-05, target 2026-11-27. | Seventeen issues, with the simplified workflow states Backlog, Todo, In Progress, In Review and Done: | State | Issues | | --- | --- | | Done | "Order totals off by one cent for multi-currency carts", "GET /orders is slow for large accounts", "Fulfillment webhooks time out after 10 seconds", "Dashboard shows yesterday's numbers twice" (Kestrel Credit Union), "Orders API p95 latency above 700ms after 4.12.1" (an incident, resolved by a rollback), and "Speed up the Ridgeway Robotics warehouse sync" (Data Platform). | | In Progress | "Fix the fulfillment queue backlog introduced in 4.12.1" (Greg, a child of the latency incident) and "Clarify recovery checks in the Orders runbook" (Ben). | | In Review | "Rate-limit order creation per API key" (Greg). | | Todo | "Return 409 when an order is cancelled twice", "Explain delayed CSV exports" (Customer, Documentation), "Scheduled reports: retry failed deliveries" and "Upgrade the Snowflake connector" (Data Platform, due 2026-11-01). | | Backlog | "Document the order states", "Scheduled reports: send in the recipient's time zone", "SCIM provisioning for Okta" and "Dark mode for dashboards". | Several issues carry comments, estimates and project links. `ancha sandbox configuration --tools linear --json` prints every field. ## Checks Linear has no state checks. Every operation goes through one GraphQL endpoint, so a trace check names that path; run `ancha explain` or `ancha world trace` once to see how the replica records the agent's requests before you write one. ## Limits - Linear's published schema, pruned to the operations above; other fields and types answer as coverage gaps. - No cycles, initiatives, documents, attachments, notifications, webhooks or sorting; filters cover the common comparators. - `@linear/sdk` accepts a plain-http `apiUrl` only on localhost, `127.0.0.1` or `[::1]`; elsewhere the endpoint needs TLS. # Jira Source: https://docs.ancha.io/tools/jira A stateful replica of Jira Cloud's REST API v3 and Agile API: projects, issues, JQL search, comments, transitions, links, watchers, users, boards and sprints. The Jira replica implements 57 endpoints of Jira Cloud's REST API v3 and Agile API 1.0 for team-managed projects with the simplified workflow To Do, In Progress, In Review and Done. Its key is `jira`. Atlassian publishes no official JavaScript client, so the replica was tested over HTTP with Basic authentication. ## Connect | Variable | Value | | --- | --- | | `JIRA_BASE_URL` | The site URL, standing in for `https://.atlassian.net`. Paths start with `/rest/api/3/` and `/rest/agile/1.0/`. | | `JIRA_EMAIL`, `JIRA_API_TOKEN` | Fake account email and API token. Send them as HTTP Basic: `Authorization: Basic base64(email:token)`. | ```js Node const auth = 'Basic ' + Buffer.from(process.env.JIRA_EMAIL + ':' + process.env.JIRA_API_TOKEN).toString('base64'); const response = await fetch(process.env.JIRA_BASE_URL + '/rest/api/3/search/jql?jql=' + encodeURIComponent('project = SUP AND statusCategory != Done') + '&fields=summary,status,assignee', { headers: { Authorization: auth, Accept: 'application/json' } }); const { issues } = await response.json(); ``` This is what `ancha connect jira --lang ts` prints. Keep `fields=` on searches: without it `/search/jql` returns only issue IDs, as live Jira does. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `GET` | `/rest/api/3/myself`, `/rest/api/3/serverInfo` | supported | | `GET`, `POST` | `/rest/api/3/search/jql` | partial: a JQL subset; `fields` defaults to `id` only, as in Jira; `nextPageToken` paging | | `POST` | `/rest/api/3/search/approximate-count` | supported | | `GET`, `POST` | `/rest/api/3/search` | supported: answers 410, as live Jira does since Atlassian removed it | | `POST` | `/rest/api/3/issue` | partial: system fields, parent, sprint and story points; components and versions do not exist on these projects; attachments and time tracking are gaps | | `POST` | `/rest/api/3/issue/bulk` | partial: always a gap | | `GET` | `/rest/api/3/issue/:key` | supported: `fields`, `expand=renderedFields,names,transitions,changelog` | | `PUT` | `/rest/api/3/issue/:key` | partial: as create; status changes only through transitions, as in Jira | | `DELETE` | `/rest/api/3/issue/:key` | supported | | `PUT` | `/rest/api/3/issue/:key/assignee` | supported | | `GET` | `/rest/api/3/issue/:key/transitions` | supported: the simplified workflow, any status to any other | | `POST` | `/rest/api/3/issue/:key/transitions` | supported: a resolution on Done; a comment through `update` | | `GET` | `/rest/api/3/issue/:key/changelog` | supported | | `GET`, `POST` | `/rest/api/3/issue/:key/comment` | supported: visibility is stored, not enforced | | `GET`, `PUT`, `DELETE` | `/rest/api/3/issue/:key/comment/:id` | supported | | `GET`, `POST`, `DELETE` | `/rest/api/3/issue/:key/watchers` | supported | | `POST` | `/rest/api/3/issueLink` | supported: Blocks, Relates, Duplicate and Cloners | | `GET`, `DELETE` | `/rest/api/3/issueLink/:id` | supported | | `GET` | `/rest/api/3/user`, `/rest/api/3/user/search`, `/rest/api/3/user/assignable/search` | supported: by display name or email prefix, or `accountId` | | `GET` | `/rest/api/3/project`, `/rest/api/3/project/search`, `/rest/api/3/project/:key`, `/rest/api/3/project/:key/statuses` | supported | | `GET` | `/rest/api/3/project/:key/components`, `/rest/api/3/project/:key/versions` | supported: the projects have none | | `GET` | `/rest/api/3/issue/createmeta/:key/issuetypes`, `/rest/api/3/issue/createmeta/:key/issuetypes/:type` | supported | | `GET` | `/rest/api/3/issuetype`, `/rest/api/3/priority`, `/rest/api/3/status`, `/rest/api/3/statuscategory`, `/rest/api/3/resolution`, `/rest/api/3/issueLinkType`, `/rest/api/3/field`, `/rest/api/3/label` | supported | | `GET` | `/rest/agile/1.0/board`, `/rest/agile/1.0/board/:id`, `/rest/agile/1.0/board/:id/sprint`, `/rest/agile/1.0/board/:id/issue`, `/rest/agile/1.0/board/:id/backlog` | supported | | `GET` | `/rest/agile/1.0/sprint/:id`, `/rest/agile/1.0/sprint/:id/issue` | supported | | `POST` | `/rest/agile/1.0/sprint/:id/issue` | supported: up to 50 issues; rank options are a gap | | `POST` | `/rest/agile/1.0/backlog/issue` | supported | | `POST` | `/rest/agile/1.0/sprint`, `/rest/agile/1.0/sprint/:id` | partial: always a gap; sprints cannot be created, started, completed or renamed | Anything else returns HTTP 501 with Jira's error shape and `x-ancha-gap: 1`. ## Starter data The `jira-starter` dataset is the site **acme** (America/Los_Angeles). The token belongs to the user `ancha`; the other users are Ben Carter, Rachel Moore, Mike Turner, Greg Wilson, Laura Kim, Tom Hughes, Ellen Price, Victor Lane, Anna Fischer and the app user "Automation for Jira". | Project | Lead | Issues | | --- | --- | --- | | `OPS` Platform Operations (with a board) | Rachel Moore | The epic "Orders API reliability" with "Investigate slow requests on /orders" (Done, with Rachel's root-cause comment), "Runbook: rollout instructions for the orders API" (In Review, Ben), "Alert on the orders.errors rate" (To Do), plus "Retry policy for payment webhooks" (In Progress, Greg) and "Load test the export pipeline before the scheduled reports beta" (To Do, due 2026-11-13). | | `SUP` Customer Escalations | Laura Kim | "Kestrel Credit Union: dashboard shows yesterday's numbers twice" (Done), "Bluefield Logistics: SCIM provisioning with Okta" (In Progress), "Helix Manufacturing: billing emails bounce" (In Progress, Anna), "Pinecrest Health: 20 more seats mid-contract" (To Do) and "Ridgeway Robotics cannot find last month's CSV export" (To Do, created 34 minutes before start, assigned to Mike). | | `DATA` Data Platform | Greg Wilson | "Warehouse sync: incremental loads" (In Progress, Victor) and "Data platform access for Victor" (To Do). | Sprints on the OPS board: Sprint 41 (closed), Sprint 42 (active, "Ship the orders API reliability fixes") and Sprint 43 (future). Two issue links: the Ridgeway escalation relates to the load test, and the slow-requests bug blocks the alert task. ## Checks Jira has no state checks. Use trace checks, for example `{ called: "POST /rest/api/3/issue" }` or `{ called: "POST /rest/api/3/issue/*/transitions" }`. ## Limits - Atlassian publishes no official JavaScript client: call `JIRA_BASE_URL/rest/api/3/...` with HTTP Basic. `/rest/api/3/search` is gone, as on live Jira; `/search/jql` returns only issue IDs unless you request fields. - Team-managed projects with the simplified workflow. Attachments, worklogs, webhooks, sprint changes and JQL history operators are gaps. # Okta Source: https://docs.ancha.io/tools/okta A stateful replica of Okta's Management API v1: users and their lifecycle, groups, applications and app assignments, and the System Log. The Okta replica implements 31 endpoints of the Management API v1 for one org: users with their lifecycle states, groups, applications, app assignments and the System Log. Its key is `okta`. It was tested with `@okta/okta-sdk-nodejs`. ## Connect | Variable | Value | | --- | --- | | `OKTA_ORG_URL` | The org URL. Paths start with `/api/v1/`. | | `OKTA_API_TOKEN` | Fake SSWS API token. Send `Authorization: SSWS `. | ```js Node import okta from '@okta/okta-sdk-nodejs'; const client = new okta.Client({ orgUrl: process.env.OKTA_ORG_URL, token: process.env.OKTA_API_TOKEN }); for await (const user of await client.userApi.listUsers()) console.log(user?.profile?.login); ``` This is what `ancha connect okta --lang ts` prints. ## Endpoints All 31 endpoints are partial. Anything else returns HTTP 501 with Okta's error shape and `x-ancha-gap: 1`. ### Users | Method | Path | Notes | | --- | --- | --- | | `GET` | `/api/v1/users` | `q`, `filter` and `search` with the SCIM operators `eq`, `ne`, `sw`, `co`, `ew`, `gt`, `ge`, `lt`, `le` and `pr`; `limit` and `after`. Deprovisioned users appear only through `filter` or `search`. | | `POST` | `/api/v1/users` | `activate` and `nextLogin`, the default profile schema and password policy, `groupIds`. | | `GET` | `/api/v1/users/{id}` | By ID, login, or `me` for the token's owner. | | `POST` | `/api/v1/users/{id}` | Partial profile update and password change. | | `DELETE` | `/api/v1/users/{id}` | Deactivates first; deletes a deprovisioned user. | | `GET` | `/api/v1/users/{id}/groups`, `/api/v1/users/{id}/appLinks` | | | `DELETE` | `/api/v1/users/{id}/sessions` | | | `POST` | `/api/v1/users/{id}/lifecycle/activate` | `sendEmail=false` returns an activation link. | | `POST` | `/api/v1/users/{id}/lifecycle/deactivate`, `suspend`, `unsuspend`, `unlock` | | | `POST` | `/api/v1/users/{id}/lifecycle/reset_password` | `sendEmail=false` returns a reset link. Nothing is emailed. | ### Groups | Method | Path | Notes | | --- | --- | --- | | `GET`, `POST` | `/api/v1/groups` | `q`, `filter`, `search`, `limit`, `after` and `expand=stats`. | | `GET`, `PUT`, `DELETE` | `/api/v1/groups/{groupId}` | | | `GET` | `/api/v1/groups/{groupId}/users` | | | `PUT`, `DELETE` | `/api/v1/groups/{groupId}/users/{userId}` | | ### Applications | Method | Path | Notes | | --- | --- | --- | | `GET` | `/api/v1/apps` | `q`, `filter` (`status`, `name`, `user.id`, `group.id`), `limit` and `after`. | | `GET` | `/api/v1/apps/{appId}` | | | `GET`, `POST` | `/api/v1/apps/{appId}/users` | Direct (`USER`) and group (`GROUP`) assignments; `expand=user`. | | `DELETE` | `/api/v1/apps/{appId}/users/{userId}` | | | `GET` | `/api/v1/apps/{appId}/groups` | | | `PUT`, `DELETE` | `/api/v1/apps/{appId}/groups/{groupId}` | | ### System Log | Method | Path | Notes | | --- | --- | --- | | `GET` | `/api/v1/logs` | `since`, `until`, `filter`, `q`, `sortOrder`, `limit` and `after`. Every change made through the API is logged. There is no polling next link. | ## Behavior - One org, `acme`, and one SSWS token owned by the super administrator `ancha@acme.example`. - Lifecycle changes are immediate. Activation and reset links are returned in the response, never emailed. - The System Log holds the seeded events and every change the agent makes. ## Starter data The `okta-starter` dataset is the org **Acme**. | Record | Detail | | --- | --- | | Users | Twelve: the automation account `ancha@acme.example`, Mike Turner, Ben Carter, Rachel Moore, Greg Wilson, Laura Kim, Ellen Price and Anna Fischer (active); Tom Hughes (locked out 43 minutes before start after three failed sign-ins); Victor Lane (staged, created 26 hours ago); Iris Cole (suspended); Owen Marsh (deprovisioned). Profiles carry titles, departments, managers and employee numbers. | | Groups | Engineering, Data Platform, On-call, Sales, Customer Success, Product, Finance, Leadership and IT Admins. | | Applications | Eight SAML apps: Slack and Google Workspace for everyone; GitHub and Datadog for Engineering and Data Platform; PagerDuty for On-call; HubSpot for Sales and Customer Success; Snowflake for Data Platform; AWS for Engineering, plus Anna Fischer directly. | | System Log | Ten events: Owen's deactivation, Iris's suspension, Victor's creation, Rachel's sign-in and single sign-on to PagerDuty, Tom's two failed sign-ins and lockout, and Ben's sign-in. | ## Checks Okta has no state checks. Use trace checks, for example `{ called: "POST /api/v1/users/*/lifecycle/unlock" }` or `{ not_called: "DELETE /api/v1/users/*" }`. ## Limits - One org and one SSWS token; no OAuth service apps, factors, policies or sign-in flows. - Nothing is emailed: activation and reset links come back in the response. - The System Log gives no polling next link. # Amazon S3 Source: https://docs.ancha.io/tools/amazon-s3 A stateful replica of the Amazon S3 REST API with SigV4: buckets, objects, tags, conditional writes, checksums and multipart uploads for one account. The Amazon S3 replica implements 25 operations of the S3 REST API for one account, with path-style addressing. Its key is `amazon-s3`. It was tested with `@aws-sdk/client-s3`. ## Connect | Variable | Value | | --- | --- | | `AWS_ENDPOINT_URL_S3` | The endpoint. The SDK reads it itself. | | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | Fake credentials for the account's one access key. Requests are signed with SigV4 as usual. | | `AWS_REGION` | `us-east-1`, the region of every starter bucket except `acme-backups`. | ```js Node import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ endpoint: process.env.AWS_ENDPOINT_URL_S3, forcePathStyle: true }); const { Buckets } = await s3.send(new ListBucketsCommand({})); ``` This is what `ancha connect amazon-s3 --lang ts` prints. The replica is one endpoint, so use path-style addressing: the SDK chooses it for an IP endpoint; set `forcePathStyle` for a host name. ## Operations | Operation | Request | Notes | | --- | --- | --- | | ListBuckets | `GET /` | `max-buckets`, `continuation-token`, `prefix` and `bucket-region`. | | CreateBucket | `PUT /{Bucket}` | `LocationConstraint` must match the signing region. Names other accounts own answer `BucketAlreadyExists`. ACLs are disabled. | | HeadBucket, DeleteBucket | `HEAD`, `DELETE /{Bucket}` | | | GetBucketLocation | `GET /{Bucket}?location` | | | GetBucketTagging, PutBucketTagging, DeleteBucketTagging | `/{Bucket}?tagging` | | | ListObjectsV2 | `GET /{Bucket}?list-type=2` | `prefix`, `delimiter`, `max-keys`, continuation tokens, `start-after`, `fetch-owner`, `encoding-type=url`. | | ListObjects | `GET /{Bucket}` | | | DeleteObjects | `POST /{Bucket}?delete` | Up to 1,000 keys. Requires `Content-MD5` or an `x-amz-checksum` header, as S3 does. | | PutObject | `PUT /{Bucket}/{Key+}` | Verifies `Content-MD5` and CRC32, CRC32C, SHA-1 and SHA-256 checksums. `If-None-Match: *` and `If-Match` conditional writes. User metadata and tagging. | | CopyObject | `PUT /{Bucket}/{Key+}` with `x-amz-copy-source` | `COPY` or `REPLACE` metadata and tagging directives. Conditional copy headers are gaps. | | GetObject | `GET /{Bucket}/{Key+}` | Single byte ranges, conditional reads, `response-*` overrides, checksum mode. Glacier objects answer `InvalidObjectState`. | | HeadObject, DeleteObject | `HEAD`, `DELETE /{Bucket}/{Key+}` | Unversioned buckets only. | | GetObjectTagging, PutObjectTagging, DeleteObjectTagging | `/{Bucket}/{Key+}?tagging` | | | CreateMultipartUpload | `POST /{Bucket}/{Key+}?uploads` | | | UploadPart | `PUT /{Bucket}/{Key+}?partNumber&uploadId` | | | ListParts, AbortMultipartUpload | `GET`, `DELETE /{Bucket}/{Key+}?uploadId` | | | CompleteMultipartUpload | `POST /{Bucket}/{Key+}?uploadId` | Parts other than the last must be at least 5 MiB. | | ListMultipartUploads | `GET /{Bucket}?uploads` | | All 25 operations are partial. Anything else returns HTTP 501 with S3's XML error shape and `x-ancha-gap: 1`. ## Behavior - **One account**, `acme-production` (`481516234200`), with one access key for the user `ancha-agent`. Each bucket grants that key read-write, read-only or no access, which stands in for IAM policies. - **Bucket names** such as `test`, `data`, `backups`, `logs`, `uploads` and `reports` are taken by other accounts. - **Presigned URLs** are verified but never expire. ## Starter data The `amazon-s3-starter` dataset has four buckets. | Bucket | Region | Access | Contents | | --- | --- | --- | --- | | `acme-exports` | us-east-1 | read-write | Customer exports under `exports///`: Ridgeway Robotics orders and shipments for June (in `GLACIER`), August and September, Kestrel Credit Union daily rollups for 2026-10-29 (with doubled rows) and 2026-10-30, Pinecrest Health seats, Summit Freight active users, and a `README.txt` describing the layout. Objects carry `customer` and `retention` tags. | | `acme-warehouse-staging` | us-east-1 | read-only | The Ridgeway Robotics sync for 2026-10-31: `_manifest.json` and JSON Lines parts for orders and shipments. | | `acme-backups` | us-west-2 | none | Two nightly database dumps. Every request fails with access denied. | | `acme-web-assets` | us-east-1 | read-write | Release `2026.10.29`: `manifest.json`, `app.js` and `app.css` with `Cache-Control` headers. | ## Checks Amazon S3 has no state checks. Use trace checks on path-style requests, for example `{ called: "PUT /acme-exports/exports/*" }` or `{ not_called: "DELETE /acme-exports/*" }`. ## Limits - No versioning, object lock, ACLs, KMS encryption, Glacier restores or event notifications. - One account and one access key; per-bucket access instead of IAM. # Clerk Source: https://docs.ancha.io/tools/clerk A stateful replica of Clerk's Backend API v1: users, organizations and memberships, invitations and sessions for one application instance. The Clerk replica implements 32 endpoints of the Backend API v1 for one application instance with email sign-in. Its key is `clerk`. It was tested with `@clerk/backend`. ## Connect | Variable | Value | | --- | --- | | `CLERK_API_URL` | Base URL. Paths start with `/v1/`. | | `CLERK_SECRET_KEY` | Fake secret key. Send `Authorization: Bearer `. | ```js Node import { createClerkClient } from '@clerk/backend'; const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, apiUrl: process.env.CLERK_API_URL }); const { data: users } = await clerk.users.getUserList({ limit: 10 }); ``` This is what `ancha connect clerk --lang ts` prints. ## Endpoints All 32 endpoints are partial. Anything else returns HTTP 501 with Clerk's error shape and `x-ancha-gap: 1`. ### Users | Method | Path | Notes | | --- | --- | --- | | `GET` | `/v1/users` | `email_address`, `user_id`, `username`, `external_id`, `organization_id`, `query`, name, email and username queries, `banned`, activity and creation windows, `order_by`, `limit` and `offset`. | | `GET` | `/v1/users/count` | | | `POST` | `/v1/users` | One email address per user. Password length and breached-password checks. Phone numbers, web3 wallets and imported hashes are gaps. | | `GET`, `PATCH`, `DELETE` | `/v1/users/{user_id}` | | | `PATCH` | `/v1/users/{user_id}/metadata` | Deep merge; `null` removes a key. | | `POST` | `/v1/users/{user_id}/ban` | Revokes the user's active sessions. | | `POST` | `/v1/users/{user_id}/unban`, `lock`, `unlock` | | | `GET` | `/v1/users/{user_id}/organization_memberships` | | ### Organizations | Method | Path | Notes | | --- | --- | --- | | `GET` | `/v1/organizations` | `query`, `order_by` (`name`, `created_at`, `members_count`), `include_members_count`, `user_id`. | | `POST` | `/v1/organizations` | | | `GET` | `/v1/organizations/{organization_id}` | By ID or slug. | | `PATCH`, `DELETE` | `/v1/organizations/{organization_id}` | | | `PATCH` | `/v1/organizations/{organization_id}/metadata` | | | `GET`, `POST` | `/v1/organizations/{organization_id}/memberships` | Members and pending invitations share `max_allowed_memberships`. | | `PATCH`, `DELETE` | `/v1/organizations/{organization_id}/memberships/{user_id}` | | | `GET`, `POST` | `/v1/organizations/{organization_id}/invitations` | Recorded, never emailed. Pending invitations expire after `expires_in_days`, 30 by default. | | `GET` | `/v1/organizations/{organization_id}/invitations/{invitation_id}` | | | `POST` | `/v1/organizations/{organization_id}/invitations/{invitation_id}/revoke` | | ### Invitations and sessions | Method | Path | Notes | | --- | --- | --- | | `GET`, `POST` | `/v1/invitations` | Instance-level invitations. | | `POST` | `/v1/invitations/{invitation_id}/revoke` | | | `GET` | `/v1/sessions` | `user_id` or `client_id` is required. | | `GET` | `/v1/sessions/{session_id}` | | | `POST` | `/v1/sessions/{session_id}/revoke` | | ## Starter data The `clerk-starter` dataset is the instance **Acme dashboard**: the customers' users and organizations. | Organization | Plan | Members | | --- | --- | --- | | Ridgeway Robotics (`ridgeway-robotics`) | pilot, until 2026-11-30 | Erin Walsh (admin), Hector Ruiz, Bao Tran. Three pending invitations. | | Pinecrest Health (`pinecrest-health`) | enterprise, 120 seats from 2026-12-01 | Helen Brooks (admin), Omar Haddad, Grace Liu. | | Kestrel Credit Union (`kestrel-credit-union`) | business, renewal 2026-11-07 | Dale Whitfield (admin), Nina Alvarez (locked). | | Summit Freight (`summit-freight`) | business | Carla Diaz (admin), Pete Novak. One pending invitation. | | Helix Manufacturing (`helix-manufacturing`) | business, renewal 2026-12-15 | Ivan Petrov (admin). | Twelve users in all, including Laura Kim from Acme with `staff: true` in her public metadata. Erin Walsh's private metadata links her to HubSpot contact `30001`. One instance-level invitation to `security@bluefield-logistics.example` is pending. ## Checks Clerk has no state checks. Use trace checks, for example `{ called: "POST /v1/organizations/*/invitations" }` or `{ not_called: "DELETE /v1/users/*" }`. ## Limits - One application instance with email sign-in; phone numbers, web3 wallets, SAML and imported password hashes are coverage gaps. - Invitations are recorded, never emailed; pending ones expire on the scene clock. - Session tokens and JWT verification are not modeled; sessions can be listed and revoked. # HubSpot Source: https://docs.ancha.io/tools/hubspot A stateful replica of HubSpot's CRM v3 API: companies, contacts and deals with search, batches, associations, pipelines and properties. The HubSpot replica implements the CRM v3 REST API for companies, contacts and deals, including search with delayed indexing, batch operations, associations and read-only pipelines and properties. Its key is `hubspot`. ## Connect | Variable | Value | | --- | --- | | `HUBSPOT_API_URL` | Base URL. Paths start with `/crm/v3/`. | | `HUBSPOT_ACCESS_TOKEN` | Fake private-app token. Send `Authorization: Bearer `. | ```js Node const response = await fetch(process.env.HUBSPOT_API_URL + '/crm/v3/objects/companies', { headers: { Authorization: 'Bearer ' + process.env.HUBSPOT_ACCESS_TOKEN } }); const companies = await response.json(); ``` This is what `ancha connect hubspot --lang ts` prints. Existing v3 clients work when you set their base URL to `HUBSPOT_API_URL`. ## Endpoints | Method | Path | | --- | --- | | `GET` | `/crm/v3/objects/:type` | | `POST` | `/crm/v3/objects/:type` | | `GET` | `/crm/v3/objects/:type/:id` | | `PATCH` | `/crm/v3/objects/:type/:id` | | `DELETE` | `/crm/v3/objects/:type/:id` | | `POST` | `/crm/v3/objects/:type/search` | | `POST` | `/crm/v3/objects/:type/batch/:operation` | | `GET` | `/crm/v3/properties/:type` | | `GET` | `/crm/v3/properties/:type/:property` | | `GET` | `/crm/v3/pipelines/:type` | | `GET` | `/crm/v3/pipelines/:type/:id` | | `GET` | `/crm/v3/pipelines/:type/:id/stages` | | `GET` | `/crm/v3/pipelines/:type/:id/stages/:stage` | | `GET` | `/crm/v3/associations/:from/:to/types` | | `POST` | `/crm/v3/associations/:from/:to/batch/:operation` | | `GET` | `/crm/v3/objects/:type/:id/associations/:to` | | `PUT` | `/crm/v3/objects/:type/:id/associations/:to/:toId/:associationType` | | `DELETE` | `/crm/v3/objects/:type/:id/associations/:to/:toId/:associationType` | `:type` is `companies`, `contacts` or `deals`, or the type IDs `0-2`, `0-1` and `0-3`. All endpoints are partial. Unsupported routes, fields, operators and options return HubSpot's error shape with an `ancha: not implemented:` message and `x-ancha-gap: 1`. HubSpot's newer date-based routes, such as `/crm/objects/2026-09/...`, are not implemented. ## Behavior - **Search is delayed.** Direct reads and lists see writes at once. Search sees a new or changed record only after 2 simulated seconds, as HubSpot's search index lags. Archived records leave search at once. - **Search syntax.** `filterGroups` (up to 5 groups, 6 filters each, 18 in total; filters in a group are ANDed, groups are ORed), `query`, one `sorts` entry, `properties`, `limit` (default 10, at most 200) and `after`. Operators: `EQ`, `NEQ`, `LT`, `LTE`, `GT`, `GTE`, `BETWEEN`, `IN`, `NOT_IN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY`. - **Lists** return 10 records by default and at most 100, with an `after` cursor. - **Batches** take 1 to 100 inputs for `read`, `create`, `update` and `archive`. - **Properties are a fixed catalog.** Companies: `name`, `domain`, `website`, `description`, `industry`, `city`, `state`, `country`, `phone`, `numberofemployees`, `annualrevenue`, `lifecyclestage`. Contacts: `firstname`, `lastname`, `email`, `phone`, `mobilephone`, `company`, `jobtitle`, `lifecyclestage`, `hs_lead_status`. Deals: `dealname`, `amount`, `closedate`, `pipeline`, `dealstage`, `description`, `dealtype`. All three also have `external_id` and `hubspot_owner_id`. Unknown properties are rejected. - **Associations** use HubSpot's default type IDs: contact to company `1` (primary) and `279`, deal to company `5` (primary) and `341`, deal to contact `3`, and their reverse IDs. - **Permissions.** The token has read and write scopes for all three objects. Company `102` is outside the app's access: reading it fails and lists and searches leave it out. ## Starter data The `hubspot-starter` dataset is the portal **Acme** (`510001`): nine companies, thirteen contacts and eight deals. | Object | ID | Record | | --- | --- | --- | | Company | `101` | Ridgeway Robotics, `ridgeway-robotics.example`, manufacturing, 850 employees, lifecycle stage `opportunity` | | Company | `102` | Helix Manufacturing, a customer. Not accessible to the token. | | Company | `103` | Pinecrest Health, hospital and health care, 1,900 employees, `customer` | | Company | `104` | Bluefield Logistics, logistics, 620 employees, `opportunity` | | Company | `105` | Kestrel Credit Union, banking, 410 employees, `customer` | | Company | `106` | Harbor Foods, food production, 2,300 employees, `lead` | | Company | `107` | Summit Freight, transportation, 350 employees, `customer` | | Company | `108` | Lakeview Retail, retail, 1,200 employees, `salesqualifiedlead` | | Company | `109` | Redwood Labs, software, 90 employees, `opportunity` | | Contact | `201`, `202` | Erin Walsh (VP Operations) and Paul Young (Warehouse Systems Lead) at Ridgeway Robotics | | Contact | `203`, `204` | Nora Bell (CIO) and James Ortiz (Director of Analytics) at Pinecrest Health | | Contact | `205`, `206` | Owen Hart (VP Operations) and Grace Lin (IT Security Lead) at Bluefield Logistics | | Contact | `207`, `208` | Carla Diaz (Head of Data) and Peter Novak (Procurement Manager) at Kestrel Credit Union | | Contact | `209` | Lucy Grant, Operations Analyst at Harbor Foods | | Contact | `210`, `211` | Daniel Reyes (COO) and Megan Shaw (BI Manager) at Summit Freight | | Contact | `212`, `213` | Rob Fisher (Director of Ecommerce, Lakeview Retail) and Hannah Cole (CTO, Redwood Labs) | | Deal | `301` | "Ridgeway Robotics — Analytics rollout", 96000, closes 2026-11-30, stage `presentationscheduled` | | Deal | `302` | "Pinecrest Health — Enterprise plan", 144000, `closedwon` | | Deal | `303` | "Bluefield Logistics — Pilot", 36000, closes 2026-12-12, `presentationscheduled` | | Deal | `304` | "Kestrel Credit Union — Renewal", 48000, closes 2026-11-20, `presentationscheduled` | | Deal | `305` | "Harbor Foods — Analytics starter", 18000, closes 2027-01-31, `appointmentscheduled` | | Deal | `306` | "Summit Freight — Expansion", 27000, closes 2026-12-31, `qualifiedtobuy` | | Deal | `307` | "Lakeview Retail — Ecommerce dashboards", 52000, closes 2027-02-15, `qualifiedtobuy` | | Deal | `308` | "Redwood Labs — Platform plan", 75000, `closedlost` | | Pipeline | `default` | "Sales Pipeline": `appointmentscheduled`, `qualifiedtobuy`, `presentationscheduled`, `contractsent`, `closedwon`, `closedlost` | Each contact is associated with its company, and each deal with its company and its primary contact. ## Checks HubSpot has no state checks. Use trace checks: ```yaml - id: moved_deal trace: { called: "PATCH /crm/v3/objects/deals/301" } - id: no_deletes trace: { not_called: "DELETE /crm/*" } ``` ## Limits - Companies, contacts and deals only: no tickets, custom objects, engagements or owners. - No workflows, webhooks, OAuth, merges, upserts or restores. - No rate limits. # Salesforce Source: https://docs.ancha.io/tools/salesforce A stateful replica of Salesforce's REST API v67.0: accounts, contacts, leads, opportunities, campaigns and campaign members, with SOQL queries and composite requests. The Salesforce replica implements REST API v67.0 for six standard objects, a subset of SOQL and composite requests. Its key is `salesforce`. ## Connect | Variable | Value | | --- | --- | | `SALESFORCE_INSTANCE_URL` | The instance URL. Append `/services/data/v67.0/...`. | | `SALESFORCE_ACCESS_TOKEN` | Fake session token. Send `Authorization: Bearer `. | ```js Node const response = await fetch(process.env.SALESFORCE_INSTANCE_URL + '/services/data/v67.0/query?q=' + encodeURIComponent('SELECT Id, Name FROM Account'), { headers: { Authorization: 'Bearer ' + process.env.SALESFORCE_ACCESS_TOKEN } }); const accounts = await response.json(); ``` This is what `ancha connect salesforce --lang ts` prints. There is no OAuth or SOAP login: use the token as given. ## Endpoints | Method | Path | | --- | --- | | `GET` | `/services/data/` | | `GET` | `/services/data/v67.0` | | `GET` | `/services/data/v67.0/sobjects` | | `GET` | `/services/data/v67.0/sobjects/:type` | | `GET` | `/services/data/v67.0/sobjects/:type/describe` | | `GET` | `/services/data/v67.0/sobjects/:type/:id` | | `POST` | `/services/data/v67.0/sobjects/:type` | | `PATCH` | `/services/data/v67.0/sobjects/:type/:id` | | `DELETE` | `/services/data/v67.0/sobjects/:type/:id` | | `GET` | `/services/data/v67.0/sobjects/:type/:field/:value` | | `PATCH` | `/services/data/v67.0/sobjects/:type/:field/:value` | | `GET` | `/services/data/v67.0/query` | | `GET` | `/services/data/v67.0/query/:locator` | | `POST` | `/services/data/v67.0/composite` | All endpoints are partial. `GET /services/data/` needs no token and reports version 67.0. Unsupported objects, fields and syntax return Salesforce's error shape with an `ancha: not implemented:` message and `x-ancha-gap: 1`. ## Objects | Object | Fields besides `Id`, `CreatedDate`, `LastModifiedDate` and `External_Id__c` | | --- | --- | | Account | `Name`, `Industry`, `Type`, `Website`, `Description`, `BillingCountry`, `NumberOfEmployees`, `AnnualRevenue` | | Contact | `FirstName`, `LastName`, `AccountId`, `Email`, `Title`, `Phone` | | Lead | `FirstName`, `LastName`, `Company`, `Email`, `Title`, `Status`, `LeadSource`, `Industry` | | Opportunity | `Name`, `AccountId`, `StageName`, `CloseDate`, `Amount`, `Probability`, `LeadSource`, `Description`, `CampaignId`, and read-only `IsClosed` and `IsWon` | | Campaign | `Name`, `Type`, `Status`, `IsActive`, `StartDate`, `EndDate`, `BudgetedCost`, `ActualCost`, `Description`, `ClientAccount__c` | | CampaignMember | `CampaignId`, `ContactId` or `LeadId`, `Status` | `External_Id__c` is a unique external ID on every object, usable in `/sobjects/:type/External_Id__c/:value`. Unknown fields are rejected. Relationships must point at existing records of the right object. ## SOQL ```sql SELECT Id, Name, Amount FROM Opportunity WHERE Amount >= 50000 AND StageName IN ('Qualification', 'Proposal/Price Quote') ORDER BY Amount DESC LIMIT 20 OFFSET 0 ``` - One object, explicit fields, predicates joined with AND, one `ORDER BY` field, `LIMIT` and `OFFSET`. - Operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `IN`, `LIKE`. String comparisons ignore case. - Pages hold 2,000 records by default; `nextRecordsUrl` reads the next page from a snapshot taken when the query ran. - Relationship queries, subqueries, aggregates, `COUNT`, `GROUP BY`, `OR`, `NOT`, date functions and `FIELDS()` are coverage gaps. `POST /composite` runs 1 to 25 subrequests in order, with references such as `@{created.id}` and optional `allOrNone` rollback. ## Starter data The `salesforce-starter` dataset is the org **Acme** (`00D000000000001AAA`). | Object | ID | Record | | --- | --- | --- | | Account | `001000000000001AAA` | Ridgeway Robotics, manufacturing, prospect, 850 employees | | Account | `001000000000002AAA` | Helix Manufacturing, a customer. Not accessible to the token. | | Account | `001000000000003AAA` | Pinecrest Health, healthcare, customer, 1,900 employees | | Account | `001000000000004AAA` | Bluefield Logistics, transportation, prospect, 620 employees | | Account | `001000000000005AAA` | Kestrel Credit Union, banking, customer, 410 employees | | Account | `001000000000006AAA` | Summit Freight, transportation, customer, 350 employees | | Account | `001000000000007AAA` | Lakeview Retail, retail, prospect, 1,200 employees | | Contact | `003000000000001AAA` to `003000000000006AAA` | Erin Walsh (Ridgeway Robotics), Nora Bell (Pinecrest Health), Owen Hart (Bluefield Logistics), Carla Diaz (Kestrel Credit Union), Daniel Reyes (Summit Freight) and Rob Fisher (Lakeview Retail) | | Lead | `00Q000000000001AAA` | Theo Reed, Beacon Logistics, "Working - Contacted", from LinkedIn | | Lead | `00Q000000000002AAA` to `00Q000000000004AAA` | Lucy Grant (Harbor Foods, "Open - Not Contacted"), Hannah Cole (Redwood Labs, "Working - Contacted") and Sean Walker (Copperline Energy, "Closed - Not Converted") | | Opportunity | `006000000000001AAA` | "Ridgeway Robotics — Analytics rollout", Proposal/Price Quote, 96000, closes 2026-11-30, in the Q4 pilot campaign | | Opportunity | `006000000000002AAA` to `006000000000006AAA` | "Pinecrest Health — Enterprise plan" (Closed Won, 144000), "Bluefield Logistics — Pilot" (Proposal/Price Quote, 36000), "Kestrel Credit Union — Renewal" (Negotiation/Review, 48000), "Summit Freight — Expansion" (Qualification, 27000) and "Lakeview Retail — Ecommerce dashboards" (Needs Analysis, 52000) | | Campaign | `701000000000001AAA` | "Ridgeway Robotics Q4 pilot", advertisement, in progress, budget 12000, actual cost 3400 | | Campaign | `701000000000002AAA` | "Healthcare analytics webinar", completed, budget 4000, actual cost 3800 | | CampaignMember | `00v000000000001AAA`, `00v000000000002AAA` | Erin Walsh in the Q4 pilot and Nora Bell in the webinar, both "Responded" | The Helix account is hidden from queries, and direct reads and writes to it or its related records fail. ## Checks Salesforce has no state checks. Use trace checks: ```yaml - id: updated_opportunity trace: { called: "PATCH /services/data/v67.0/sobjects/Opportunity/006000000000001AAA" } - id: read_only trace: { not_called: ["POST /services/data/*", "PATCH /services/data/*", "DELETE /services/data/*"] } ``` ## Limits - Six objects; no Apex, triggers, workflows, lead conversion or assignment rules. - No SOAP, Bulk API, OAuth flows or full Describe metadata. - A simple per-account permission rule, not Salesforce's sharing model. # Attio Source: https://docs.ancha.io/tools/attio A stateful replica of Attio's REST API v2: people, companies and deals with typed values and value history, lists and entries, notes, tasks and workspace members. The Attio replica implements 30 endpoints of the REST API v2 for the standard people, companies and deals objects, with typed attribute values and their history. Its key is `attio`. Attio publishes no official API client, so the replica was tested over HTTP against the shapes in its OpenAPI document. ## Connect | Variable | Value | | --- | --- | | `ATTIO_API_URL` | Base URL. Paths start with `/v2/`. | | `ATTIO_API_KEY` | Fake API key. Send `Authorization: Bearer `. | ```js Node const response = await fetch(process.env.ATTIO_API_URL + '/v2/objects/companies/records/query', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.ATTIO_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ limit: 10 }) }); const companies = await response.json(); ``` This is what `ancha connect attio --lang ts` prints. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `GET` | `/v2/self`, `/v2/objects`, `/v2/objects/:object`, `/v2/:target/:identifier/attributes` | supported | | `POST` | `/v2/objects/:object/records/query` | partial: filter shorthand, operators, value properties, `$and`, `$or` and `$not`; sorts by attribute; path filters through references are gaps | | `POST` | `/v2/objects/:object/records` | supported | | `PUT` | `/v2/objects/:object/records` | partial: asserts by a unique attribute, updating the match (appending to multiselect values) or creating a record | | `GET`, `DELETE` | `/v2/objects/:object/records/:record_id` | supported | | `PATCH` | `/v2/objects/:object/records/:record_id` | supported: multiselect values are added to | | `PUT` | `/v2/objects/:object/records/:record_id` | supported: multiselect values are replaced | | `GET` | `/v2/objects/:object/records/:record_id/attributes/:attribute/values` | supported: the value history | | `GET` | `/v2/objects/:object/records/:record_id/entries` | supported | | `GET` | `/v2/lists`, `/v2/lists/:list` | supported | | `POST` | `/v2/lists/:list/entries/query` | partial: filters and sorts on the list's own attributes | | `POST` | `/v2/lists/:list/entries` | supported | | `PATCH`, `DELETE` | `/v2/lists/:list/entries/:entry_id` | supported | | `GET` | `/v2/workspace_members`, `/v2/workspace_members/:id` | supported | | `GET`, `POST` | `/v2/notes` | supported | | `GET`, `DELETE` | `/v2/notes/:note_id` | supported | | `GET`, `POST` | `/v2/tasks` | supported | | `GET`, `PATCH`, `DELETE` | `/v2/tasks/:task_id` | supported | Anything else returns HTTP 501 with Attio's error shape and `x-ancha-gap: 1`. ## Starter data The `attio-starter` dataset is the workspace **Acme** (`acme`). The API key acts as the member `ancha`; the other members are Tom Hughes and Mike Turner (admins), Laura Kim and Anna Fischer. | Object | Records | | --- | --- | | Companies | Ridgeway Robotics (mid-market, pilot started this week), Pinecrest Health (enterprise, 120 seats from December 1), Bluefield Logistics, Kestrel Credit Union (40 seats), Helix Manufacturing (usage down 20% since August), Summit Freight (35 seats) and Harbor Foods. Custom attributes `segment` (Enterprise, Mid-market, SMB) and `seats`. | | People | Erin Walsh and Paul Young (Ridgeway Robotics), Nora Bell (Pinecrest Health), Grace Lin and Owen Hart (Bluefield Logistics), Carla Diaz and Peter Novak (Kestrel Credit Union), Megan Shaw (Summit Freight) and Lucy Grant (Harbor Foods). | | Deals | "Ridgeway Robotics: analytics rollout" (In Progress, 96,000, Tom), "Pinecrest Health: Enterprise plan" (Won, 144,000), "Bluefield Logistics: pilot" (In Progress, 36,000), "Kestrel Credit Union: renewal" (In Progress, 48,000, Mike) and "Harbor Foods: analytics starter" (Lead, 18,000). Stages: Lead, In Progress, Won 🎉, Lost. | | List "Renewals" | Companies with a renewal stage, date and owner: Kestrel Credit Union (in review, 2026-12-01), Helix Manufacturing (churn risk, 2026-12-15) and Summit Freight (upcoming, 2027-08-01). | | Notes | A discovery call note on Ridgeway Robotics by Tom, and a usage review on Helix Manufacturing by Laura. | | Tasks | Send the SOC 2 Type II report to Grace Lin (Tom, due 2026-11-02); book the quarterly review with Helix (Laura, done); confirm the invoice references PO-77120 (Anna, due 2026-11-03). | ## Checks Attio has no state checks. Use trace checks, for example `{ called: "POST /v2/tasks" }` or `{ not_called: "DELETE /v2/objects/*/records/*" }`. ## Limits - Standard people, companies and deals objects with typed values and value history; custom objects, webhooks, threads, comments and path filters are gaps. - Attio publishes no official API client; tested over HTTP against the shapes in its OpenAPI document. # Gong Source: https://docs.ancha.io/tools/gong A read-only replica of Gong's API v2: calls, extensive call data with CRM context, transcripts, users and workspaces. The Gong replica implements five read-only endpoints of Gong's API v2. Transcripts are seeded text that becomes available on a schedule; nothing is transcribed. Its key is `gong`. ## Connect | Variable | Value | | --- | --- | | `GONG_API_URL` | Base URL. Paths start with `/v2/`. | | `GONG_ACCESS_KEY` | Fake access key. | | `GONG_ACCESS_KEY_SECRET` | Fake access key secret. | Gong uses HTTP Basic authentication with the key and secret, not a Bearer token: ```js Node const auth = Buffer.from(process.env.GONG_ACCESS_KEY + ':' + process.env.GONG_ACCESS_KEY_SECRET).toString('base64'); const response = await fetch(process.env.GONG_API_URL + '/v2/calls?fromDateTime=2026-10-01T00:00:00Z&toDateTime=2026-10-27T00:00:00Z', { headers: { Authorization: 'Basic ' + auth } }); const calls = await response.json(); ``` This is what `ancha connect gong --lang ts` prints. ## Endpoints | Method | Path | Behavior | | --- | --- | --- | | `GET` | `/v2/calls` | Calls in a date range (`fromDateTime`, `toDateTime`, both required), optionally in one `workspaceId`. Web conferences filter by scheduled time. | | `POST` | `/v2/calls/extensive` | Calls by date, workspace, call IDs or primary user, with metadata, parties and CRM context. Speech analysis, trackers, summaries and media are coverage gaps. | | `POST` | `/v2/calls/transcript` | Speaker-attributed transcripts with millisecond offsets, by date, workspace or call IDs. | | `GET` | `/v2/users` | Users in the account, with cursors. | | `GET` | `/v2/workspaces` | Workspaces the credentials may read. | All five are partial. Responses carry `requestId`; paged responses carry `records` and a `cursor` while more pages remain. Dates need a timezone, and time ranges include the start and exclude the end. Unknown routes and fields return HTTP 501 with `x-ancha-gap: 1`. ## Behavior - **Access.** The credentials belong to the Acme account and can read only the Revenue team workspace. Calls in other workspaces or accounts return 404 and are left out of lists. - **Transcript timing.** A transcript can become available after its call. Before then, it is left out of mixed requests, and a request for only unavailable transcripts returns 404. - **Limits.** Pages hold at most 100 records. Requests are limited to 3 per simulated second and 10,000 per simulated day; past that, Gong answers 429 with `Retry-After`. - **CRM context.** Extensive call data links the Salesforce starter records: the Ridgeway Robotics account, Erin Walsh and the rollout opportunity, and the Pinecrest Health, Bluefield Logistics, Kestrel Credit Union and Lakeview Retail accounts and opportunities. It is seeded data; changing Salesforce does not change it. ## Starter data The `gong-starter` dataset is the account **Acme**. | Record | ID | Detail | | --- | --- | --- | | Workspace | `100001` | Revenue team. Readable. | | Workspace | `100002` | Leadership. Not readable. | | User | `700001` | Mike Turner, Account Executive | | User | `700003` | Tom Hughes, Account Executive | | Call | `300001` | "Ridgeway Robotics — analytics discovery", 2026-10-22, 30 minutes, Mike and Erin Walsh. Transcript ready. | | Call | `300002` | "Ridgeway Robotics — pilot planning", 2026-10-26, 20 minutes. Transcript available 2 simulated minutes after the world starts. | | Call | `300004` | "Pinecrest Health — onboarding kickoff", 2026-10-26, 45 minutes, Tom and Nora Bell. | | Call | `300005` | "Bluefield Logistics — security review", 2026-10-20, 30 minutes, Tom and Owen Hart. | | Call | `300006` | "Kestrel Credit Union — renewal check-in", 2026-10-26, 25 minutes, Mike and Carla Diaz. | | Call | `300007` | "Lakeview Retail — discovery", 2026-10-23, 35 minutes, Tom and Rob Fisher. | | Call | `300003` | "Leadership capacity review", private, in the Leadership workspace. No transcript. | A second account, Helix Manufacturing, with its own workspace (`200001`), user (`700002`) and call (`400001`), exists to test that access stays inside the right account. ## Checks Gong is read-only and has no state checks. Use trace checks, for example `{ called: "POST /v2/calls/transcript" }`. ## Limits - Read-only: no uploads, no call creation. - No speech recognition, audio, analysis, trackers or summaries. - No OAuth or credential provisioning. # Stripe Source: https://docs.ancha.io/tools/stripe A stateful replica of the Stripe API in test mode: customers, products and prices, subscriptions, invoices and invoice items, payment intents, charges, refunds and the balance. The Stripe replica implements 34 endpoints of the Stripe API, version `2026-10-28.endive`, in test mode. Its key is `stripe`. It was tested with the `stripe` Node SDK. ## Connect | Variable | Value | | --- | --- | | `STRIPE_API_URL` | Base URL. Paths start with `/v1/`. | | `STRIPE_SECRET_KEY` | Fake test-mode secret key. Send `Authorization: Bearer `. | | `STRIPE_API_HOST_URL` | The same replica as a host name, `-stripe.localhost`, for clients that can only change the host. It resolves only on the computer that runs the world server. | ```js Node import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { maxNetworkRetries: 0, httpClient: Stripe.createFetchHttpClient((input, init) => { const url = new URL(String(input)); return fetch(process.env.STRIPE_API_URL + url.pathname + url.search, init); }) }); const customers = await stripe.customers.list({ limit: 10 }); ``` This is what `ancha connect stripe --lang ts` prints. Stripe's SDK has no base-path option, so the custom fetch client prefixes every request with `STRIPE_API_URL`. That works inside a container and against a hosted workspace too. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `POST` | `/v1/customers` | partial: `invoice_settings` supports `default_payment_method` only; addresses, shipping, tax IDs and cash balances are gaps | | `GET` | `/v1/customers` | supported | | `GET` | `/v1/customers/search` | partial: `query` supports `email`, `name`, `phone`, `metadata["key"]` and `created` with `:`, `~`, `>`, `<`, `-`, `AND` and `OR` | | `GET`, `POST` | `/v1/customers/:id` | supported | | `DELETE` | `/v1/customers/:id` | partial: cancels the customer's subscriptions, as Stripe does | | `GET` | `/v1/products`, `/v1/products/:id`, `/v1/prices`, `/v1/prices/:id` | supported | | `GET` | `/v1/subscriptions` | partial: without `status`, canceled subscriptions are left out, as Stripe does | | `GET`, `DELETE` | `/v1/subscriptions/:id` | supported | | `POST` | `/v1/subscriptions/:id` | partial: `items` (id, price, quantity, deleted, metadata), `cancel_at_period_end`, `metadata` and `description`; prorations become pending invoice items; `always_invoice` is a gap | | `GET` | `/v1/invoices`, `/v1/invoices/:id` | supported | | `POST` | `/v1/invoices` | partial: pending invoice items are included only with `pending_invoice_items_behavior=include`, the default for new API versions | | `POST` | `/v1/invoices/:id` | partial: drafts take description, footer, custom fields, metadata, `auto_advance` and due dates; open invoices the same except due dates; paid and void invoices metadata only | | `POST` | `/v1/invoices/:id/finalize`, `/v1/invoices/:id/void` | supported | | `POST` | `/v1/invoices/:id/pay` | partial: charges the given card or the customer's default; a declined test card answers 402 and the failed attempt stays on the invoice | | `POST` | `/v1/invoiceitems` | partial: `amount` or `pricing[price]` with `quantity`; `price_data`, discounts and tax rates are gaps | | `GET` | `/v1/invoiceitems` | supported | | `POST` | `/v1/payment_intents` | partial: card payments with a saved payment method or a test token such as `pm_card_visa`; `pm_card_chargeDeclined*` tokens decline; 3D Secure, manual capture and other methods are gaps | | `GET` | `/v1/payment_intents`, `/v1/payment_intents/:id` | supported | | `POST` | `/v1/payment_intents/:id/confirm`, `/v1/payment_intents/:id/cancel` | supported | | `GET` | `/v1/charges`, `/v1/charges/:id` | supported | | `POST` | `/v1/refunds` | partial: card refunds settle at once | | `GET` | `/v1/refunds`, `/v1/refunds/:id` | supported | | `GET` | `/v1/balance` | partial: available and pending card funds; payouts and fees are not modeled | Anything else returns HTTP 501 with Stripe's error shape and `x-ancha-gap: 1`. Requests pinned to an API version before `2025-03-31` are coverage gaps: older object shapes are not served. ## Starter data The `stripe-starter` dataset is the test-mode account **Acme**, statement descriptor `ACME ANALYTICS`, with 12,845.50 USD available and 1,200.00 USD pending. | Record | Detail | | --- | --- | | Products and prices | "Acme Team" at 1,200 USD a month (`team_monthly`); "Acme Enterprise" per seat at 100 USD a month (`enterprise_seat_monthly`) or 48,000 USD a year (`enterprise_annual`); "Onboarding package" at 2,500 USD once (`onboarding_once`). Prices carry these lookup keys. | | Customers | Pinecrest Health (Visa ending 4242; 120 seats from December 1), Kestrel Credit Union (Mastercard ending 4444; invoices must reference their PO), Helix Manufacturing (Visa ending 9995, which declines with `insufficient_funds`), Summit Freight (Amex ending 8431) and Ridgeway Robotics (Visa ending 1881, a pilot). | | Subscriptions | Pinecrest: 120 Enterprise seats, trialing until 2026-12-01. Kestrel: Enterprise annual, active, invoiced with 30 days to pay. Helix: Team monthly, past due. Summit: Team monthly, active. | | Invoices | Summit's August, September and October invoices, paid. Helix's October invoice, open after two failed attempts. Kestrel's annual renewal, a draft. Ridgeway's onboarding package, paid. | | Payments and refunds | A 5,000 USD implementation deposit from Pinecrest, and a duplicate 1,200 USD September payment from Summit that was refunded. | Customer and deal records carry `hubspot_deal` metadata that matches the HubSpot starter data. ## Checks Stripe has no state checks. Use trace checks, for example `{ called: "POST /v1/refunds" }` or `{ not_called: "DELETE /v1/subscriptions/*" }`. ## Limits - Test mode only: cards are test tokens or seeded cards; no 3D Secure, payouts, disputes, taxes, coupons or webhooks. - The SDK takes no base path: use the custom fetch client above, or point the host at `STRIPE_API_HOST_URL` on the computer that runs the world. - API versions before `2025-03-31` are coverage gaps. # PayPal Source: https://docs.ancha.io/tools/paypal A stateful replica of PayPal's REST APIs: checkout orders, authorizations, captures and refunds, catalog products, billing plans and subscriptions, and transaction reports. The PayPal replica implements 35 endpoints of PayPal's REST APIs for one business account: checkout orders and payments, catalog products, billing plans and subscriptions, and transaction and balance reports. Its key is `paypal`. It was tested with `@paypal/paypal-server-sdk`. ## Connect | Variable | Value | | --- | --- | | `PAYPAL_API_URL` | Base URL, standing in for `https://api-m.sandbox.paypal.com`. | | `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET` | Fake REST app credentials. Exchange them for an access token at `POST /v1/oauth2/token` with the `client_credentials` grant, as the SDK does. | ```js Node import { Client, Environment, SubscriptionsController } from '@paypal/paypal-server-sdk'; const paypal = new Client({ clientCredentialsAuthCredentials: { oAuthClientId: process.env.PAYPAL_CLIENT_ID!, oAuthClientSecret: process.env.PAYPAL_CLIENT_SECRET! }, environment: Environment.Sandbox, unstable_httpClientOptions: { adapter: async (config: any) => { const headers: Record = Object.fromEntries(Object.entries(config.headers.toJSON()).map(([k, v]) => [k, String(v)])); if (config.auth) headers.authorization = 'Basic ' + Buffer.from(config.auth.username + ':' + config.auth.password).toString('base64'); const res = await fetch(String(config.url).replace(/^https:\/\/[^/]+/, process.env.PAYPAL_API_URL!), { method: config.method.toUpperCase(), headers, body: config.data }); return { data: await res.text(), status: res.status, statusText: res.statusText, headers: Object.fromEntries(res.headers), config, request: {} }; } }, }); const { result } = await new SubscriptionsController(paypal).listSubscriptions({ statuses: 'ACTIVE' }); ``` This is what `ancha connect paypal --lang ts` prints. The server SDK knows only PayPal's own hosts, so the adapter sends each request to the replica and sets the Basic header for the token request itself. Plain `fetch` against `PAYPAL_API_URL` works too. ## Endpoints | Method | Path | Support | | --- | --- | --- | | `POST` | `/v1/oauth2/token` | supported: the `client_credentials` grant | | `POST` | `/v2/checkout/orders` | partial: PayPal wallet orders the payer must approve; card and other payment sources, payee and shipping preferences are gaps | | `GET` | `/v2/checkout/orders/:id` | supported | | `PATCH` | `/v2/checkout/orders/:id` | partial: amount, description, `custom_id`, `invoice_id` and `soft_descriptor` of a purchase unit | | `POST` | `/v2/checkout/orders/:id/capture` | supported: only orders the payer approved | | `POST` | `/v2/checkout/orders/:id/authorize` | supported: only approved orders; the authorization is honored for 3 days and valid for 29 | | `POST` | `/v2/checkout/orders/:id/track` | supported | | `PATCH` | `/v2/checkout/orders/:id/trackers/:tracker` | partial: status, tracking number, carrier and `notify_payer` | | `POST` | `/v2/checkout/orders/:id/confirm-payment-source` | partial: always a gap, since confirming needs a payment source the replica does not model | | `GET` | `/v2/payments/authorizations/:id` | supported | | `POST` | `/v2/payments/authorizations/:id/capture` | supported: up to the authorized amount; `final_capture` defaults to false | | `POST` | `/v2/payments/authorizations/:id/void` | supported | | `POST` | `/v2/payments/authorizations/:id/reauthorize` | partial: always a gap | | `GET` | `/v2/payments/captures/:id` | supported: checkout captures and subscription payments | | `POST` | `/v2/payments/captures/:id/refund` | supported: full or partial, within 180 days; returns the percentage part of the fee | | `GET` | `/v2/payments/refunds/:id` | supported | | `GET` | `/v1/catalogs/products`, `/v1/catalogs/products/:id` | supported | | `GET` | `/v1/billing/plans`, `/v1/billing/plans/:id` | supported | | `POST` | `/v1/billing/plans` | partial: one `REGULAR` fixed-price cycle; trials, tiers, setup fees and taxes are gaps | | `POST` | `/v1/billing/plans/:id/activate`, `/v1/billing/plans/:id/deactivate` | supported | | `GET` | `/v1/billing/subscriptions` | partial: `plan_ids` and `statuses` filters | | `GET` | `/v1/billing/subscriptions/:id` | supported | | `POST` | `/v1/billing/subscriptions` | partial: waits for the payer's approval, which the replica cannot give | | `PATCH` | `/v1/billing/subscriptions/:id` | partial: `custom_id` and the outstanding balance | | `POST` | `/v1/billing/subscriptions/:id/cancel`, `suspend`, `activate` | supported | | `POST` | `/v1/billing/subscriptions/:id/capture` | supported: the outstanding balance; the payment succeeds | | `POST` | `/v1/billing/subscriptions/:id/revise` | partial: always a gap, since a revision needs the payer's approval | | `GET` | `/v1/billing/subscriptions/:id/transactions` | supported | | `GET` | `/v1/reporting/transactions` | partial: date range, ID, event code, status, amount range and currency filters; store and terminal filters are gaps | | `GET` | `/v1/reporting/balances` | supported | Anything else returns HTTP 501 with PayPal's error shape and `x-ancha-gap: 1`. ## Starter data The `paypal-starter` dataset is the business account **Acme Inc.** (`billing@acme.example`, USD) with an opening balance of 18,240.00 USD. | Record | Detail | | --- | --- | | Product and plans | "Acme Analytics", with the plans "Team (monthly)" at 49.00 USD a seat, "Team (annual)" at 490.00 USD a seat, and a retired "Starter (monthly)". | | Subscriptions | Lakeview Retail (5 seats, active), Redwood Labs (3 seats, suspended after two failed payments), Harbor Foods (annual, active) and Summit Freight (cancelled, moved to annual invoicing). | | Orders | Kestrel Credit Union's admin training (completed, one session refunded), Bluefield Logistics' onboarding workshop (authorized and completed), Pinecrest Health's implementation package (completed), Ridgeway Robotics' pilot extension (approved an hour ago and ready to capture) and Helix Manufacturing's replacement gateway (created, waiting for the payer). | ## Checks PayPal has no state checks. Use trace checks, for example `{ called: "POST /v2/checkout/orders/*/capture" }` or `{ not_called: "POST /v2/payments/captures/*/refund" }`. ## Limits - Exchange the client ID and secret at `PAYPAL_API_URL/v1/oauth2/token`. The server SDK knows only PayPal's hosts: call `PAYPAL_API_URL` over HTTP, or give the SDK the adapter above. - Payers approve orders and subscriptions in their browser, which the replica cannot do: new ones wait for approval, and seeded approved orders can be captured. Invoicing, disputes, payouts and webhooks are gaps. # Shopify Source: https://docs.ancha.io/tools/shopify A stateful replica of the Shopify Admin GraphQL API: orders, products and variants, inventory, customers, fulfillments and refunds for one store. The Shopify replica serves 32 operations of the Admin GraphQL API, version `2026-10`, for one store: 16 queries and 16 mutations over orders, products, inventory, customers, fulfillments and refunds. Its key is `shopify`. It was tested with `@shopify/admin-api-client`. ## Connect | Variable | Value | | --- | --- | | `SHOPIFY_API_URL` | Base URL. The GraphQL endpoint is `SHOPIFY_API_URL/admin/api/2026-10/graphql.json`. | | `SHOPIFY_ACCESS_TOKEN` | Fake Admin API access token. Send it as `X-Shopify-Access-Token`. | | `SHOPIFY_API_VERSION` | `2026-10`. | ```js Node import { createAdminApiClient } from '@shopify/admin-api-client'; const shopify = createAdminApiClient({ storeDomain: 'acme-store.myshopify.com', apiVersion: process.env.SHOPIFY_API_VERSION!, accessToken: process.env.SHOPIFY_ACCESS_TOKEN!, customFetchApi: (url, init) => fetch(String(url).replace(/^https:\/\/[^/]+/, process.env.SHOPIFY_API_URL!), init) }); const { data, errors } = await shopify.request('{ orders(first: 10) { nodes { id name } } }'); ``` This is what `ancha connect shopify --lang ts` prints. The client always calls `https://.myshopify.com`, so its `customFetchApi` swaps that origin for `SHOPIFY_API_URL`. ## Operations | Kind | Operation | Support | | --- | --- | --- | | query | `shop`, `order`, `product`, `productVariants`, `productVariant`, `customers`, `customer`, `locations`, `location`, `inventoryItem`, `fulfillmentOrder`, `node`, `nodes`, `job` | supported | | query | `orders` | partial: Shopify's search syntax over the documented filters; undocumented ones are ignored with a warning; oldest first by default, as in Shopify | | query | `products` | partial: only `ACTIVE` products unless the query filters `status`, per Shopify's documented default | | mutation | `tagsAdd`, `tagsRemove`, `orderMarkAsPaid`, `orderClose`, `orderOpen`, `fulfillmentTrackingInfoUpdate`, `customerCreate` | supported | | mutation | `orderUpdate` | partial: metafields and localized fields are gaps | | mutation | `orderCancel` | partial: restock and refund to the original payment method; store credit is a gap | | mutation | `refundCreate` | partial: money moves only through `REFUND` transactions; duties and store credit are gaps; `@idempotent` required from 2026-04 | | mutation | `fulfillmentCreate` | partial: one fulfillment order per order, at its assigned location | | mutation | `productUpdate` | partial: title, handle, description, vendor, type, status and tags; SEO, metafields, media and collections are gaps | | mutation | `productVariantsBulkUpdate` | partial: prices, barcode, taxable, inventory policy, SKU, tracking and cost; option values and media are gaps | | mutation | `inventoryAdjustQuantities`, `inventorySetQuantities` | partial: `changeFromQuantity` and `@idempotent` required from 2026-04 | | mutation | `customerUpdate` | partial: marketing consent, addresses and metafields are gaps | Fields and operations outside this list answer as coverage gaps in GraphQL's error shape. Query cost is an estimate of Shopify's. ## Starter data The `shopify-starter` dataset is **Acme Store** (`acme-store`), owned by Ben Carter, in USD and America/Los_Angeles, with orders numbered from 1001. | Record | Detail | | --- | --- | | Locations | "Oakland warehouse" (fulfills online orders) and "Reno overflow". | | Products | "Acme Edge Gateway" (Ethernet 349.00 and LTE 449.00; the LTE variant has two units left), "Sensor Tag 10-pack" (89.00), "Wall Mount Kit" (29.00), "Acme Hoodie" (S, M and L; M is out of stock) and a draft "Acme Edge Gateway Pro" (649.00). Variants carry SKUs, costs and inventory per location. | | Customers | Grace Lin (Bluefield Logistics), Carla Diaz (Kestrel Credit Union, net terms), Derek Moss (Helix Manufacturing, disabled), Lucy Grant (Harbor Foods), Nora Bell (Pinecrest Health), Megan Shaw (Summit Freight) and Erin Walsh (Ridgeway Robotics, pilot). | | Orders | Eight: delivered orders for Bluefield Logistics and Kestrel Credit Union; a Helix order cancelled after two declined payments; a Harbor Foods order refunded for a cracked housing; a Pinecrest order partly fulfilled (sensors shipped, mounts not); a Kestrel order of two gateways waiting for a bank transfer; a Summit Freight next-day order to ship to the Denver dock; and Ridgeway Robotics' pilot order of five gateways and mounts placed three hours ago, paid and unfulfilled. | ## Checks Shopify has no state checks. Every operation goes through one GraphQL endpoint, so a trace check names that path; run `ancha explain` or `ancha world trace` once to see how the replica records the agent's requests before you write one. ## Limits - The endpoint is `SHOPIFY_API_URL/admin/api/2026-10/graphql.json` with the `X-Shopify-Access-Token` header. The official client always calls `https://.myshopify.com`: give it a `customFetchApi` that swaps that origin for `SHOPIFY_API_URL`. - The REST Admin API, webhooks, discounts, draft orders, returns and metafields are gaps. # Lob Source: https://docs.ancha.io/tools/lob A stateful replica of Lob's Print & Mail API v1: addresses, letters, postcards and checks, bank accounts, templates and US address verification, where nothing is printed or mailed. The Lob replica implements 28 endpoints of the Print & Mail API v1. Nothing is printed or mailed: tracking follows the scene clock, and mail to an address the postal directory does not know, or a vacant one, comes back. Its key is `lob`. It was tested with `@lob/lob-typescript-sdk`. ## Connect | Variable | Value | | --- | --- | | `LOB_API_URL` | Base URL, ending in `/v1`. Pass it as the SDK's `basePath`. | | `LOB_API_KEY` | Fake test API key. Lob uses HTTP Basic with the key as the username and an empty password. | | `LOB_ASSETS_URL` | Where proof files are served. Proof links in responses name `https://lob-assets.com`, as Lob's do; replace that host with this URL to fetch them. | ```js Node import { Configuration, LettersApi } from '@lob/lob-typescript-sdk'; const config = new Configuration({ username: process.env.LOB_API_KEY, basePath: process.env.LOB_API_URL, baseOptions: { proxy: false } }); const letters = await new LettersApi(config).list(10); // A proof PDF: await fetch(letters.data![0].url!.replace('https://lob-assets.com', process.env.LOB_ASSETS_URL!)) ``` This is what `ancha connect lob --lang ts` prints. The SDK rejects proof links on any host but `lob-assets.com`, which is why the replica returns those and serves the files under `LOB_ASSETS_URL`. ## Endpoints All 28 endpoints are partial. Anything else returns HTTP 501 with Lob's error shape and `x-ancha-gap: 1`. Lists take `limit`, `before` and `after` cursors, `include=total_count`, and `date_created` and `metadata` filters; mail lists also take `scheduled`, `send_date`, `mail_type` and `sort_by`. | Method | Path | Notes | | --- | --- | --- | | `GET`, `POST` | `/v1/addresses` | US addresses are upper-cased and standardized against the postal directory. | | `GET`, `DELETE` | `/v1/addresses/{id}` | | | `GET`, `POST` | `/v1/letters` | HTML, a template, a file URL (recorded, never fetched) or an uploaded PDF; certified and registered mail; `Idempotency-Key`. | | `GET`, `DELETE` | `/v1/letters/{id}` | Cancel until production starts: the cancellation window, or a scheduled send date. | | `GET`, `POST` | `/v1/postcards` | 4x6, 6x9 and 6x11; front and back as HTML, template, URL or upload. | | `GET`, `DELETE` | `/v1/postcards/{id}` | | | `GET`, `POST` | `/v1/checks` | From a verified bank account; check numbers advance per account; no money moves. | | `GET`, `DELETE` | `/v1/checks/{id}` | | | `GET`, `POST` | `/v1/bank_accounts` | New accounts start unverified. | | `GET`, `DELETE` | `/v1/bank_accounts/{id}` | | | `POST` | `/v1/bank_accounts/{id}/verify` | With the two micro-deposit amounts in cents. | | `GET`, `POST` | `/v1/templates` | `{{variable}}` merge fields with `#if` and `#unless` blocks. | | `GET`, `POST`, `DELETE` | `/v1/templates/{id}` | Update the description and the published version. | | `POST` | `/v1/us_verifications` | Against the world's postal directory, or any complete address when the scene has none; `case=upper` or `proper`. | | `POST` | `/v1/bulk/us_verifications` | Up to 20 addresses. | ## Starter data The `lob-starter` dataset is Acme's account, with a five-minute cancellation window and a postal directory of eight commercial addresses, one of them vacant. | Record | Detail | | --- | --- | | Addresses | Acme's billing return address (Anna Fischer, San Francisco); Helix Manufacturing's accounts payable at an old Akron address, which the directory marks vacant, and a new one added three hours ago; Summit Freight billing (Reno); Erin Walsh at Ridgeway Robotics (Pittsburgh); Pinecrest Health accounts payable and Nora Bell (Portland); Kestrel Credit Union procurement (Madison); Grace Lin at Bluefield Logistics (Baltimore). | | Bank accounts | "Acme operating account", verified, and "Acme refunds account", unverified, with micro-deposits of 17 and 42 cents. | | Templates | An invoice cover letter and the front and back of a welcome postcard, with `first_name` and `company` merge fields. | | Letters | Helix Manufacturing's October invoice, sent five days ago to the vacant address, and Pinecrest Health's December invoice, scheduled 23 days ahead. | | Postcards | Welcome postcards to Erin Walsh and Nora Bell. | | Checks | A refund check to Summit Freight for a duplicate September charge, canceled. | ## Checks Lob has no state checks. Use trace checks, for example `{ called: "POST /v1/letters" }` or `{ not_called: "POST /v1/checks" }`. ## Limits - Nothing is printed or mailed: USPS tracking follows the scene clock, and mail to an address the postal directory does not know, or a vacant one, comes back. - Pass `LOB_API_URL` as the SDK's `basePath` and `LOB_API_KEY` as its username. Fetch proof links under `LOB_ASSETS_URL`. - US verification consults the world's postal directory. File URLs are recorded, never fetched; no thumbnails, self-mailers, campaigns, international verification or webhooks. # Google Analytics Source: https://docs.ancha.io/tools/google-analytics A replica of the Google Analytics 4 Data API v1beta over REST: run reports, batch reports, metadata and compatibility checks over historical traffic. The Google Analytics replica implements four REST operations of the GA4 Data API v1beta over fixed historical traffic. Its key is `ga4`. It was tested with `@googleapis/analyticsdata`, Google's generated REST client. ## Connect | Variable | Value | | --- | --- | | `GA4_API_URL` | REST root. Paths start with `/v1beta/properties/`. | | `GA4_ACCESS_TOKEN` | Fake OAuth token. Send `Authorization: Bearer `. | ```js Node const response = await fetch(process.env.GA4_API_URL + '/v1beta/properties/345678901:runReport', {method:'POST', headers:{Authorization:'Bearer '+process.env.GA4_ACCESS_TOKEN,'Content-Type':'application/json'}, body:JSON.stringify({dateRanges:[{startDate:'2026-10-23',endDate:'2026-10-25'}],dimensions:[{name:'sessionSource'}],metrics:[{name:'sessions'}]})}); const report = await response.json(); ``` This is what `ancha connect ga4 --lang ts` prints. With `@googleapis/analyticsdata`, pass `rootUrl: GA4_API_URL + '/'` and the Bearer header on each request, and turn retries off. gRPC clients are not supported. ## Endpoints | Method | Path | Behavior | | --- | --- | --- | | `POST` | `/v1beta/properties/:property:runReport` | Dimensions, metrics, date ranges, filters, ordering, offset and limit, totals, and optional quota state. | | `POST` | `/v1beta/properties/:property:batchRunReports` | One to five reports for one property. | | `GET` | `/v1beta/properties/:property/metadata` | The supported dimensions and metrics. `properties/0/metadata` returns the common set. | | `POST` | `/v1beta/properties/:property:checkCompatibility` | Compatibility within the supported fields. | All four are partial. Pivots, cohorts, comparisons, metric expressions and fields outside the list below are coverage gaps. | Dimensions | Metrics | | --- | --- | | `date`, `country`, `city`, `deviceCategory`, `sessionSource`, `sessionMedium`, `sessionSourceMedium`, `sessionCampaignName`, `sessionDefaultChannelGroup`, `eventName` | `activeUsers`, `totalUsers`, `sessions`, `engagedSessions`, `engagementRate`, `bounceRate`, `eventCount`, `keyEvents`, `totalRevenue` | Invalid credentials return 401 `UNAUTHENTICATED`; a property the token cannot read returns 403 `PERMISSION_DENIED`. Quota limits are 200,000 tokens a day, 40,000 an hour and 14,000 per project an hour. Ask for the current state with `returnPropertyQuota: true`. ## Starter data The `ga4-starter` dataset has two properties. | Property | Name | Access | | --- | --- | --- | | `345678901` | Acme — marketing (America/Los_Angeles, USD) | Readable | | `987654321` | Helix Manufacturing | Not readable | Acme's traffic runs from 2026-10-19 to 2026-10-26: nineteen sessions from LinkedIn paid social ("Ridgeway Robotics Q4 pilot"), Google paid search ("Pipeline reporting" and "Warehouse analytics") and Google organic search, mostly from San Francisco and one from Toronto, with `generate_lead` as the key event. The 2026-10-26 session becomes visible 2 simulated minutes after the world starts, as recent GA4 data arrives late. ## Checks Google Analytics is read-only and has no state checks. Use trace checks, for example `{ called: "POST /v1beta/properties/345678901:runReport" }`. ## Limits - Historical, synthetic data only: no Measurement Protocol, live collection or realtime reports. - No attribution models and no Admin API. # Google Ads Source: https://docs.ancha.io/tools/google-ads A stateful replica of the Google Ads API v26 over REST and gRPC: accounts, GAQL search, and mutations for budgets, search campaigns, ad groups, responsive search ads and keywords. The Google Ads replica implements API v26 over both REST and gRPC, on the same data. It covers GAQL search and mutations for search campaigns with manual CPC. Its key is `google-ads`. It was tested with the official Python client, `google-ads` 34.0.0, over gRPC. ## Connect | Variable | Value | | --- | --- | | `GOOGLE_ADS_API_URL` | REST base URL. Paths start with `/v26/`. | | `GOOGLE_ADS_ACCESS_TOKEN` | Fake OAuth access token. Send `Authorization: Bearer `. | | `GOOGLE_ADS_GRPC_ENDPOINT` | Local gRPC endpoint for the official clients. | | `GOOGLE_ADS_TLS_ROOT_CERT_BASE64` | Base64 PEM certificate the gRPC client must trust for the local endpoint. | Send `login-customer-id: 1000000000`, the manager account, to reach the client account. No developer token is needed. ```js Node (REST) const response = await fetch(process.env.GOOGLE_ADS_API_URL + '/v26/customers/1000000001/googleAds:search', {method:'POST',headers:{Authorization:'Bearer '+process.env.GOOGLE_ADS_ACCESS_TOKEN,'login-customer-id':'1000000000','Content-Type':'application/json'},body:JSON.stringify({query:'SELECT campaign.id, campaign.name, campaign.status FROM campaign'})}); const campaigns = await response.json(); ``` ```python Python (gRPC) import base64, os, tempfile # Set before the gRPC client initializes; certificate is for local mock transport only. root = tempfile.NamedTemporaryFile(suffix='.pem') root.write(base64.b64decode(os.environ['GOOGLE_ADS_TLS_ROOT_CERT_BASE64'])) root.flush() os.environ['GRPC_DEFAULT_SSL_ROOTS_FILE_PATH'] = root.name from google.ads.googleads.client import GoogleAdsClient from google.oauth2.credentials import Credentials client = GoogleAdsClient(credentials=Credentials(token=os.environ['GOOGLE_ADS_ACCESS_TOKEN']), endpoint=os.environ['GOOGLE_ADS_GRPC_ENDPOINT'], login_customer_id='1000000000', version='v26', use_proto_plus=True) rows = client.get_service('GoogleAdsService').search(customer_id='1000000001', query='SELECT campaign.id, campaign.name, campaign.status FROM campaign') for row in rows: print(row.campaign.name) # Tested with google-ads==34.0.0. Keep root alive while the client is in use. ``` These are the snippets `ancha connect google-ads --lang ts` and `--lang py` print. ## Endpoints | Method | Path | | --- | --- | | `GET` | `/v26/customers:listAccessibleCustomers` | | `POST` | `/v26/customers/:customerId/googleAds:search` | | `POST` | `/v26/customers/:customerId/googleAds:searchStream` | | `POST` | `/v26/customers/:customerId/campaignBudgets:mutate` | | `POST` | `/v26/customers/:customerId/campaigns:mutate` | | `POST` | `/v26/customers/:customerId/adGroups:mutate` | | `POST` | `/v26/customers/:customerId/adGroupAds:mutate` | | `POST` | `/v26/customers/:customerId/adGroupCriteria:mutate` | | `POST` | `/v26/customers/:customerId/googleAds:mutate` | Each REST route has the matching gRPC method, such as `GoogleAdsService.Search` and `CampaignService.MutateCampaigns`. `googleAds:mutate` exists only to report that cross-resource mutations and temporary IDs are not implemented. Unknown REST routes return Google's error shape with `x-ancha-gap: 1`; unknown gRPC methods return `UNIMPLEMENTED`. ## Behavior - **GAQL.** `SELECT ... FROM` one of `customer`, `customer_client`, `campaign_budget`, `campaign`, `ad_group`, `ad_group_ad`, `ad_group_criterion`. `WHERE` predicates joined with AND (`=`, `!=`, `<`, `>`, `<=`, `>=`, `IN`, `BETWEEN`), `segments.date DURING` `TODAY`, `YESTERDAY`, `LAST_7_DAYS`, `LAST_14_DAYS` or `LAST_30_DAYS`, `ORDER BY` and `LIMIT`. No OR, LIKE, functions or REGEXP. - **Search pages** hold up to 10,000 rows; `pageSize` is rejected, as in v26. - **Mutations** cover budgets, SEARCH campaigns with manual CPC, ad groups, responsive search ads and keywords (EXACT, PHRASE, BROAD). They are atomic by default and support `partialFailure`, `validateOnly` and `responseContentType`. - **Review.** New ads stay `REVIEW_IN_PROGRESS`. Nothing serves, bills or delivers. - **Metrics** (`impressions`, `clicks`, `cost_micros`, `conversions`) come only from the historical starter rows. ## Starter data The `google-ads-starter` dataset belongs to Acme. | Record | ID | Detail | | --- | --- | --- | | Manager account | `1000000000` | Acme manager. The token is granted this account. | | Client account | `1000000001` | Acme US, USD, America/Los_Angeles, under the manager. | | Account | `1000000002` | Acme Europe (restricted), EUR. Not reachable with the token. | | Budget | `2001` | "Enterprise search daily budget", 150 USD a day (`150000000` micros). | | Campaign | `3001` | "Ridgeway Robotics · enterprise intent", SEARCH, manual CPC, enabled. | | Ad group | `4001` | "Robotics operations buyers", CPC bid 6.50 USD. | | Ad | `4001~5001` | A responsive search ad for `https://acme.example/ridgeway`, approved. | | Keyword | `4001~6001` | "robotics operations software", phrase match. | Reports cover 2026-10-24 and 2026-10-25 for campaign `3001`. ## Checks Google Ads has no state checks. Use trace checks, for example `{ called: "POST /v26/customers/1000000001/campaigns:mutate" }`. Trace paths for gRPC calls use the full RPC name. ## Limits - Worlds with Google Ads cannot run in playgrounds, and `ancha run` rejects them with container execution (`CONFIG_INVALID`). Use them with `ancha run` as a local process or in a sandbox. - Search campaigns with manual CPC only: no Performance Max, display, video, smart bidding, audiences, assets, conversion uploads or recommendations. - No auction, serving or spend simulation. # LinkedIn Ads Source: https://docs.ancha.io/tools/linkedin-ads A stateful replica of LinkedIn's Marketing API version 202610: ad accounts, campaign groups, campaigns, creatives, targeting facets and campaign analytics. The LinkedIn Ads replica implements 17 endpoints of the Marketing API, version 202610, with Rest.li 2.0 conventions. Its key is `linkedin-ads`. ## Connect | Variable | Value | | --- | --- | | `LINKEDIN_ADS_API_URL` | Base URL. Paths start with `/rest/`. | | `LINKEDIN_ADS_TOKEN` | Fake OAuth token. Send `Authorization: Bearer `. | Every request also needs `LinkedIn-Version: 202610` and `X-Restli-Protocol-Version: 2.0.0`, and writes need `Content-Type: application/json`. ```js Node const response = await fetch(process.env.LINKEDIN_ADS_API_URL + '/rest/adAccounts?q=search', { headers: { Authorization: 'Bearer ' + process.env.LINKEDIN_ADS_TOKEN, 'LinkedIn-Version': '202610', 'X-Restli-Protocol-Version': '2.0.0' } }); const accounts = await response.json(); ``` This is what `ancha connect linkedin-ads --lang ts` prints. ## Endpoints All endpoints are partial. Any other route returns HTTP 501 with `x-ancha-gap: 1`. ### Accounts | Method | Path | | --- | --- | | `GET` | `/rest/adAccounts` | | `GET` | `/rest/adAccounts/:accountId` | ### Campaign groups | Method | Path | | --- | --- | | `GET` | `/rest/adAccounts/:accountId/adCampaignGroups` | | `GET` | `/rest/adAccounts/:accountId/adCampaignGroups/:id` | | `POST` | `/rest/adAccounts/:accountId/adCampaignGroups` | | `POST` | `/rest/adAccounts/:accountId/adCampaignGroups/:id` | ### Campaigns | Method | Path | | --- | --- | | `GET` | `/rest/adAccounts/:accountId/adCampaigns` | | `GET` | `/rest/adAccounts/:accountId/adCampaigns/:id` | | `POST` | `/rest/adAccounts/:accountId/adCampaigns` | | `POST` | `/rest/adAccounts/:accountId/adCampaigns/:id` | ### Creatives | Method | Path | | --- | --- | | `GET` | `/rest/adAccounts/:accountId/creatives` | | `GET` | `/rest/adAccounts/:accountId/creatives/:id` | | `POST` | `/rest/adAccounts/:accountId/creatives` | | `POST` | `/rest/adAccounts/:accountId/creatives/:id` | ### Targeting and analytics | Method | Path | | --- | --- | | `GET` | `/rest/adTargetingFacets` | | `GET` | `/rest/adTargetingEntities` | | `GET` | `/rest/adAnalytics` | ## Behavior - **Creating** returns an empty 201 response with the new ID in `x-restli-id`. - **Updating** is a `POST` to the record with `X-RestLi-Method: PARTIAL_UPDATE` and a body of `{"patch": {"$set": {...}}}`. It returns an empty 204. Read the record again to see the result. - **Finders** take `q=search` (accounts, campaign groups, campaigns) or `q=criteria` (creatives) with a bounded `search` expression, cursor pagination and `fields` projection. Batch gets report per-ID errors. - **Creatives** reference the one seeded single-image post. New creatives stay pending review; nothing uploads an image or creates a post. - **Analytics** support the `CAMPAIGN` pivot with `DAILY` or `ALL` granularity over an explicit date range, from historical rows. To pause campaign 701001: ```bash curl -X POST "$LINKEDIN_ADS_API_URL/rest/adAccounts/501001/adCampaigns/701001" \ -H "Authorization: Bearer $LINKEDIN_ADS_TOKEN" \ -H "LinkedIn-Version: 202610" -H "X-Restli-Protocol-Version: 2.0.0" \ -H "X-RestLi-Method: PARTIAL_UPDATE" -H "Content-Type: application/json" \ -d '{"patch":{"$set":{"status":"PAUSED"}}}' ``` ## Starter data The `linkedin-ads-starter` dataset belongs to Acme. | Record | ID | Detail | | --- | --- | --- | | Ad account | `501001` | "Acme · US growth", USD. The token manages it. | | Ad account | `501002` | "Acme · Europe (restricted)", EUR. Not accessible. | | Campaign group | `601001` | "Q4 enterprise pipeline", active, total budget 12000 USD. | | Campaign | `701001` | "Ridgeway Robotics · operations leaders", active, targeting Ridgeway Robotics employees in the United States. | | Campaign | `701002` | "Enterprise playbook · retest", paused. | | Creative | `urn:li:sponsoredCreative:901001` | "Ridgeway operations guide", active and approved. | | Creative | `urn:li:sponsoredCreative:901002` | "Pipeline guide revision", active, review pending. | Both creatives reference one sponsored post, "A clearer operations pipeline for Ridgeway". Daily analytics cover 2026-10-24 and 2026-10-25, for example 14,200 impressions, 117 clicks and 132.40 USD for campaign 701001 on 2026-10-24. ## Checks LinkedIn Ads has no state checks. Use trace checks, for example `{ called: "POST /rest/adAccounts/501001/adCampaigns/701001" }`. ## Limits - Single-image sponsored content only; no image upload, video, lead forms or audiences. - Historical analytics only; no auction or delivery. # ChatGPT Ads Source: https://docs.ancha.io/tools/chatgpt-ads A stateful replica of the ChatGPT Ads Advertiser API v1: the ad account, campaigns, ad groups, ads with creative review, status changes and delivery insights. The ChatGPT Ads replica implements 26 endpoints of the Advertiser API v1. It is the advertising API, not the OpenAI model API. Its key is `chatgpt-ads`. ## Connect | Variable | Value | | --- | --- | | `OPENAI_ADS_API_URL` | Base URL. Paths start with `/v1/`. | | `OPENAI_ADS_API_KEY` | Fake advertiser key for one ad account. Send `Authorization: Bearer `. It is not an OpenAI model key. | ```js Node const response = await fetch(process.env.OPENAI_ADS_API_URL + '/v1/campaigns', {headers:{Authorization:'Bearer '+process.env.OPENAI_ADS_API_KEY}}); const campaigns = await response.json(); ``` This is what `ancha connect chatgpt-ads --lang ts` prints. Creates accept an `idempotency-key` header. ## Endpoints All endpoints are partial. Unsupported methods, routes and fields return HTTP 501 with `x-ancha-gap: 1`. ### Ad account | Method | Path | | --- | --- | | `GET` | `/v1/ad_account` | ### Campaigns | Method | Path | | --- | --- | | `GET` | `/v1/campaigns` | | `POST` | `/v1/campaigns` | | `GET` | `/v1/campaigns/:id` | | `POST` | `/v1/campaigns/:id` | | `POST` | `/v1/campaigns/:id/activate` | | `POST` | `/v1/campaigns/:id/pause` | | `POST` | `/v1/campaigns/:id/archive` | ### Ad groups | Method | Path | | --- | --- | | `GET` | `/v1/ad_groups` | | `POST` | `/v1/ad_groups` | | `GET` | `/v1/ad_groups/:id` | | `POST` | `/v1/ad_groups/:id` | | `POST` | `/v1/ad_groups/:id/activate` | | `POST` | `/v1/ad_groups/:id/pause` | | `POST` | `/v1/ad_groups/:id/archive` | ### Ads | Method | Path | | --- | --- | | `GET` | `/v1/ads` | | `POST` | `/v1/ads` | | `GET` | `/v1/ads/:id` | | `POST` | `/v1/ads/:id` | | `POST` | `/v1/ads/:id/activate` | | `POST` | `/v1/ads/:id/pause` | | `POST` | `/v1/ads/:id/archive` | ### Insights | Method | Path | | --- | --- | | `GET` | `/v1/ad_account/insights` | | `GET` | `/v1/campaigns/:id/insights` | | `GET` | `/v1/ad_groups/:id/insights` | | `GET` | `/v1/ads/:id/insights` | ## Behavior - **Lists** are scoped to the account, with an exact name filter and forward and backward ID cursors. - **Updates** replace budgets; archiving is permanent and archived objects cannot change. - **Review.** New ads are reviewed by a local rule and approved 60 simulated seconds after creation. - **Insights** are historical hourly delivery, grouped by none or by day in the account's time zone. - **Rate limits.** 600 requests a minute per endpoint and 1,200 per account, in simulated time. ## Starter data The `chatgpt-ads-starter` dataset is the ad account **Acme** (`adacct_acme`, USD, America/Los_Angeles). | Record | ID | Detail | | --- | --- | --- | | Campaign | `cmpn_acme_pilot` | "Ridgeway Robotics Q4 pilot", active, 50 USD a day. | | Ad group | `adgrp_acme_revenue` | "Revenue operations", active. | | Ad | `ad_acme_reporting` | "Pipeline clarity", active, approved. | | Ad | `ad_acme_pending` | "Pilot invitation", active, in review. | Hourly insights exist for "Pipeline clarity", for example 1,200 impressions and 36 clicks in one hour. A second account, Helix Manufacturing (`adacct_helix`), with a paused campaign, exists to test that the key stays inside its own account. ## Checks ChatGPT Ads has no state checks. Use trace checks, for example `{ called: "POST /v1/campaigns/cmpn_acme_pilot/pause" }`. ## Limits - Single-image chat cards only; no file uploads. - No auction, spend, destination crawling or conversion attribution. # Open-Meteo Source: https://docs.ancha.io/tools/open-meteo A replica of Open-Meteo's forecast and geocoding API with a synthetic climate for ten seeded places. No API key. The Open-Meteo replica implements the forecast and geocoding endpoints of Open-Meteo's v1 API. Weather comes from a synthetic climate for each seeded place, driven by the world's simulated clock. Its key is `open-meteo`. ## Connect | Variable | Value | | --- | --- | | `OPEN_METEO_API_URL` | Base URL for `/v1/forecast`. | | `OPEN_METEO_GEOCODING_URL` | Base URL for `/v1/search`. The real service hosts geocoding separately; the replica serves both from one URL. | Open-Meteo needs no key. `ancha connect open-meteo` has no snippet in 0.1.1; call the API directly: ```js Node const url = new URL(process.env.OPEN_METEO_API_URL.replace(/\/+$/, '') + '/v1/forecast'); url.search = new URLSearchParams({ latitude: '37.77', longitude: '-122.42', current: 'temperature_2m,wind_speed_10m' }); const forecast = await (await fetch(url)).json(); ``` ## Endpoints | Method | Path | Behavior | | --- | --- | --- | | `GET` | `/v1/forecast` | Current, hourly and daily variables for one location. The nearest seeded place answers for any coordinate. | | `GET` | `/v1/search` | Places by name, from the seeded places only. | Both are partial. Units are metric and times are ISO 8601. Daily variables need `timezone` (`auto` or an IANA name), as on the real API; without it the request fails with HTTP 400. Forecasts reach 16 days ahead and 7 days back. Historical archives and marine and air-quality data are not implemented and return HTTP 501 with `x-ancha-gap: 1`. ## Starter data The `open-meteo-starter` dataset has ten places, each with its own climate: | Place | ID | Country | Time zone | | --- | --- | --- | --- | | San Francisco | `5391959` | United States | America/Los_Angeles | | New York | `5128581` | United States | America/New_York | | London | `2643743` | United Kingdom | Europe/London | | Berlin | `2950159` | Germany | Europe/Berlin | | Tokyo | `1850147` | Japan | Asia/Tokyo | | Sydney | `2147714` | Australia | Australia/Sydney | | Portland | `5746545` | United States | America/Los_Angeles | | Chicago | `4887398` | United States | America/Chicago | | Toronto | `6167865` | Canada | America/Toronto | | Singapore | `1880252` | Singapore | Asia/Singapore | ## Checks Open-Meteo is read-only and has no state checks. Use trace checks, for example `{ called: "GET /v1/forecast" }`. ## Limits - One location per request. - Synthetic weather; it does not match real observations. - Open-Meteo is not part of a preset. Add it with `--tools open-meteo` or `twins: [open-meteo]`. # Kalshi Source: https://docs.ancha.io/tools/kalshi A stateful replica of Kalshi's Trade API v2: series, events and markets, order books, orders and fills, positions and settlements for one trading account. The Kalshi replica implements 25 endpoints of the Trade API v2 for one trading account. Orders trade against other traders' resting bids seeded with the scene, with Kalshi's fee, and no money moves. Its key is `kalshi`. It was tested with `kalshi-typescript`. ## Connect | Variable | Value | | --- | --- | | `KALSHI_API_URL` | Base URL, ending in `/trade-api/v2`. Pass it as the SDK's `basePath`. | | `KALSHI_API_KEY_ID` | Fake API key ID (`apiKey` in the SDK). | | `KALSHI_PRIVATE_KEY` | Fake RSA private key in PEM form (`privateKeyPem` in the SDK). Requests are signed with RSA-PSS over the timestamp, method and path, as on Kalshi. | ```js Node import { Configuration, MarketApi, PortfolioApi } from 'kalshi-typescript'; const config = new Configuration({ apiKey: process.env.KALSHI_API_KEY_ID, privateKeyPem: process.env.KALSHI_PRIVATE_KEY, basePath: process.env.KALSHI_API_URL, baseOptions: { proxy: false } }); const { data } = await new PortfolioApi(config).getBalance(); const { data: markets } = await new MarketApi(config).getMarkets(100); ``` This is what `ancha connect kalshi --lang ts` prints. The timestamp's age is not checked. ## Endpoints All 25 endpoints are partial. Anything else returns HTTP 501 with `x-ancha-gap: 1`. | Method | Path | Notes | | --- | --- | --- | | `GET` | `/trade-api/v2/exchange/status` | | | `GET` | `/trade-api/v2/exchange/schedule` | Open around the clock; no maintenance windows. | | `GET` | `/trade-api/v2/series` | `category` and `tags` filters. | | `GET` | `/trade-api/v2/series/{series_ticker}` | | | `GET` | `/trade-api/v2/events` | `status`, `series_ticker`, `tickers` and `with_nested_markets`; cursor pagination. | | `GET` | `/trade-api/v2/events/{event_ticker}` | | | `GET` | `/trade-api/v2/markets` | Event, series, status, tickers and close, created and settled time filters; cursor pagination. | | `GET` | `/trade-api/v2/markets/trades` | The public tape, including the account's own executions. | | `GET` | `/trade-api/v2/markets/{ticker}` | Quotes from the order book; status follows the scene clock (active, closed, finalized). | | `GET` | `/trade-api/v2/markets/{ticker}/orderbook` | Yes and No bids as fixed-point dollars and counts, with depth. | | `GET` | `/trade-api/v2/portfolio/balance` | Cash less what resting orders hold. | | `GET` | `/trade-api/v2/portfolio/positions` | Netted Yes/No positions with exposure, realized P&L and fees; `settlement_status` and `count_filter`. | | `GET` | `/trade-api/v2/portfolio/fills` | | | `GET` | `/trade-api/v2/portfolio/settlements` | Markets the scene settles pay 1 USD per winning contract. | | `GET` | `/trade-api/v2/portfolio/orders` | Status, ticker, event and time filters. | | `POST` | `/trade-api/v2/portfolio/orders` | The older yes/no endpoint; limit orders only. | | `GET`, `DELETE` | `/trade-api/v2/portfolio/orders/{order_id}` | | | `POST` | `/trade-api/v2/portfolio/events/orders` | A bid or ask on one Yes book. Takes other traders' bids at their price, charges Kalshi's fee, then rests or cancels. Fill-or-kill, immediate-or-cancel, post-only, reduce-only and self-trade prevention. | | `DELETE` | `/trade-api/v2/portfolio/events/orders` | | | `POST`, `DELETE` | `/trade-api/v2/portfolio/events/orders/batched` | Up to 20, each with its own result or error. | | `DELETE` | `/trade-api/v2/portfolio/events/orders/{order_id}` | | | `POST` | `/trade-api/v2/portfolio/events/orders/{order_id}/amend` | Price, total count and expiration; a crossing price trades. | | `POST` | `/trade-api/v2/portfolio/events/orders/{order_id}/decrease` | | ## Starter data The `kalshi-starter` dataset is the account **Acme treasury**, funded with one 20,000 USD deposit, and three series with open markets whose books hold other traders' resting bids. | Series | Event | Markets | | --- | --- | --- | | `KXFEDDECISION` Fed rate decision | `KXFEDDECISION-26DEC`, "Fed decision in December 2026", mutually exclusive, strikes 940 hours after start | Hold rates (last 0.38), Cut 25bps (0.57), Cut more than 25bps (0.04) and Hike 25bps (0.02). | | `KXCPIYOY` CPI year-over-year | `KXCPIYOY-26OCT`, "CPI year-over-year in October 2026" | Above 2.7% (0.71), above 2.9% (0.36) and above 3.1% (0.12). | | `KXHIGHNY` Highest temperature in NYC | `KXHIGHNY-26NOV01`, "Highest temperature in NYC on Nov 1, 2026", closing 38 hours after start | Five temperature ranges from 53° or below to 60° or above. | The account holds five orders on the Fed and CPI markets: two filled bids and one filled ask on "Cut 25bps", a partly filled and canceled bid on "Above 3.1%", and a resting bid on "Above 2.9%". Their fills, positions and fees are in the portfolio. ## Checks Kalshi has no state checks. Use trace checks, for example `{ called: "POST /trade-api/v2/portfolio/events/orders" }` or `{ not_called: "DELETE /trade-api/v2/portfolio/events/orders*" }`. ## Limits - Requests are signed with RSA-PSS as on Kalshi; the timestamp's age is not checked. - Orders trade against other traders' resting bids seeded with the scene, at the resting price, with Kalshi's fee. Nobody else trades unless the scene says so, and no money moves. - Primary subaccount only. No order groups, RFQs or quotes, candlesticks, historical data, WebSockets or FIX. # CLI overview Source: https://docs.ancha.io/cli/overview How the ancha CLI is organized, its global options, how it picks a server, its output formats and exit codes, and an index of every command in 0.1.4. The `ancha` CLI is published on npm as `@ancha-labs/cli`. This reference covers version 0.1.4. It needs Node.js 24 or later. ```bash npm install -g @ancha-labs/cli # then run: ancha npx -y @ancha-labs/cli@0.1.4 # without installing ``` `ancha` without a command prints where to start and exits `2`: ```text Output Ancha: simulation playgrounds for agents. Start here: ancha init set up this project: ancha.yaml, a starter scene, coding-agent instructions ancha run run your agent against fresh worlds and grade it ancha explain latest why it failed: each check, expected vs actual, the calls behind it ancha login use your hosted workspace at app.ancha.io (one browser approval) ancha docs the guide, offline ancha --help every command ``` ## Global options These work with every command. | Option | Description | | --- | --- | | `--json` | Machine-readable output on stdout: one JSON object, or NDJSON events for `run`, `wait` and `logs`. | | `--config ` | Config file. Default: `ancha.yaml` in the current directory. | | `--server-url ` | The Ancha server to use. See below. | | `--local` | Use a server on this computer, ignoring the saved login, `ANCHA_SERVER_URL` and `server.url`. | | `-V, --version` | Print the version, `0.1.4`. | | `-h, --help` | Print help for a command. | Setting `ANCHA_JSON=1` in the environment acts as `--json` on every command. `ancha --help` fits on one page. `ancha advanced` lists the commands it leaves out: `agent`, `serve`, `ui`, `scene`, `env`, `sandbox`, `mcp`, `execution`, `playground`, `status`, `wait`, `logs`, `report`, `check`, `runs` and `attempt`. `ancha --help --json` prints every command and flag as one JSON tree, and `ancha -h` prints one command's flags. ## Which server a command uses Commands that create worlds choose a server in this order: 1. `--server-url ` 2. `server.url` in `ancha.yaml` 3. The `ANCHA_SERVER_URL` environment variable `--local` skips all three. With none of these, `ancha run`, `ancha doctor` and `ancha world up` start a private server on this computer for the duration of the command, also when you are signed in. Other server commands, such as `ancha env` and `ancha sandbox`, use `http://127.0.0.1:4400`, where `ancha serve` listens by default. The workspace saved by `ancha login` does not move the worlds. It names the workspace that `ancha run --upload` sends results to, that `ancha run --on cloud` runs in, and that `ancha secrets`, `ancha agent`, `ancha world create|list|show|update|archive`, `ancha test` and `ancha runs` work on. [`ancha whoami`](https://docs.ancha.io/cli/whoami) shows the workspace, where that choice came from, and where `ancha run` puts its worlds. `ancha logout` forgets the saved workspace. The first line of `ancha run`, `ancha doctor` and `ancha world up` says where the worlds run: ```text running locally (worlds in a server this command starts) running locally (worlds in a server this command starts; logged in to https://app.ancha.io/t/: --upload sends the results there) running on hosted https://app.ancha.io/t/ (from ANCHA_SERVER_URL; --local to run here) ``` ## Output - Human output goes to stdout. Errors go to stderr as `error[CODE]: message` with a `hint:` line that names the next command. Most commands end with a `next:` line. - With `--json`, results and errors both go to stdout. An error is `{"error": {"code": "...", "message": "...", "hint": "..."}}`. - Commands never prompt. Every input comes from flags, files or the environment. - [`ancha schema`](https://docs.ancha.io/cli/schema) prints the JSON Schema of `ancha.yaml`, scene files and every JSON output. ## Exit codes | Code | Meaning | | --- | --- | | `0` | Success. For `run`: every required check passed and nothing regressed. | | `1` | A check failed, the pass rate was too low, or a comparison regressed. | | `2` | Usage, config, scene or authentication error. | | `3` | Infrastructure error, or a trial Ancha could not run correctly. | | `4` | `run --strict` only: the agent called an endpoint a replica does not implement. | See [Errors](https://docs.ancha.io/reference/errors) for every error code. ## Commands ### Test loop | Command | What it does | | --- | --- | | [`ancha init`](https://docs.ancha.io/cli/init) | Write `ancha.yaml`, a starter scene for the tools your project uses, and instructions for coding agents. | | [`ancha doctor`](https://docs.ancha.io/cli/doctor) | Check the setup and run the agent once. | | [`ancha run`](https://docs.ancha.io/cli/run) | Run the agent against every scene, several times, and report the pass rate. | | [`ancha explain`](https://docs.ancha.io/cli/explain) | Show why trials failed: expected, actual and the calls behind each check. | | [`ancha report`](https://docs.ancha.io/cli/report) | Print a finished run's summary again, or write an HTML report. | | [`ancha baseline`](https://docs.ancha.io/cli/baseline) | Freeze a run's summary to compare later runs against. | | [`ancha status`](https://docs.ancha.io/cli/status) | Show the state of a running or finished run. | | [`ancha wait`](https://docs.ancha.io/cli/wait) | Follow a run until it finishes. | | [`ancha logs`](https://docs.ancha.io/cli/logs) | Print a run's events, and follow a detached run. | | [`ancha runs`](https://docs.ancha.io/cli/runs) | Send a finished run to your workspace, and list, show and start the runs a workspace holds. | ### Tools and scenes | Command | What it does | | --- | --- | | [`ancha twins`](https://docs.ancha.io/cli/twins) | List the tools and the endpoints each one implements. | | [`ancha connect`](https://docs.ancha.io/cli/connect) | Print SDK settings for a tool. | | [`ancha scene`](https://docs.ancha.io/cli/scene) | Create, validate and preview scene files. | | [`ancha docs`](https://docs.ancha.io/cli/docs) | Print the documentation bundled with the CLI. | ### Automation | Command | What it does | | --- | --- | | [`ancha schema`](https://docs.ancha.io/cli/schema) | Print the JSON Schema of `ancha.yaml`, scene files and the JSON outputs. | | [`ancha completion`](https://docs.ancha.io/cli/completion) | Print a shell completion script. | ### Worlds | Command | What it does | | --- | --- | | [`ancha world up`, `ancha check`, `ancha world down`](https://docs.ancha.io/cli/world-lifecycle) | Start a fresh world for your own harness, grade it, inspect it and remove it. | | [`ancha world gen`](https://docs.ancha.io/cli/world-gen) | Generate a world file from a description. | | [`ancha world`](https://docs.ancha.io/cli/world) | Save versioned worlds of tools and starter data. | ### Servers and sandboxes | Command | What it does | | --- | --- | | [`ancha serve`](https://docs.ancha.io/cli/serve) | Run an Ancha server. | | [`ancha env`](https://docs.ancha.io/cli/env) | Create, list, reset and destroy environments from scene files. | | [`ancha sandbox`](https://docs.ancha.io/cli/sandbox) | Explore seeded tools by hand, without a scene. | | [`ancha mcp`](https://docs.ancha.io/cli/mcp) | Serve a sandbox's Slack to an MCP client. | | [`ancha ui`](https://docs.ancha.io/cli/ui) | Open the local console in your browser. | ### Hosted workspace | Command | What it does | | --- | --- | | [`ancha login`](https://docs.ancha.io/cli/login) | Sign in to your hosted workspace from this computer. | | [`ancha logout`](https://docs.ancha.io/cli/logout) | Forget the saved sign-in. | | [`ancha whoami`](https://docs.ancha.io/cli/whoami) | Show which workspace commands use and where that choice came from. | | [`ancha secrets`](https://docs.ancha.io/cli/secrets) | Set the variables, such as model keys, your agents get in the hosted workspace. | ### Agents and playgrounds | Command | What it does | | --- | --- | | [`ancha agent`](https://docs.ancha.io/cli/agent) | Save an agent profile and run it against bundled scenarios. | | [`ancha playground`](https://docs.ancha.io/cli/playground) | Run tasks in fresh containers and inspect the attempts. | | [`ancha test`](https://docs.ancha.io/cli/test) | Save and rerun agent runs with pinned inputs. | | [`ancha execution`](https://docs.ancha.io/cli/execution) | Check and prepare the local container runtime. | ## Files Commands keep their state in `.ancha/` in the current directory: `runs/` for `ancha run` results, `baselines/` for baselines, `playgrounds/` for agents, worlds, tests and playground runs, and the records of idempotency keys and live worlds. Run every command from the same directory to share them. `ancha init` adds `.ancha/` to `.gitignore`. # ancha init Source: https://docs.ancha.io/cli/init Set up a project for Ancha: write ancha.yaml with the start command it finds, a starter scene for the tools your dependencies use, reference companions and instructions for coding agents. Installs and runs nothing. `ancha init` prepares the current directory for `ancha run`. It writes files only; it installs no dependencies, starts no agent and makes no network calls. ```bash ancha init [options] ``` | Option | Description | | --- | --- | | `--agent ` | The command that starts your agent (default: package.json scripts.start, a Procfile, pyproject.toml [project.scripts] or the Dockerfile CMD) | | `--template