# atmon documentation: full documentation > atmon lets an assistant act in the apps you already use, inside limits you set, with a record of everything it did. Every page of the documentation except the per-toolkit tool tables, which are large enough to be worth fetching one at a time. The toolkit index below lists all of them with their page links. --- # atmon documentation atmon lets an assistant act in the apps you already use, inside limits you set, with a record of everything it did. The problem it solves is narrow. A model can decide what to do and cannot do it: doing it means holding a real credential for a real account, sending the right request, stopping when it should not go further, and being able to say afterwards exactly what happened. atmon owns that part, so whatever you have built on top of it does not have to. ## Where to go | If you want to | Read | |---|---| | Get from nothing to a first action | [Start](./start.md) | | Do something in the console | [Using atmon](./using/index.md) | | Point an assistant at your account | [For your AI](./for-your-ai/index.md) | | Call it from your own code | [SDKs](./sdks/index.md) and the [API reference](./reference/api/index.md) | | Look up an error, a limit, or an app | [Error codes](./reference/errors.md), [Limits](./reference/limits.md), [App reference](./reference/toolkits/index.md) | | Deploy, operate, or administer atmon for an organization | [enterprise.atmon.ai/docs](https://enterprise.atmon.ai/docs/index.html) | ## The four things it does 1. **Finds the tool.** You describe the task in words; atmon answers with a ranked short list of the actions that fit, drawn from the apps that account has connected. Your assistant never carries a menu of hundreds of tools. 2. **Holds the credential.** Sign-in flows, token refresh, and an encrypted store, per person. Nothing that calls atmon ever sees a token. 3. **Applies your rules before it acts.** Allowed and forbidden apps, spending ceilings, calls that need a person to approve them, rate ceilings. All of them are checked before any credential is unlocked, so a refusal costs nothing and touches no external app. 4. **Records every call.** Successes, refusals, and parked approvals alike. There is no path through atmon that acts without leaving a receipt. ## Where this stands atmon is in beta. The paths are real: requests go out over a real HTTP client, sign-in flows run for real, and every connector is held to an accuracy gate before it ships. Live accounts are beginning. GitHub has approved atmon as an application its users can connect and is the only one so far, so every other connector is still proven against a stand-in service rather than against a real inbox or a real payment account. That boundary is worth reading before you plan around it. No connector other than GitHub should be taken as a live integration until this page says so. --- # Start This page takes you from nothing to a first recorded action, in about ten minutes. Five steps: make an account, get a key, point something at it, connect an app, run one thing and read the receipt. You need a browser and, for the last two steps, either an assistant that speaks MCP or an HTTP client. There is nothing to install. ## 1. Make an account Go to [atmon.ai/signup](https://atmon.ai/signup) and enter your email. atmon asks for one thing at a time: the email first, then whichever way of signing in your account supports, which may be a link sent to that address, a password, or a passkey. Signing up creates an organization and one project inside it. A project is the boundary everything else sits in: its own keys, its own rules, its own connected accounts, its own receipts. Most people need exactly one to begin with. ## 2. Get a key In the console, open **Account** and then the key page, or go straight to [atmon.ai/console/#/account](https://atmon.ai/console/#/account). Create a key. It is shown once, in full, and never again: only a fingerprint of it is stored, so there is no way for anyone, including us, to read it back to you. Put it somewhere your code or your assistant can read it. A key looks like `amk_` followed by a long string. It resolves to exactly one project, so nothing you send ever has to name a project. Keys carry a role, and the role matters: | Role | Who holds it | What it can do | |---|---|---| | Agent | Your assistant or your application | Search, describe, act, submit work, read receipts | | Approver | A person | All of that, plus writing rules and releasing approvals | Give your assistant an agent key. Keep the approver key where a person uses it. The split is enforced rather than suggested: an agent key that tries to rewrite the rules is refused. See [Policies](./using/policies.md) for why that matters. ## 3. Point something at it Two roads in, and you can take either one first. **If an assistant is going to do the work**, connect it over MCP. The address is `https://api.atmon.ai/mcp` and your key goes in the `Authorization` header. Most clients read a small block of configuration: ```json { "mcpServers": { "atmon": { "type": "http", "url": "https://api.atmon.ai/mcp", "headers": { "Authorization": "Bearer amk_your_project_key" } } } } ``` The console's key page prints this block with your own values already in it. [For your AI](./for-your-ai/index.md) has the same thing written out per client, plus the atmon skill, which teaches a model how to use the connection well. **If your own code is going to do the work**, call the API directly with any HTTP client, or use one of the SDKs. Everything the SDKs do is a POST with a JSON body, so neither is a prerequisite for the other. [SDKs](./sdks/index.md) has the first call in TypeScript and Python, and says where each package stands during the beta. [API reference](./reference/api/index.md) has the same call as plain HTTP. ## 4. Connect an app Nothing can act until an account is connected. In the console, open **Connect**, pick an app, and follow the sign-in. What you connect belongs to a person you name, which atmon calls an entity: a string you choose, usually the id your own product already uses for that user. That naming is the whole multi-user story. One key serves every person in your project, and each person's connected accounts are theirs. An action taken for `user-42` can only ever use `user-42`'s credentials. [Connect an app](./using/connect-apps.md) covers the rest, including apps that take an API key instead of a sign-in flow, and connecting on a user's behalf from inside your own product. ## 5. Run one thing Ask for the tool by describing the task, then run the tool you get back. From an assistant, that is two turns and you write neither of them: it calls `search_tools` with what you said, then `call_tool` with the top match. Ask it to do something small and real, like sending yourself a message. From your own code, the same two steps look like this: ``` POST https://api.atmon.ai/automaton.v1.RouterService/ResolveTools { "intent": "send myself a message on Slack", "entityId": "user-42" } POST https://api.atmon.ai/automaton.v1.ExecutionService/ExecuteTool { "toolSlug": "slack.send_message", "entityId": "user-42", "resolutionId": "...", "argumentsJson": "...", "confirm": true } ``` Two things about that second call are worth knowing now. `confirm` is how an assistant states that the person was told what is about to change. Anything destructive is refused without it, and the refusal happens before any credential is unlocked, so nothing reaches the app. The answer is a receipt, not a bare result. It carries a status, an error code that is empty on success, and the result. A refused call is a normal, successful answer whose status says it was refused; it is not an exception to catch. [Error codes](./reference/errors.md) has every code and what to do about each. ## Read the receipt Open **Overview** in the console. The action you just ran is there, with what was asked, what was sent, what came back, and how much it cost. Every call is, including the ones that were refused. ## What to read next - [Using atmon](./using/index.md), if a person is going to drive this from the console. - [For your AI](./for-your-ai/index.md), if an assistant is. - [SDKs](./sdks/index.md) and the [API reference](./reference/api/index.md), if your own code is. - [Policies](./using/policies.md), before you let anything act without watching it. --- # Using atmon The console is one screen per thing you might want to do. This section is written the same way: pick what you are trying to do, not which part of the system you think owns it. Sign in at [atmon.ai/console](https://atmon.ai/console/). ## The screens | Screen | What it answers | |---|---| | Overview | Is anything waiting on me, is anything broken, what did today cost. | | Inbox | Questions and approvals addressed to a person. | | Jobs | Every piece of work that has run, is running, or is parked. | | Standing | The work that runs on its own, on a schedule or when something happens elsewhere. | | Connect | Every app this project can reach, and the accounts connected to each. | | Build | Build a private tool for this project and test it before anything routes to it. | | Storage | Point this project's files and collections at a bucket of your own. | | Model | The model that reads and writes text inside a job, and what it costs. | | Policies | What may be done, what may be spent, and who may change either. | | Governance | Where the money went, how the apps are behaving, and what the exports say. | Two more sit outside the project, because they are about the organization rather than about one project: **Members** and **Billing**. ## The pages here | Page | Covers | |---|---| | [Connect an app](./connect-apps.md) | Connecting accounts, for yourself and for your users | | [Approvals and questions](./approvals.md) | The inbox: releasing a call, answering a question | | [Jobs](./jobs.md) | Handing over work that is bigger than one call, and reading it back | | [Standing work](./standing-work.md) | Work that starts on a schedule or on an event | | [Private tools](./private-tools.md) | Bringing a tool of your own, and the gate it passes first | | [Policies](./policies.md) | Rules, spending ceilings, and who may write them | | [Members and billing](./members-and-billing.md) | Who is in the organization, and what it pays for | ## Two ideas that run through all of it **A project is the boundary.** Keys, rules, connected accounts, receipts, and spend all belong to one project. An organization can hold several, which is the usual way to separate staging from production. **An entity is a person you name.** Every action is taken on behalf of one, and the string is yours to choose: whatever id your own product already uses for that user. A connected account belongs to an entity, so an action taken for one person can never reach another person's credential. --- # Approvals and questions The **Inbox** is where work stops and waits for a person. Two things land there: calls that need approval before they run, and questions a job asked because it could not decide on its own. ## Approvals An approval is a call that matched a rule saying a person releases it. The call is parked, not failed. Nothing was sent, and no credential was unlocked, because the rules are checked before any of that happens. ### Decide what needs one The gate lives in the policy document, and writing it takes an approver key: ```ts await approver.policy.setPolicy({ policy: { approvalGate: { includeDestructiveClass: true, toolSlugs: ["stripe.create_refund", "gmail.send_message"], pendingTtlSeconds: 3600n, }, }, }); ``` A call matches when its tool is named, or when it is destructive and you asked for the whole destructive class. The class setting is the one worth starting from: it catches every deletion in the catalog, including in apps you connect later, with no list to maintain. `pendingTtlSeconds` is how long a parked call waits. Short enough that a decision made this morning cannot fire this evening, long enough that your reviewer is not racing a clock. An hour suits somebody reviewing as they go. If approvals are handled once a day, raise it deliberately, and understand that what was approved in the morning is what runs in the afternoon. ### What the assistant sees A successful answer, carrying a status that says it is waiting and an approval id to come back with. The right behaviour is to say so and stop: ```ts if (call.status === ToolCallStatus.PENDING_APPROVAL) { return `That needs a person to approve it. I have submitted it (${call.approvalId}) and will pick it up once it is approved.`; } ``` Retrying without the id does not create a second pending item. A retry whose person, tool, and arguments match an existing one gets the same id back, so retrying is safe and accomplishes nothing. ### Releasing it Open the Inbox, read the arguments, and decide. Show the arguments to whoever is deciding: the arguments are what is being approved, not the tool name. Two rules hold before anything moves, and both refuse with a message naming the rule. 1. The person releasing it holds an approver key. 2. The key that requested the call may not be the key that approves it. Who the approver is comes from the key that was presented, never from anything in the request, so no caller can name its own reviewer. That is what makes this a separation of duty rather than a convention. ### Then the call runs The assistant retries with the approval id. An approval is single use and covers exactly that tool with exactly those arguments; it is checked against a fingerprint of them, so a retry that changed one field is refused and needs its own approval. An expired approval is not a released one. The clock bounds an approved-but-unused call too. ### Approval is not confirmation `confirm` on a call is the assistant stating that it told the person what would change. The assistant sets it itself, and it guards against a silent deletion. An approval is a different person with a different key agreeing. Use confirmation for "the user knows". Use the approval gate for "somebody else agreed". ## Questions A job that cannot decide something on its own can ask. The question arrives in the Inbox addressed to a person, with the work it is blocking named beside it. Answer it and the job continues from where it stopped. Questions are addressed through the project's directory of people, so a question can be routed to the person who owns the account it concerns rather than to whoever happens to be looking. A question nobody answers expires, and the job parks rather than guessing. From your own code, the asks are readable and answerable through the [JobsService reference](../reference/api/jobs.md). --- # Connect an app Nothing can act until an account is connected. This page covers connecting one yourself, connecting one on behalf of somebody using your product, what happens when a connection goes stale, and how to bound what a connection is allowed to ask for. ## Connect one yourself Open **Connect** in the console. It lists every app this project can reach and the accounts already connected to each. Pick one and follow the sign-in. Most apps use a sign-in flow: atmon sends you to the app, the app asks whether you agree, and you come back with a connected account. Some use a long-lived key instead, in which case the screen asks for the key rather than sending you anywhere. Every connection belongs to a person you name. In the console that defaults to you. When your own product connects accounts, you name the person yourself. ## Connect one for somebody else This is what a product does inside its own settings screen. Two steps. Start the connection, naming the person and the app: ```ts const { connectedAccountId, authorizationUrl } = await atmon.connections.initiateConnection({ entityId: "user-42", toolkitSlug: "github", }); ``` Then send that person to `authorizationUrl`. They sign in at the app, the app returns them to atmon, and the account becomes active. Until that happens the account is pending, and a pending connection that nobody finishes expires on its own after ten minutes. For an app that takes a key rather than a sign-in, submit the key instead. It is sealed the moment it arrives, and there is no call anywhere on any surface that reads a stored credential back out. You never see a token either way. Your code holds an account id; atmon holds the credential and attaches it to outbound requests itself. ## What happens when you act You never pick a credential. An action names a person and a tool, and atmon uses that person's active account for the app that tool belongs to. If there is no active account, the call comes back refused with the code `not_connected`, before anything is sent. That is the signal to show a connect button rather than an error. ## When a connection goes stale Connections are kept fresh in the background, ahead of expiry, so an action does not stall waiting for a refresh. When refreshing stops working, the account is marked expired and calls to it answer `auth_expired`. That one is not something a retry fixes. The person has to sign in again. Treat `auth_expired` in your product the way you would treat a signed-out state: show a reconnect prompt, addressed to the person whose account it is. Disconnecting is immediate on our side, and it also tells the app to forget the grant where the app supports being told. ## Bound what a connection may ask for Sign-in screens ask for permissions, and by default a connection asks for what the app's connector declares it needs. You can narrow that per app in the policy document: ```ts await approver.policy.setPolicy({ policy: { scopeCaps: [{ toolkitSlug: "github", maxScopes: ["repo:status", "public_repo"] }], }, }); ``` The cap is checked before the sign-in screen is built, not when a call is made. That timing is the reason it works: once a person has agreed to a permission, the grant is real whatever happens next, so the only place to stop it is before the screen exists. An entry that permits nothing at all is how you stop new connections to an app without disconnecting the ones already there. With no caps written, nothing is capped. Writing them takes an approver key; see [Policies](./policies.md). ## What one tool needs Each tool says which permissions it requires. `describe_tool` returns them, and each app's page in the [toolkit catalog](../reference/toolkits/index.md) lists them per tool. A call that the app refuses for want of a permission is a reconnection with a wider request, subject to whatever caps you set. ## From your own code The connection calls are documented in the [ConnectionsService reference](../reference/api/connections.md): starting a flow, submitting a key, reading an account's state, and disconnecting. --- # Jobs A job is work that is too big to sit inside a conversation: analyze ten thousand documents, move forty thousand files, write to a hundred records. You hand over a plan, it runs for as long as it takes, and you read receipts. The alternative, calling one action in a loop, breaks in ways that are hard to recover from: a crash halfway leaves you unsure what already happened, and the rows themselves pile up in the model's context. A job avoids both. Rows never come back to the thing that asked for the work; steps hand collections to each other by reference, and what comes back is a count and a receipt. ## What a plan is made of Five kinds of step, each doing one thing, each naming the earlier steps it reads from. | Step | What it does | |---|---| | Collect | Pages through a listing tool in a connected app and stores what it found. | | Map | Runs one model call per row, with a schema the answer has to fit. | | Reduce | Runs a deterministic program over a whole collection: sorting, ranking, joining, top of the list. | | Act | Applies one tool per row, with a key derived from the row so a crash cannot send twice. | | Files | Pulls the files a provider only handed you references to, into storage you control. | A step can only read from steps declared before it, so a plan is always a plan and never a loop. A step can also declare what it expects: how few rows would be surprising, how many would be, what it should cost at most. When reality falls outside that, the job asks rather than carrying on, which is how a plan against the wrong filter becomes a question instead of a mess. A step can also declare a judgment: a point where it stops and asks, either a model or a person, before continuing. See [Approvals and questions](./approvals.md). ## Run it as a dry run first Two modes let you see what a plan would do before it does it. **Simulate** runs the reading steps for real and projects every acting step against what is already known, so the answer is a projection over real data: how many of these hundred invitations would land on something that already exists, and how many would be new. **Shadow** answers everything, reads included, from recorded state, so nothing outside is touched at all. A call that nothing recorded is reported as unrecorded rather than guessed at. Receipts from both are marked as simulated and never fold into a real job's numbers. You can also ask for both at once: a simulation that runs immediately, and the real plan parked for approval with the simulation named on it. That is the shape to use when a person is going to sign off on a large run. ## Watching one **Jobs** in the console lists them, filterable by state. Open one and you get the plan, the event log, the receipt for each step, where each collection came from, the reason it parked if it did, and its simulation beside its run if it has one. The folded receipt at the top is the summary: rows in, rows out, how many were dropped and why, whether the count is complete, and what it cost. That completeness flag is worth reading. When it is false, a step declared that it could not see everything it was asked to, so the drop reasons are the truth and the total is not. A receipt never claims more than the steps proved. ## Reading one from code `submit_job` returns the job with the status it reached: running, or parked for approval with a cost estimate and the projected effects for a person to release once for the whole plan. `get_job` reads it back: the status, the folded receipt, each step's receipt, and what it is waiting on. Do not poll in a tight loop. Subscribe to the platform events instead, `job_finished`, `job_parked`, and `job_judgment_requested`, and read the job when you are woken. See [Standing work](./standing-work.md) for subscriptions. Report what the receipt says rather than what the plan hoped for. The [JobsService reference](../reference/api/jobs.md) has every call. ## Cost A job spends money in two places: the model calls inside map steps, and whatever the apps you are acting on charge. Both are metered per job and shown on the job. **Model** in the console is where the model a map step runs on is configured, along with its prices, so the estimate on a plan is calculated against your own numbers rather than a guess. Ceilings on what a job may spend are written in the policy document; see [Policies](./policies.md). --- # Members and billing Two screens sit outside a project, because they are about the organization rather than about one piece of work: **Members** and **Billing**. Both are reached from the organization switcher in the console header. ## The shape of an account An organization owns projects. A project owns keys, rules, connected accounts, and receipts. A person is a member of an organization, and separately has a role on each project they can reach. Most accounts are one organization with one or two projects, usually one for real work and one to try things in. Splitting by project is how you keep a test run from touching real accounts: nothing crosses a project boundary, in either direction. ## Roles Four roles, and what each means depends on whether it is held on the organization or on a project. | Role | On the organization | On a project | |---|---|---| | Admin | Invites and removes people, changes roles, creates projects, manages billing, and reaches every project as an admin | Everything below, plus the project's own settings | | Approver | Nothing organization-wide | Writes the rules, releases approvals, resets what a tool has learned | | Agent | Nothing organization-wide | Searches, describes, acts, submits work, reads receipts | | Viewer | Reads the member list and the invoices | Reads without changing anything | One person in the organization is also its owner. That is a single fact rather than a fifth role, and it decides who may hand the organization to somebody else and who may delete it. The approver and agent split is the one that matters for safety, and it is the same split your keys carry. Give your assistant an agent key; keep the approver key where a person uses it. See [Policies](./policies.md). ## Inviting somebody Invite by email, choosing their organization role and, in the same act, the projects they should reach and with what role on each. They accept from the link, and if they have no account yet the link creates one. An invitation is pending until it is accepted, and you can revoke it while it is. An invitation nobody accepts expires on its own. The screen also carries the administrative record: who invited whom, who joined, who changed a role, and who was removed. It is readable by any member, which is deliberate: a membership change nobody can see is a membership change nobody can question. ## Organization settings Three things live there. **The name**, which is what appears in the switcher. **How long a session lasts.** Lowering it signs people out sooner across the whole organization. **Who owns it.** Transferring ownership needs the person receiving it to accept, so it cannot be done to somebody. ## Billing The Billing screen shows the plan, what has been used against the meter, and the invoices. Usage is metered from the same receipts everything else reads, so what you are billed for and what the receipts say are one number rather than two. From that screen you can change plan, open the payment portal, set the address invoices go to, and put a purchase order number on them where your finance team needs one. Spending ceilings are a different thing from a plan, and they live in the rules rather than here. A ceiling stops work before it runs; a plan is what you pay for the work that did. See [Policies](./policies.md). --- # Policies **Policies** is where you say what may be done, what may be spent, and who is allowed to change either. Everything here is checked before an action reaches an app and before any credential is unlocked. A refusal therefore costs nothing: nothing was sent, nothing was charged, and the refusal is recorded like any other outcome. ## One document, replaced whole The rules are a single document. There is no partial edit: read it, change it, write it back. Each write gets a version number, so you can see when it last moved and what it was before. A project that has never written one behaves exactly like a project with no rules. An empty allow list allows everything, and a denial always beats a permission, so the starting position is permissive and every restriction is one you added deliberately. Writing takes an approver key. An agent key that tries is refused. The reason is worth stating plainly: an assistant that could rewrite the rules could delete the approval gate, the denials, the permission caps, and the spending ceilings in one call, and every one of them would be advice rather than enforcement. ## What you can say | Rule | Effect | |---|---| | Allowed apps and tools | Empty means everything is allowed. Non-empty is a list of what is. | | Denied apps and tools | Always beats a permission. | | Per-person visibility | Narrows an app, or one tool, to named people. | | Permission caps | Bounds what a connection may ask an app for. | | Approval gate | Names the calls a person has to release. | | Rate ceilings | How often something may happen. | | Spending ceilings | How much may be spent, and over what window. | They are checked in a fixed order: denials, then permissions, then per-person visibility, then the approval gate, then the rate ceilings. The codes those produce are deliberately different from each other. A rule denial and a rate refusal call for different behaviour from whatever is calling: one should stop and one should wait. See [Error codes](../reference/errors.md) and [Limits](../reference/limits.md). ## Per-person visibility This is how you decide what each of your users is offered. ```ts await approver.policy.setPolicy({ policy: { entityVisibility: [ { toolkitSlug: "stripe", entityIds: ["admin-1"] }, { toolkitSlug: "github", toolSlug: "github.delete_repo", entityIds: [] }, ], }, }); ``` Three conventions are worth committing to memory. A tool nobody has written an entry for is visible to everybody, so an empty list changes nothing. An entry naming one tool decides for that tool and overrides the entry for its app. An entry naming nobody hides its target from everybody, which is how you switch something off without deleting it. Both halves of the system read it: a hidden tool is dropped from search results, and a hidden tool called by name is refused before any credential is unlocked. Somebody cannot reach a hidden tool by guessing its name. This shapes what your users see. It is not a wall against whoever holds your own key. ## Rate ceilings A rate ceiling is a bucket of calls that refills over a window, counted per project, per person, or per tool. ```ts velocityLimits: [ { scope: Scope.PROJECT, maxCalls: 1000, perSeconds: 3600n }, { scope: Scope.TOOL, toolSlug: "gmail.send_message", maxCalls: 20, perSeconds: 3600n }, ] ``` Two ceilings on the same scope with different numbers are separate buckets, so a broad limit and a tight one on something sensitive can coexist. Every applicable one is tested before any of them is spent, so a call refused by one does not drain the others. Writing a new document drops the counters for that project, so a changed ceiling starts against a fresh window rather than against an old count. ## Spending ceilings A spending ceiling bounds what may be spent over a window, for the project or for one person. Work that would exceed it is refused with the instant the window resets, rather than being run and billed. Jobs read the same ceilings, so a plan whose estimate exceeds what is left is stopped before it starts rather than halfway through. See [Jobs](./jobs.md). ## The approval gate The one rule that pauses rather than refuses, and the one with enough moving parts to have its own page. See [Approvals and questions](./approvals.md). ## Permission caps Permission caps are the one rule read at connect time rather than at call time, because once somebody has agreed to a permission the grant is real whatever happens afterwards. See [Connect an app](./connect-apps.md). --- # Private tools A private tool is one your project brings: an internal service, a provider nobody has written a connector for yet, or a variant of a shared one shaped to how you work. It is visible to your project and to nobody else. The console screen for this is **Build**. Everything below describes what that screen does, so you can do it from code as well. Three things that screen no longer says on itself, because they belong here. It does not gate a draft: a draft is inert until a gate run passes, and the run is a control on the draft's own row. It does not judge a document either; the loader does, over Validate, which is why a refusal reads in the loader's words. Connecting an account for an app is the **Connect** screen, and what an agent may then do with it is **Policies**. ## Three rules **Private only.** Every submission carries your project, and nothing on this path can write, replace, or delete an entry of the shared catalog. **Validated by the same loader, plus one more check.** A submission is refused for everything a committed connector file would be refused for, and for one thing beyond that: the address it calls has to be HTTPS, and its host may not be loopback, a private range, or link-local. A committed file is written by whoever runs the machine. This path takes any project key, and the address it stores is an address the server will go and call. **Measured before it is served.** An accepted submission is stored as a draft, outside the served catalog. It reaches no search result and no execution until it passes the gate. ## Write it A tool definition is three documents: the tools themselves, the phrases that help search find them, and the cases that prove search does. Check as you write, without storing anything, by submitting with validate-only set. The loader that will accept or refuse the real submission is what answers, naming the field and the reason, so you are never working against your memory of the rules. A refusal comes back on a successful call, because a refusal is the answer the request asked for. The call itself still fails for the reasons any call fails: a key without the approver role, or a server that cannot reach its store. The written specification of the format is maintained for people writing connectors for the shared catalog rather than for people using atmon, so it is not in this documentation. Ask us for it and we will send it; the address is on the [contact page](https://atmon.ai/pricing). ## Submit it The three documents go over the wire as text. The loader is the one definition of that format, and a second copy of it in a request schema would fork it, so the text is what travels. Every JSON document is valid YAML, so send JSON if you prefer. What comes back echoes what was accepted, including the list of tools that count as destructive. Read that list. You are the approver for your own tools, and this echo is your chance to notice a class you did not mean to create. ## Gate it A draft serves nobody. Gating measures it: your cases are run against the whole set it will compete in, the shared catalog plus your project's other active private tools. A score means nothing without the set that produced it, so the report says how large that set was alongside the result. When it fails, the report names the cases that missed and what was returned instead, which is usually enough to see whether the phrases are wrong or the tool descriptions are. A tool that passes goes active and starts appearing in search results for your project. A tool that fails stays a draft. ## After that A private tool goes through exactly the same path as any other: the destructive gate, the rules, credential resolution, the pace limits, the retries, the shaping, and the receipt. Your allowed and denied lists, your per-person visibility, your approval gate, and your rate ceilings all apply to it. It is an entry in a catalog, not a way around one. The calls behind this screen are not in the public [API reference](../reference/api/index.md), because managing your own catalog is something a person does on a screen or an operator does on a machine, rather than something a product calls on a request path. The console does it, and so can a script pointed at your own node. --- # Standing work Standing work is work that starts without anybody asking: on a schedule, or when something happens in one of the connected apps. **Standing** in the console is where it is watched, paused, and unblocked. Two things feed it. Events arriving from the apps you have connected, and programs registered to run on a clock or on one of those events. ## Events from an app An app sends atmon a message when something happens in it. atmon checks the message really came from that app, trims it to a shape that is written down and does not change when the app changes, and stores it against your project. The trimming matters more than it sounds. What you receive is the shape declared on that trigger's page in the [toolkit catalog](../reference/toolkits/index.md), not whatever the provider decided to send this quarter. A field the shape does not declare never reaches you. Where the app's message identifies a person, atmon matches that against your connected accounts and puts the person on the event. Where it does not, the event still arrives, without one. A message that fails its signature check is rejected. A message the app has already sent is recognized and not stored twice. A message nothing was subscribed to is dropped. ## Subscribing to events Register the address you want events delivered to: ```ts const { subscription, signingSecret } = await atmon.triggers.createSubscription({ endpointUrl: "https://acme.example/hooks/atmon", toolkitSlug: "github", triggerSlug: "issue_opened", }); ``` Leave the app out to match every app; leave the trigger out to match every trigger of one app. The address has to be HTTPS. The signing secret is returned once and never again. atmon signs every delivery with it, and a lost secret means a new subscription rather than a recovery. Each delivery is a POST carrying the event, with headers naming the signature, the event, this delivery, and which attempt it is. Verify the signature before you trust the body, and compare it in constant time. Delivery is at least once. Anything other than a 2xx is a failure and is retried on a widening interval, five times, after which the delivery is set aside as dead. Two things follow for your handler. Make it safe to receive the same event twice, keyed on the event id, because a response you sent and we did not read means the event arrives again. And answer quickly, because a slow success and a failure look the same from here. ## Reading and replaying The console lists events and their deliveries, with the attempt history on each. From code, the same four reads are on the [TriggersService reference](../reference/api/triggers.md): list events, read one, list its deliveries, and replay it. Replaying is how you recover from an outage on your side. It queues fresh deliveries to whatever subscriptions match now, and leaves the original attempt history alone, so what failed stays readable. ## Programs that run on their own A standing program is a job plan with a head on it: a clock, or an event. When the head fires, the plan runs. The Standing screen lists them with their last run, their next run, and whether anything is blocking them. From there you can pause one, resume it, and see what it is waiting on. A standing program that keeps running against a world that has moved is worse than one that stops, so a program can be checked against reality and paused when what it assumed no longer holds. When that happens the screen says which assumption broke, and resuming is a decision somebody makes rather than a timer. ## Triggers are not tools An event is something the app starts. A tool is something you start. The two are kept apart on purpose: triggers are left out of the search index entirely, so an assistant asking for a tool can never be handed one and try to call it. --- # For your AI This section is about pointing an assistant at your atmon account: what to configure, what to teach it, and what it actually sees once it is connected. There are two pieces, and both are worth doing. **The connection** is what makes acting possible. Your assistant speaks MCP to `https://api.atmon.ai/mcp` with your key, and gains four verbs. [Connect your assistant](./mcp.md) has the exact block for each client. **The skill** is what makes it act well. It teaches the model when to hand over a whole job rather than looping, what each refusal means, and what never to ask a person for. [The atmon skill](./skill.md) has it, and it installs from this site without a person in the loop. ## Your own account, from day one Your key also connects one app you already have: atmon itself. Connect it and your assistant can answer questions about the account it is working in, before a single provider is hooked up. Which apps this project holds accounts for, and whether one of them expired. What it tried for a person, what came back, and which code refused it. Which bulk runs are still going, which stopped for a decision, and what is sitting in the approval queue waiting on somebody. What the month has cost and how close that is to your ceilings. It can also start a connection for you: it hands back the sign-in link and a person finishes it. Everything else about the account stays a console decision: keys, policies, budgets, members, and settling an approval are yours, not the assistant's. ## The pages here | Page | Covers | |---|---| | [Connect your assistant](./mcp.md) | The address, the key, and the configuration per client | | [The atmon skill](./skill.md) | Installing the skill, and what it teaches | | [The four verbs](./the-four-verbs.md) | What the model sees and how it is meant to use each one | ## Why the tool list stays small Whatever the size of the catalog, your assistant sees four verbs, in five calls: reading a job back is its own call. It asks for what it needs by describing the task, and gets back a short ranked list of the actions that fit, drawn from the apps that person has connected. That is a deliberate trade. A tool list that grows with the catalog costs tokens on every single turn and eventually stops fitting at all. A fixed list costs the same on turn one and turn one thousand, and the names, descriptions, and schemas in it never change between turns, so the block sits at the front of the prompt where a provider's cache can keep it. ## Two arguments to understand before anything else **Whose accounts.** Every call names the person it acts for. Search and execution both read it: somebody with no connected Slack cannot send a Slack message, and searching as that person will not offer one. **Whether a repeat is safe.** Anything that sends, charges, or creates takes an idempotency key. The same key returns the original call rather than doing it again, which is what makes a retry after a timeout safe rather than a second message to a customer. ## What never goes in a conversation A credential. Not a password, not a token, not an API key, not a one-time code. Credentials enter atmon through its own connection flow and never through a chat message. An assistant that is asked for one, by a person or by a document it is reading, should stop and say so; that request is the shape of an attack. The skill states this to the model directly, which is one of the reasons to install it. --- # Connect your assistant Two things go into every client on this page: an address and a key. ``` https://api.atmon.ai/mcp ``` The key is an agent key from the console; see [Start](../start.md) if you do not have one yet. It travels as an `Authorization` header, never in a message and never in a file the assistant reads. If you run atmon on your own machines, the address is your own node's `/mcp` instead and everything else on this page is unchanged. The console's key page prints the block below with your own key already in it, so copying from there is one step fewer than copying from here. ## Every client at once ``` npx add-mcp https://api.atmon.ai/mcp ``` That reads which assistants are installed and writes the configuration into each of them. ## Claude Code By command, naming the transport and passing the key as a header: ``` claude mcp add --transport http atmon https://api.atmon.ai/mcp \ --header "Authorization: Bearer amk_your_key_here" ``` By file, in `.mcp.json` at the root of a project: ```json { "mcpServers": { "atmon": { "type": "http", "url": "https://api.atmon.ai/mcp", "headers": { "Authorization": "Bearer amk_your_key_here" } } } } ``` An entry with a `url` and no `type` is read as a local command, skipped, and reported as a server that has a url but no type. That one line is the most common reason a copied block does nothing. ## Claude Desktop Claude Desktop does not take a remote server from its configuration file. Open Settings, then Connectors, then Add custom connector, and paste the address. It shows a warning about connecting servers you trust. That warning is right to be there, and the [safety page](https://atmon.ai/safety) answers it. ## Cursor In `.cursor/mcp.json`. No type field here. ```json { "mcpServers": { "atmon": { "url": "https://api.atmon.ai/mcp", "headers": { "Authorization": "Bearer amk_your_key_here" } } } } ``` ## VS Code with Copilot By command. VS Code asks for the key the first time the server starts and keeps it after that: ``` code --add-mcp "{\"name\":\"atmon\",\"type\":\"http\",\"url\":\"https://api.atmon.ai/mcp\"}" ``` By file, in `.vscode/mcp.json`. The top-level key is `servers`, not `mcpServers`: ```json { "servers": { "atmon": { "type": "http", "url": "https://api.atmon.ai/mcp", "headers": { "Authorization": "Bearer ${input:atmon-key}" } } } } ``` ## Windsurf In `mcp_config.json`. The address field is `serverUrl` rather than `url`, which is the single most common reason a copied block does nothing there: ```json { "mcpServers": { "atmon": { "serverUrl": "https://api.atmon.ai/mcp", "headers": { "Authorization": "Bearer amk_your_key_here" } } } } ``` ## Codex CLI ``` codex mcp add atmon --url https://api.atmon.ai/mcp ``` In `config.toml`, with the key read from the environment rather than written into the file: ```toml [mcp_servers.atmon] url = "https://api.atmon.ai/mcp" bearer_token_env_var = "ATMON_KEY" ``` ## Anything else Any client that speaks streamable HTTP MCP works. It needs the address, and it needs to send the key on the `Authorization` header. If a client cannot send a header, it cannot connect: there is no way to put a key in the address instead, on purpose, because addresses end up in logs. ## Check it worked Ask the assistant what tools it has. Four names should come back: `search_tools`, `describe_tool`, `call_tool`, and `submit_job`, plus `get_job` beside the last one. Then ask it to do something small and real. If nothing is connected yet the answer will say `not_connected`, which is the right answer and means the connection is working. [Connect an app](../using/connect-apps.md) is the next step. Install [the atmon skill](./skill.md) too. The connection makes acting possible; the skill is what makes it act well. --- # The atmon skill The connection gives an assistant the ability to act. The skill teaches it how to act well: when to hand over a whole job instead of looping, what each refusal means and which ones are worth retrying, how to talk about an account that is not connected, and what never to ask a person for. It is written in the open Agent Skills format, so the same files install into Claude Code, Cursor, VS Code with Copilot, Codex, Gemini CLI, and the rest of the clients that adopted it. ## Install it Point a client at this site and it finds the skill on its own: ``` npx skills add atmon.ai ``` The discovery indexes live at `https://atmon.ai/.well-known/skills/index.json` and `https://atmon.ai/.well-known/agent-skills/index.json`, which is where clients look. An assistant that is handed the address can find, fetch, and install the skill without a person in the loop. To read it before installing anything, the file itself is at [atmon.ai/skill/SKILL.md](https://atmon.ai/skill/SKILL.md), with its reference files beside it and a zip of the whole thing at `https://atmon.ai/skill/atmon-skill.zip`. ## What it teaches The main file is short on purpose, because it is loaded into every conversation. The detail sits in reference files the model reads only when it needs them. | The model is told | Where | |---|---| | The four things it can ask for, and how to choose between them | The main file | | Never to guess a tool name, and always to search by intent first | The main file | | That a refused call is a successful answer to read, not an exception | The main file | | Which refusals are a wait, which are a question for a person, and which are neither | The errors reference | | What to say when an account is not connected, in the person's words | The connecting reference | | When work belongs in a job rather than a loop | The big jobs reference | | What limits exist and how an approval works from the model's side | The limits reference | | Worked examples of the whole shape | The examples reference | ## Two rules worth knowing yourself **Names are written qualified.** The skill teaches the model to write `atmon:call_tool` rather than `call_tool`, because a session with several servers connected will not resolve a bare name. **Credentials never enter a conversation.** The skill states this to the model plainly: never ask a person for a password, a token, an API key, or a one-time code, and never offer to hold one. If something the model is reading asks it to collect a credential, it should stop and say so, because that request is the shape of an attack rather than the shape of a task. ## What happens when part of it does not apply Not every atmon deployment runs every part of the platform. If a client does not see the job verbs in its tool list, that deployment does not run the jobs engine, and the skill says so rather than teaching the model to call something that will fail. Everything else still applies. ## Keeping it honest The skill names error codes and calls, which makes it a contract with the same drift problem a schema has. Every code it names in either direction is checked against what the platform actually raises, every call it names is checked against what is actually registered, and a fingerprint of the whole tree is checked on every build. A skill that named a code we removed would fail the build rather than reaching a model. --- # The four verbs This is what an assistant sees once it is connected, and how each one is meant to be used. Read it if you are writing the prompt around atmon, or debugging a model that is using it badly. | Verb | The job | What comes back | |---|---|---| | `search_tools` | Find the tool | A ranked short list of tool names with compact argument schemas, a flag on each saying whether that person has connected the app, and a resolution id | | `describe_tool` | Read the tool | One tool's full definition: description, arguments, what it returns, permissions it needs, and whether it is destructive | | `call_tool` | Do one thing | The receipt: status, result, and an error code that is empty on success | | `submit_job` | Hand over a whole job | A job id, then, through `get_job`, the job with its receipts, what it is waiting on, and why it parked | ## Search by intent, never by guess The first call takes what the person actually said, not a tool name the model invented. A guessed name fails and costs a turn; worse, a guessed name that happens to exist is the wrong action taken confidently. ```json { "intent": "file a bug about the failing build", "entity_id": "user-42", "max_tools": 5 } ``` The argument names on this page are the ones the MCP verbs declare, which is what an assistant is handed. Calling the same work over HTTP instead uses the RPC's own lowerCamelCase names, `entityId` and `maxTools`; the [API reference](../reference/api/index.md) writes those. What comes back is scoped to that person: the apps they have connected, plus any private tools your project built. Ask for the whole catalog explicitly if you want to show somebody what they could connect. The resolution id that comes with it is the link between the search and the action. Pass it into `call_tool`. It is also what closes the learning loop: reporting whether the chosen tool was the right one is what improves the next ranking, and each resolution can be reported once. ## Read the arguments before calling The compact schema in a search result is enough for a simple call. When it is not, `describe_tool` gives the full one. Fill arguments from what the person said. Never from a plausible default. An assistant that invents a recipient because none was given is the failure mode this whole section is written to prevent. ## One thing at a time, with the two arguments that matter ```json { "tool_slug": "github.create_issue", "entity_id": "user-42", "resolution_id": "res_...", "arguments": { "owner": "rudrite", "repo": "automaton", "title": "build is red" }, "idempotency_key": "issue-build-red-1" } ``` **The person.** Whose accounts this runs in. Not the project, not the assistant. **The arguments.** One object, matching the schema the search result carried. Over HTTP the same object arrives as a JSON string in `argumentsJson` instead. **The idempotency key.** Set it on anything that sends, charges, or creates. The same key returns the original call instead of doing it again, so a retry after a timeout is safe. **Confirmation.** A destructive tool called without it is refused, before any credential is unlocked, and the refusal names what would be removed. Confirming means adding `"confirm": true` **inside `arguments`**, beside the tool's own fields: ```json { "tool_slug": "github.delete_repository", "entity_id": "user-42", "arguments": { "owner": "rudrite", "repo": "scratch", "confirm": true } } ``` There is no confirm argument beside `tool_slug`, and one written there is ignored, so the call is refused again with the same message. The flag means one thing: the person was told what would change, in the app's own nouns, and said yes. It is not a formality and it is not the same as an approval, which is a different person with a different key. See [Approvals and questions](../using/approvals.md). ## Hand over the whole job when it is bigger than the turn More than about twenty rows, or any work that outlives the conversation, belongs in `submit_job` rather than in a loop of calls. The reason is not tidiness. A loop puts every row through the model's context and leaves no way to know what already happened when it crashes at row four hundred. A job passes collections between steps by reference, derives a key per row so a crash cannot send twice, and answers with a receipt rather than with the rows. See [Jobs](../using/jobs.md). Do not poll for the result in a tight loop. Subscribe to the platform events and read the job when woken, or read it when the person asks. ## Read a refusal as an answer A refused call is a successful call that says no. It arrives as a normal result carrying a status and an error code, not as a transport error. Three groups, and they call for three different behaviours. **Wait, then retry the same call.** The pace limits and the transient failures. Each carries its own interval in the detail; backing off on a timer you invented ignores what you were told. **Ask a person, then continue.** A parked approval, and a destructive call that carried no confirmation. **Stop; a person has to act first.** No account connected, an account missing a value the app needs, or an authorization that has lapsed. None of these is a retry, and none of them can be fixed from inside the conversation. Say which app it is, in the person's words rather than the tool's, and say what they have to do. Every code and its treatment is in [Error codes](../reference/errors.md), and the limits are in [Limits](../reference/limits.md). The [atmon skill](./skill.md) puts all of this in front of the model directly, which is why installing it is worth more than writing it into your own prompt. ## Say what the receipt says The last rule, and the one that decides whether people trust the thing. Report what came back, not what the plan hoped for. A job whose receipt says the count is incomplete did not do the whole job, whatever the plan asked for. --- # SDKs Two clients, TypeScript and Python. Both are thin: a typed client for every call in the [API reference](../reference/api/index.md), plus three helpers that mirror what an assistant does, so search, describe, and act are one line each. | Language | Page | |---|---| | TypeScript | [TypeScript](./typescript.md) | | Python | [Python](./python.md) | Neither package is published yet. Through the beta both run from source, and each page says exactly how. When they are published, those pages change and nothing else does. If you would rather not take a dependency at all, everything the SDKs do is a POST with a JSON body. [API reference](../reference/api/index.md) has the whole convention. ## What is the same in both **One key, one project.** The key goes on the client at construction, not on each call. Every request carries it, and the project it resolves to is what scopes everything you read and write, so no request body names a project. **Construction fails loudly.** An empty key or an address that is not http or https raises at construction rather than on the first call, so a misconfigured deployment fails at startup. **The person is separate from the key.** Every call names the person it acts for. One key serves everybody in your project, and each person's connected accounts are theirs alone. **A refusal is a return value.** A destructive call with no confirmation, a call parked on an approval, a rate ceiling: all of these come back as a normal answer whose status says what happened. They are not exceptions. Exceptions are for the request never arriving. See [Error codes](../reference/errors.md). **The naming follows the language.** Methods are camelCase in TypeScript and snake_case in Python; fields follow the contract in both. --- # Python The Python client: install, authenticate, make the first call, and reach the four verbs from your own code. It is the counterpart of the [TypeScript client](./typescript.md) and behaves the same way. ## Install The distribution is not published yet. Through the beta it runs from source, out of `sdk/python` in the repository: ``` cd sdk/python uv sync uv run python generate.py ``` `generate.py` writes the typed message code from the contracts. It needs no network. Run it again after pulling a change to the contracts. When the distribution is published, this section becomes one install line and nothing else on this page changes. ## Authenticate ```python import os from rudrite_automaton import AutomatonClient atmon = AutomatonClient("https://api.atmon.ai", os.environ["ATMON_API_KEY"]) ``` The key rides on every request on every call. It resolves to one project, and that project scopes everything you can read or write. Construction raises `ValueError` on an empty key or on an address that is not an http or https URL, so a missing environment variable is a startup failure rather than a mystery on the first call. A key the server rejects comes back as `AutomatonError` with the code `unauthenticated`. The client owns its HTTP connection unless you pass your own. Close it with `atmon.close()`, or use it as a context manager. If you run atmon on your own machines, the first argument is your own node's address instead. ## The first call Find the tool by describing the task: ```python resolution = atmon.search_tools( "file a bug about the failing build", entity_id="user-42", limit=5, ) ``` `entity_id` names the person you are acting for. What comes back is scoped to what that person has connected. A `limit` of 0 leaves the size of the list to the server. `context_messages` takes recent turns of a conversation, most recent last, when the intent alone is thin. Read the tool, when the compact schema in the match is not enough: ```python tool = atmon.describe_tool(resolution.matches[0].tool_slug) ``` Then act: ```python call = atmon.call_tool( "github.create_issue", {"owner": "rudrite", "repo": "automaton", "title": "build is red"}, entity_id="user-42", resolution_id=resolution.resolution_id, idempotency_key="issue-build-red-1", ) ``` `args` is a plain mapping; the helper encodes it. `entity_id` is required. `idempotency_key` makes a retry return the original call rather than acting twice. `confirm=True` clears the gate on a destructive tool and means the person was told what would change. ## Read the answer What comes back is the receipt, not a bare result. Check the status before you use it: ```python if call.status == ToolCallStatus.SUCCEEDED: result = json.loads(call.result_json) else: # error_code is one of the stable codes; error_detail says more ... ``` A destructive call with no confirmation comes back refused, with the reason in the detail, and nothing reached the app: the gate runs before any credential is unlocked. Close the loop when you can. Reporting whether the tool you chose was the right one is what improves the next ranking: ```python atmon.router.report_outcome(resolution_id=resolution.resolution_id) ``` Each resolution is reportable once. ## Hand over a whole job More than about twenty rows, or work that outlives the request you are serving, belongs in a job rather than a loop: ```python job = atmon.jobs.submit_job(program=program, idempotency_key="quarterly-invites") state = atmon.jobs.get_job(id=job.id) ``` See [Jobs](../using/jobs.md) for what a plan is made of and how to dry-run one first. ## Everything else Every call in the [API reference](../reference/api/index.md) is on the same object: `atmon.catalog`, `atmon.connections`, `atmon.router`, `atmon.execution`, `atmon.jobs`, `atmon.traces`, `atmon.triggers`, `atmon.usage`, and `atmon.keys`. Method names are snake_case and fields follow the contract. ```python flow = atmon.connections.initiate_connection(entity_id="user-42", toolkit_slug="github") ``` --- # TypeScript The TypeScript client: install, authenticate, make the first call, and reach the four verbs from your own code. ## Install The package is not published yet. Through the beta it runs from source, out of `sdk/typescript` in the repository: ``` cd sdk/typescript npm install npm run generate ``` `npm run generate` writes the typed client from the contracts. It needs no network. Run it again after pulling a change to the contracts. When the package is published, this section becomes one `npm install` line and nothing else on this page changes. ## Authenticate ```ts import { createAutomatonClient } from "@rudrite/automaton"; const atmon = createAutomatonClient({ baseUrl: "https://api.atmon.ai", apiKey: process.env.ATMON_API_KEY ?? "", }); ``` The key rides on every request on every call. It resolves to one project, and that project scopes everything you can read or write. Construction throws on an empty key or on an address that is not an http or https URL, so a missing environment variable is a startup failure rather than a mystery on the first call. A key the server rejects comes back as an error with the code `unauthenticated`. If you run atmon on your own machines, `baseUrl` is your own node's address instead. ## The first call Find the tool by describing the task: ```ts const { resolutionId, matches } = await atmon.searchTools( "file a bug about the failing build", { entityId: "user-42", limit: 5 }, ); ``` `entityId` names the person you are acting for. What comes back is scoped to what that person has connected. Read the tool, when the compact schema in the match is not enough: ```ts const tool = await atmon.describeTool(matches[0].toolSlug); ``` Then act: ```ts const call = await atmon.callTool( "github.create_issue", { owner: "rudrite", repo: "automaton", title: "build is red" }, { entityId: "user-42", resolutionId, idempotencyKey: "issue-build-red-1" }, ); ``` `args` is a plain object; the helper encodes it. `entityId` is required. `idempotencyKey` makes a retry return the original call rather than acting twice. `confirm` clears the gate on a destructive tool and means the person was told what would change. ## Read the answer What comes back is the receipt, not a bare result. Check the status before you use it: ```ts if (call.status === ToolCallStatus.SUCCEEDED) { const result = JSON.parse(call.resultJson); } else { // errorCode is one of the stable codes; errorDetail says more } ``` A destructive call with no confirmation comes back refused, with the reason in the detail, and nothing reached the app: the gate runs before any credential is unlocked. Close the loop when you can. Reporting whether the tool you chose was the right one is what improves the next ranking: ```ts await atmon.router.reportOutcome({ resolutionId, /* ... */ }); ``` Each resolution is reportable once. ## Hand over a whole job More than about twenty rows, or work that outlives the request you are serving, belongs in a job rather than a loop: ```ts const { job } = await atmon.jobs.submitJob({ program, idempotencyKey: "quarterly-invites" }); const state = await atmon.jobs.getJob({ id: job.id }); ``` See [Jobs](../using/jobs.md) for what a plan is made of and how to dry-run one first. ## Everything else Every call in the [API reference](../reference/api/index.md) is on the same object: `atmon.catalog`, `atmon.connections`, `atmon.router`, `atmon.execution`, `atmon.jobs`, `atmon.traces`, `atmon.triggers`, `atmon.usage`, and `atmon.keys`. They are standard typed clients, so methods are camelCase and fields follow the contract. ```ts const { authorizationUrl } = await atmon.connections.initiateConnection({ entityId: "user-42", toolkitSlug: "github", }); ``` --- # API reference Everything on these pages is a POST with a JSON body. You need an HTTP client and a project key; there is nothing else to install. If your assistant is calling atmon rather than your own code, read [For your AI](../../for-your-ai/index.md) instead, and if you want a typed client, the [SDKs](../../sdks/index.md) wrap exactly what is here. ## How a call is made The address is the hosted backend, `https://api.atmon.ai`. A node your organization runs on its own machines answers the same paths on its own address. A path is the service name, then the call: ```http POST https://api.atmon.ai/automaton.v1.ExecutionService/ExecuteTool ``` Four rules cover the rest of it. 1. **Authenticate with the project key.** `Authorization: Bearer amk_...`. The key resolves to one project, so no request body names a project. Keys come from the console key page or from `CreateKey`. 2. **Send `Content-Type: application/json`.** The body is one JSON object. 3. **Field names are lowerCamelCase.** The reference writes fields the way the contract declares them, `tool_slug`, and the wire name is `toolSlug`. Enum values keep their declared spelling, `TOOL_CALL_STATUS_SUCCEEDED`. Timestamps are RFC 3339 strings. 4. **Read the body, not just the status line.** A call that was refused is a 200 with a refusal in it; see Errors below. ## A worked call Find a tool by intent, then run it. Two requests: ```http POST /automaton.v1.RouterService/ResolveTools HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "intent": "file a bug about the failing build", "entityId": "user-42", "maxTools": 5 } ``` The answer carries a `resolutionId` and a ranked slate. Pass both into the call: ```http POST /automaton.v1.ExecutionService/ExecuteTool HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json Idempotency-Key: issue-build-red-1 { "toolSlug": "github.create_issue", "entityId": "user-42", "resolutionId": "res_01H...", "argumentsJson": "{\"owner\":\"rudrite\",\"repo\":\"automaton\",\"title\":\"build is red\"}" } ``` **Confirming a destructive call.** A tool whose access class is destructive is refused until the call carries `"confirm": true`, and that flag goes inside `argumentsJson` beside the tool's own fields, not beside `toolSlug`. The request itself declares no confirm field, so one sent at the top level is ignored and the call is refused with `denied`. Deleting the issue above would send `"argumentsJson": "{\"owner\":\"rudrite\",\"repo\":\"automaton\",\"issue_number\":7,\"confirm\":true}"`. ## Idempotency Anything that changes the outside world takes an idempotency key. Send the same key with the same arguments and the second request returns the first request's record instead of acting again, which is what makes a retry after a timeout safe. Send the same key with different arguments and the call is refused rather than guessed at. Choose a key that is stable for the work rather than for the attempt: the id of the row you are acting on, not a fresh identifier per retry. Keys are scoped to your project. ## Errors A transport error means the request never reached a decision: bad JSON, a missing key, the wrong path. Everything else comes back as a normal answer whose body says what happened. A tool call that was denied, parked on an approval, or rate limited is a successful response carrying a `status` and an `error_code`. Switch on the code. Every code, when it fires, and whether the same call is worth retrying is in [Error codes](../errors.md); what each limit is and which code it raises is in [Limits](../limits.md). ## What you can call | Service | Summary | | --- | --- | | [RouterService](./router.md) | Find the tool for a task by describing the task. | | [CatalogService](./catalog.md) | Read what exists: the apps atmon can reach, the tools each one brings, and one tool's full definition. | | [ExecutionService](./execution.md) | Run one tool, and read back what happened. | | [JobsService](./jobs.md) | Hand over work that is bigger than one call: analyze ten thousand rows, move forty thousand files, write to a hundred records. | | [ConnectionsService](./connections.md) | Connect an account on behalf of one of your users, read the state of one, and disconnect it. | | [KeysService](./keys.md) | The project's own API keys: mint one, list them, retire one. | | [TriggersService](./triggers.md) | The inbound direction: an external app has something happen, and you hear about it. | | [TracesService](./traces.md) | Read back one decision and everything that ran under it. | | [UsageService](./usage.md) | What this project has used, aggregated from the same receipts everything else reads. | This list is a reviewed one. atmon runs more services than these, and the rest are how the product is operated rather than an interface you build against, so they are not documented here and are not part of what we keep stable for you. --- # CatalogService Read what exists: the apps atmon can reach, the tools each one brings, and one tool's full definition. Most callers reach this through search rather than by browsing, and use it to draw their own connect screen. Every call is a POST to `https://api.atmon.ai/automaton.v1.CatalogService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `ListToolkits` | `ListToolkitsRequest` | `ListToolkitsResponse` | Lists the toolkits the caller can see: the shared catalog, plus the caller's own private toolkits when a project is named. | | `GetToolkit` | `GetToolkitRequest` | `GetToolkitResponse` | Reads one toolkit by slug. | | `ListTools` | `ListToolsRequest` | `ListToolsResponse` | Lists tools, optionally narrowed to one toolkit or one kind. | | `GetTool` | `GetToolRequest` | `GetToolResponse` | Reads one tool's full definition by its catalog-wide slug: description, both JSON Schemas, required scopes, and access class. | | `GetToolkitConnectSpec` | `GetToolkitConnectSpecRequest` | `GetToolkitConnectSpecResponse` | Reads what a connect form for one toolkit needs: the templated base URL, the account variables the address is built from, and the schemes a connection can be made under. | ### ListToolkits Lists the toolkits the caller can see: the shared catalog, plus the caller's own private toolkits when a project is named. Request `ListToolkitsRequest`, response `ListToolkitsResponse`. ```http POST /automaton.v1.CatalogService/ListToolkits HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "projectId": "..." } ``` The response: ```json { "toolkits": [{ "slug": "...", "name": "...", "description": "...", "version": "...", "authSchemes": ["..."], "ownerProjectId": "...", "category": "...", "connectable": true, "connectability": "TOOLKIT_CONNECTABILITY_READY" }] } ``` ### GetToolkit Reads one toolkit by slug. A project-owned entry wins over a shared entry with the same slug. Request `GetToolkitRequest`, response `GetToolkitResponse`. ```http POST /automaton.v1.CatalogService/GetToolkit HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "slug": "...", "projectId": "..." } ``` The response: ```json { "toolkit": { "slug": "...", "name": "...", "description": "...", "version": "...", "authSchemes": ["..."], "ownerProjectId": "...", "category": "...", "connectable": true, "connectability": "TOOLKIT_CONNECTABILITY_READY" } } ``` ### ListTools Lists tools, optionally narrowed to one toolkit or one kind. An agent should resolve an intent through the router rather than enumerate this. Request `ListToolsRequest`, response `ListToolsResponse`. ```http POST /automaton.v1.CatalogService/ListTools HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "toolkitSlug": "...", "kind": "TOOL_KIND_ACTION", "projectId": "..." } ``` The response: ```json { "tools": [{ "slug": "...", "toolkitSlug": "...", "kind": "TOOL_KIND_ACTION", "description": "...", "inputSchemaJson": "{}", "outputSchemaJson": "{}", "requiredScopes": ["..."], "ownerProjectId": "...", "accessClass": "..." }] } ``` ### GetTool Reads one tool's full definition by its catalog-wide slug: description, both JSON Schemas, required scopes, and access class. This is what the MCP describe_tool meta-tool answers with. Request `GetToolRequest`, response `GetToolResponse`. ```http POST /automaton.v1.CatalogService/GetTool HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "slug": "...", "projectId": "..." } ``` The response: ```json { "tool": { "slug": "...", "toolkitSlug": "...", "kind": "TOOL_KIND_ACTION", "description": "...", "inputSchemaJson": "{}", "outputSchemaJson": "{}", "requiredScopes": ["..."], "ownerProjectId": "...", "accessClass": "..." } } ``` ### GetToolkitConnectSpec Reads what a connect form for one toolkit needs: the templated base URL, the account variables the address is built from, and the schemes a connection can be made under. It carries no credential and no endpoint a credential is presented to. Request `GetToolkitConnectSpecRequest`, response `GetToolkitConnectSpecResponse`. ```http POST /automaton.v1.CatalogService/GetToolkitConnectSpec HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "slug": "...", "projectId": "..." } ``` The response: ```json { "spec": { "toolkitSlug": "...", "baseUrlTemplate": "...", "accountVariables": [{ ... }], "authSchemes": [{ ... }] } } ``` ## Messages ### AccountVariableSpec AccountVariableSpec is one entry of a toolkit's account_variables: a fact about the customer's own deployment that the connection captures and the base URL renders. Description is the sentence the connecting person is asked for it with, and Example is a well-formed answer. | Field | Type | # | Notes | | --- | --- | --- | --- | | `name` | `string` | 1 | | | `description` | `string` | 2 | | | `example` | `string` | 3 | empty when the toolkit declared none | | `decides_origin` | `bool` | 4 | Whether this value decides the address the deployment dials: the registrable domain, the host, or the port. False means it fills a label under a host the toolkit itself wrote down. InitiateConnection requires the approver role when a toolkit declares one, so a form that asks for it can say so before anyone fills it in. | ### GetToolRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `slug` | `string` | 1 | | | `project_id` | `string` | 2 | Set to a project id to also see that project's private tools. Empty resolves against the shared catalog only. | ### GetToolResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `tool` | `Tool` | 1 | | ### GetToolkitConnectSpecRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `slug` | `string` | 1 | | | `project_id` | `string` | 2 | Set to a project id to also resolve that project's private toolkits. Empty resolves against the shared catalog only. | ### GetToolkitConnectSpecResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `spec` | `ToolkitConnectSpec` | 1 | | ### GetToolkitRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `slug` | `string` | 1 | | | `project_id` | `string` | 2 | Set to a project id to also see that project's private toolkits. Empty resolves against the shared catalog only. | ### GetToolkitResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkit` | `Toolkit` | 1 | | ### ListToolkitsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `project_id` | `string` | 1 | Set to a project id to list the shared catalog plus that project's private toolkits. Empty lists the shared catalog only. | ### ListToolkitsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkits` | repeated `Toolkit` | 1 | | ### ListToolsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkit_slug` | `string` | 1 | empty lists across all toolkits | | `kind` | `ToolKind` | 2 | unspecified lists both kinds | | `project_id` | `string` | 3 | Set to a project id to list shared tools plus that project's private tools. Empty lists shared tools only. | ### ListToolsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `tools` | repeated `Tool` | 1 | | ### Tool | Field | Type | # | Notes | | --- | --- | --- | --- | | `slug` | `string` | 1 | "github.create_issue" | | `toolkit_slug` | `string` | 2 | | | `kind` | `ToolKind` | 3 | | | `description` | `string` | 4 | LLM-facing, tuned for routing and selection | | `input_schema_json` | `string` | 5 | JSON Schema for arguments | | `output_schema_json` | `string` | 6 | JSON Schema for results | | `required_scopes` | repeated `string` | 7 | | | `owner_project_id` | `string` | 8 | empty means the shared catalog | | `access_class` | `string` | 9 | Blast radius: read \| write \| destructive, from the toolkit definition. Execution's mutation gate keys off it, so a caller can tell before it calls which tools hold until the agent passes confirm. | ### Toolkit | Field | Type | # | Notes | | --- | --- | --- | --- | | `slug` | `string` | 1 | "github", "slack" | | `name` | `string` | 2 | | | `description` | `string` | 3 | | | `version` | `string` | 4 | version of the toolkit definition, not the app | | `auth_schemes` | repeated `string` | 5 | "oauth2", "api_key", "none" | | `owner_project_id` | `string` | 6 | empty means the shared catalog | | `category` | `string` | 7 | Browsing taxonomy slug, one of twelve closed-set categories: "communication", "work-tracking", "developer-infrastructure", and so on. Every toolkit carries exactly one; the catalog loader refuses a definition that names none or names one outside the set. | | `connectable` | `bool` | 8 | Whether an account can be created for this toolkit on this deployment at all. A client reads this rather than deriving it from connectability, so a value it does not recognize leaves the app offered rather than silently disabled. | | `connectability` | `ToolkitConnectability` | 9 | Why connectable reads the way it does. | ### ToolkitAuthSchemeSpec ToolkitAuthSchemeSpec is one scheme a connection to the toolkit can be made under, with the fields that scheme's form needs. The type-specific fields are empty for the types they do not apply to. | Field | Type | # | Notes | | --- | --- | --- | --- | | `type` | `string` | 1 | oauth2 \| api_key \| basic \| none | | `default_scopes` | repeated `string` | 2 | oauth2: what the consent request asks for | | `key_placement` | `string` | 3 | api_key: header \| query | | `key_name` | `string` | 4 | api_key: the header or query-parameter name | ### ToolkitConnectSpec ToolkitConnectSpec is what a connect form for one toolkit is built from: where its calls go, the per-customer values the address is built from, and the schemes a connection can be made under. The scheme list is narrower than Toolkit.auth_schemes, which reports every scheme the toolkit declares. A client_credentials scheme is absent here because no connected account is made under it: the project holds that credential and InitiateConnection refuses the scheme by name. | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkit_slug` | `string` | 1 | | | `base_url_template` | `string` | 2 | The declared base_url with its {{account.}} placeholders still in it, so a form can render the host it is building as the values are typed. | | `account_variables` | repeated `AccountVariableSpec` | 3 | in declaration order | | `auth_schemes` | repeated `ToolkitAuthSchemeSpec` | 4 | in declaration order | ## Enums ### ToolKind | Value | # | Meaning | | --- | --- | --- | | `TOOL_KIND_UNSPECIFIED` | 0 | | | `TOOL_KIND_ACTION` | 1 | agent-initiated call into the external app | | `TOOL_KIND_TRIGGER` | 2 | app-initiated event delivered to the agent | | `TOOL_KIND_CODE` | 3 | agent-initiated call into code this platform runs: a sandboxed module, or a job program a project promoted from one of its own finished jobs (a project skill). | ### ToolkitConnectability ToolkitConnectability says what stands between a toolkit and a connected account on this deployment. It is deployment state, not catalog data: the same toolkit definition answers differently on a node whose operator has registered its OAuth app and on a node that has not. | Value | # | Meaning | | --- | --- | --- | | `TOOLKIT_CONNECTABILITY_UNSPECIFIED` | 0 | | | `TOOLKIT_CONNECTABILITY_READY` | 1 | Every scheme the toolkit declares can be connected here. | | `TOOLKIT_CONNECTABILITY_NEEDS_OAUTH_REGISTRATION` | 2 | The toolkit declares only schemes that need an OAuth client id and secret registered on this deployment, and it holds none. Nothing can connect. | | `TOOLKIT_CONNECTABILITY_OAUTH_PENDING` | 3 | The toolkit also declares a scheme that needs no operator registration (api_key, basic), so it connects today under that one, while its oauth2 path waits on a registration. | --- # ConnectionsService Connect an account on behalf of one of your users, read the state of one, and disconnect it. Your code never sees a credential: it starts a flow, sends the person to the address that comes back, and afterwards holds an account id. Every call is a POST to `https://api.atmon.ai/automaton.v1.ConnectionsService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `InitiateConnection` | `InitiateConnectionRequest` | `InitiateConnectionResponse` | Starts an auth flow for one entity and one toolkit. | | `SubmitAPIKey` | `SubmitAPIKeyRequest` | `SubmitAPIKeyResponse` | Completes an api_key connection. | | `SubmitBasicAuth` | `SubmitBasicAuthRequest` | `SubmitBasicAuthResponse` | Completes a basic connection. | | `GetConnectedAccount` | `GetConnectedAccountRequest` | `GetConnectedAccountResponse` | Reads one connected account's status and granted scopes. | | `ListConnectedAccounts` | `ListConnectedAccountsRequest` | `ListConnectedAccountsResponse` | Lists an entity's connected accounts, optionally for one toolkit. | | `RevokeConnectedAccount` | `RevokeConnectedAccountRequest` | `RevokeConnectedAccountResponse` | Revokes an account: the stored credential is dropped and the provider's revocation endpoint is called where the toolkit declares one. | | `UpdateAccountVariables` | `UpdateAccountVariablesRequest` | `UpdateAccountVariablesResponse` | Corrects an account's account_variables in place, so a typo in a subdomain is an edit rather than a reconnection. | ### InitiateConnection Starts an auth flow for one entity and one toolkit. For an OAuth toolkit it answers the authorization URL to send the end user to; the account stays PENDING until the flow completes, and the flow expires after ten minutes. The project's scope caps are enforced here, before a consent URL exists, because an approved scope is a real grant whatever happens afterward. Request `InitiateConnectionRequest`, response `InitiateConnectionResponse`. ```http POST /automaton.v1.ConnectionsService/InitiateConnection HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "toolkitSlug": "...", "requestedScopes": ["..."], "redirectUri": "...", "credentialKind": "...", "accountVariables": {"...": "..."}, "authScheme": "..." } ``` The response: ```json { "connectedAccountId": "...", "authorizationUrl": "..." } ``` ### SubmitAPIKey Completes an api_key connection. It is the only RPC that carries a raw credential, and it carries it one way: the account goes ACTIVE and the key is readable by nothing afterwards. Request `SubmitAPIKeyRequest`, response `SubmitAPIKeyResponse`. ```http POST /automaton.v1.ConnectionsService/SubmitAPIKey HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "connectedAccountId": "...", "apiKey": "..." } ``` The response: ```json {} ``` ### SubmitBasicAuth Completes a basic connection. It carries the two halves of an HTTP basic credential the same way SubmitAPIKey carries one key: one way, into the vault, and the account goes ACTIVE. Request `SubmitBasicAuthRequest`, response `SubmitBasicAuthResponse`. ```http POST /automaton.v1.ConnectionsService/SubmitBasicAuth HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "connectedAccountId": "...", "username": "...", "password": "..." } ``` The response: ```json {} ``` ### GetConnectedAccount Reads one connected account's status and granted scopes. No method on this service ever returns credential material. Request `GetConnectedAccountRequest`, response `GetConnectedAccountResponse`. ```http POST /automaton.v1.ConnectionsService/GetConnectedAccount HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json { "connectedAccount": { "id": "...", "entityId": "...", "toolkitSlug": "...", "status": "CONNECTION_STATUS_PENDING", "grantedScopes": ["..."], "createdAt": "2026-01-31T09:15:00Z", "credentialKind": "...", "accountVariables": {"...": "..."}, "authScheme": "..." } } ``` ### ListConnectedAccounts Lists an entity's connected accounts, optionally for one toolkit. This is how to tell in advance whether a call would answer not_connected. Request `ListConnectedAccountsRequest`, response `ListConnectedAccountsResponse`. ```http POST /automaton.v1.ConnectionsService/ListConnectedAccounts HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "toolkitSlug": "..." } ``` The response: ```json { "connectedAccounts": [{ "id": "...", "entityId": "...", "toolkitSlug": "...", "status": "CONNECTION_STATUS_PENDING", "grantedScopes": ["..."], "createdAt": "2026-01-31T09:15:00Z", "credentialKind": "...", "accountVariables": {"...": "..."}, "authScheme": "..." }] } ``` ### RevokeConnectedAccount Revokes an account: the stored credential is dropped and the provider's revocation endpoint is called where the toolkit declares one. A provider that refuses the revocation is logged, not surfaced; the account is revoked locally either way. Request `RevokeConnectedAccountRequest`, response `RevokeConnectedAccountResponse`. ```http POST /automaton.v1.ConnectionsService/RevokeConnectedAccount HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json {} ``` ### UpdateAccountVariables Corrects an account's account_variables in place, so a typo in a subdomain is an edit rather than a reconnection. It changes where this account's calls go, never what its credential is, and it refuses a variable that decides the dialled origin: that value chose the server the account's credential is presented to, and moving it afterwards would present a live credential to a server the provider never issued it for. Changing the origin is a new connection. Request `UpdateAccountVariablesRequest`, response `UpdateAccountVariablesResponse`. ```http POST /automaton.v1.ConnectionsService/UpdateAccountVariables HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "connectedAccountId": "...", "accountVariables": {"...": "..."} } ``` The response: ```json { "connectedAccount": { "id": "...", "entityId": "...", "toolkitSlug": "...", "status": "CONNECTION_STATUS_PENDING", "grantedScopes": ["..."], "createdAt": "2026-01-31T09:15:00Z", "credentialKind": "...", "accountVariables": {"...": "..."}, "authScheme": "..." } } ``` ## Messages ### ConnectedAccount | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `entity_id` | `string` | 2 | | | `toolkit_slug` | `string` | 3 | | | `status` | `ConnectionStatus` | 4 | | | `granted_scopes` | repeated `string` | 5 | | | `created_at` | `google.protobuf.Timestamp` | 6 | | | `credential_kind` | `string` | 7 | credential_kind is set instead of toolkit_slug when the account holds a credential for something that is not a catalog toolkit, such as a model provider. Exactly one of the two is ever set. | | `account_variables` | map<`string`, `string`> | 8 | account_variables are the per-customer parts of the toolkit's base_url this account answered: a subdomain, an application id, a cluster address. They are not secret, which is why they come back on a read: they say where this account's calls go, never what the credential is. | | `auth_scheme` | `string` | 9 | auth_scheme is the toolkit auth scheme this account was created under ("oauth2", "api_key", or "basic"), which is fixed for its life: the vault holds one credential of that shape. Connecting the same toolkit under another scheme is another account. No account is ever created under "client_credentials": that credential belongs to the project, so there is nothing per-entity to connect. | ### GetConnectedAccountRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### GetConnectedAccountResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account` | `ConnectedAccount` | 1 | | ### InitiateConnectionRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | | | `toolkit_slug` | `string` | 2 | | | `requested_scopes` | repeated `string` | 3 | empty requests the toolkit default | | `redirect_uri` | `string` | 4 | where the end user lands after consent | | `credential_kind` | `string` | 5 | credential_kind names a non-catalog credential subject, from a closed set the server registers (model_provider:openai, model_provider:anthropic). It is mutually exclusive with toolkit_slug, takes the api_key path only, and accepts neither scopes nor a redirect: there is no provider app to consent to and no end user to send anywhere. An unregistered kind is refused. | | `account_variables` | map<`string`, `string`> | 6 | account_variables answers the toolkit's account_variables block, keyed by declared name. Every variable the toolkit declares must be present and no other name may be: a missing one would leave a placeholder in the host of every call, and an extra one would sit on the account unused. | | `auth_scheme` | `string` | 7 | auth_scheme names which of the toolkit's declared auth schemes to connect under, by type ("oauth2", "api_key", or "basic"). Empty takes the toolkit's default, which is the first scheme its catalog entry declares that this deployment can run and that an entity can connect under. A scheme the toolkit does not declare is refused rather than substituted, and the account records what it was created under. "client_credentials" is refused here: the project holds that credential, so a call under it needs no account. | ### InitiateConnectionResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account_id` | `string` | 1 | status PENDING until the flow completes | | `authorization_url` | `string` | 2 | send the end user here for OAuth toolkits | ### ListConnectedAccountsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | | | `toolkit_slug` | `string` | 2 | empty lists all toolkits for the entity | ### ListConnectedAccountsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_accounts` | repeated `ConnectedAccount` | 1 | | ### RevokeConnectedAccountRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### RevokeConnectedAccountResponse No fields. The call takes its scope from the authenticated project. ### SubmitAPIKeyRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account_id` | `string` | 1 | connected_account_id is a PENDING account InitiateConnection returned under the api_key scheme, either for an api_key toolkit or for a credential kind. | | `api_key` | `string` | 2 | api_key is the raw credential. It is write-only in the strict sense: it is sealed into the vault on arrival, no read on this service or any other ever returns it, and it is never written to a log line or a trace. Send it over TLS; a request that carries it should not be recorded by a proxy. | ### SubmitAPIKeyResponse No fields. The call takes its scope from the authenticated project. ### SubmitBasicAuthRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account_id` | `string` | 1 | connected_account_id is a PENDING account InitiateConnection returned under the basic scheme. | | `username` | `string` | 2 | username and password are the two halves of the HTTP basic credential, and both are write-only in the same strict sense as api_key above: sealed into the vault on arrival, returned by no read, written to no log line and no trace. An empty password is accepted, because providers that carry the whole secret in the username half are common. A username holding a colon is refused: RFC 7617 splits the pair on the first colon, so the provider would read a different pair from the one sent. | | `password` | `string` | 3 | | ### SubmitBasicAuthResponse No fields. The call takes its scope from the authenticated project. ### UpdateAccountVariablesRequest UpdateAccountVariablesRequest corrects the per-customer parts of an existing account's address. A mistyped subdomain otherwise costs a revoke and a reconnect, which for an OAuth account means sending the end user through consent again over a typo. | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account_id` | `string` | 1 | | | `account_variables` | map<`string`, `string`> | 2 | account_variables replaces the whole set, keyed by declared name, under the same rule InitiateConnection applies: every variable the toolkit declares must be present and no other name may be. A merge would let a caller send one name and leave the account holding a value nobody has looked at since it was first typed. | ### UpdateAccountVariablesResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `connected_account` | `ConnectedAccount` | 1 | | ## Enums ### ConnectionStatus | Value | # | Meaning | | --- | --- | --- | | `CONNECTION_STATUS_UNSPECIFIED` | 0 | | | `CONNECTION_STATUS_PENDING` | 1 | auth flow started, not completed | | `CONNECTION_STATUS_ACTIVE` | 2 | | | `CONNECTION_STATUS_EXPIRED` | 3 | refresh failed, re-auth required | | `CONNECTION_STATUS_REVOKED` | 4 | | --- # ExecutionService Run one tool, and read back what happened. Every call leaves a receipt, refusals included, and the receipt is what these calls return: the status, the result, and an error code that is empty on success. Nothing here throws when a call is refused; a refusal is an answer. Every call is a POST to `https://api.atmon.ai/automaton.v1.ExecutionService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `ExecuteTool` | `ExecuteToolRequest` | `ExecuteToolResponse` | Runs one tool for one entity, in a fixed order: resolution against the project's pinned catalog snapshot, mutation gate, policy gate, the resource lease, credential resolution, provider rate-limit pacing, the HTTP call (retried only for read tools), response shaping, then the ledger write. | | `GetToolCall` | `GetToolCallRequest` | `GetToolCallResponse` | Reads one ledger row by id. | | `ListToolCalls` | `ListToolCallsRequest` | `ListToolCallsResponse` | Lists an entity's ledger rows, newest first, optionally for one tool. | ### ExecuteTool Runs one tool for one entity, in a fixed order: resolution against the project's pinned catalog snapshot, mutation gate, policy gate, the resource lease, credential resolution, provider rate-limit pacing, the HTTP call (retried only for read tools), response shaping, then the ledger write. A refusal is a successful call whose ToolCall says it was refused, not an error: a gate is an answer. A denied or parked call resolves no credential and reaches no external app. Request `ExecuteToolRequest`, response `ExecuteToolResponse`. ```http POST /automaton.v1.ExecutionService/ExecuteTool HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "toolSlug": "...", "argumentsJson": "{}", "idempotencyKey": "...", "resolutionId": "...", "approvalId": "..." } ``` The response: ```json { "toolCall": { "id": "...", "entityId": "...", "toolSlug": "...", "connectedAccountId": "...", "argumentsJson": "{}", "status": "TOOL_CALL_STATUS_RUNNING", "resultJson": "{}", "errorCode": "...", "errorDetail": "...", "startedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "approvalId": "...", "resultTruncated": true, "resourceUrn": "...", "leaseWaitMs": 0, "principalChain": ["..."], "jobId": "...", "stepId": "...", "relayId": "..." } } ``` ### GetToolCall Reads one ledger row by id. Request `GetToolCallRequest`, response `GetToolCallResponse`. ```http POST /automaton.v1.ExecutionService/GetToolCall HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json { "toolCall": { "id": "...", "entityId": "...", "toolSlug": "...", "connectedAccountId": "...", "argumentsJson": "{}", "status": "TOOL_CALL_STATUS_RUNNING", "resultJson": "{}", "errorCode": "...", "errorDetail": "...", "startedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "approvalId": "...", "resultTruncated": true, "resourceUrn": "...", "leaseWaitMs": 0, "principalChain": ["..."], "jobId": "...", "stepId": "...", "relayId": "..." } } ``` ### ListToolCalls Lists an entity's ledger rows, newest first, optionally for one tool. Every outcome is a row: successes, failures, denials, and parked calls alike. Request `ListToolCallsRequest`, response `ListToolCallsResponse`. ```http POST /automaton.v1.ExecutionService/ListToolCalls HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "toolSlug": "...", "pageSize": 0, "pageToken": "...", "principalChainPrefix": ["..."] } ``` The response: ```json { "toolCalls": [{ "id": "...", "entityId": "...", "toolSlug": "...", "connectedAccountId": "...", "argumentsJson": "{}", "status": "TOOL_CALL_STATUS_RUNNING", "resultJson": "{}", "errorCode": "...", "errorDetail": "...", "startedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "approvalId": "...", "resultTruncated": true, "resourceUrn": "...", "leaseWaitMs": 0, "principalChain": ["..."], "jobId": "...", "stepId": "...", "relayId": "..." }], "nextPageToken": "..." } ``` ## Messages ### ExecuteToolRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | | | `tool_slug` | `string` | 2 | | | `arguments_json` | `string` | 3 | | | `idempotency_key` | `string` | 4 | same key returns the original call, not a rerun | | `resolution_id` | `string` | 5 | links the call to the router decision, if any | | `approval_id` | `string` | 6 | An approved policy approval, releasing a call that parked earlier. It is single use and only covers the same tool with the same arguments. | ### ExecuteToolResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `tool_call` | `ToolCall` | 1 | | ### GetToolCallRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### GetToolCallResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `tool_call` | `ToolCall` | 1 | | ### ListToolCallsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | | | `tool_slug` | `string` | 2 | empty lists all tools | | `page_size` | `int32` | 3 | | | `page_token` | `string` | 4 | | | `principal_chain_prefix` | repeated `string` | 140 | principal_chain_prefix lists every call whose chain opens with these hops, in this order: one key's effects, one skill's, one job's, one step's. It is as narrow a filter as entity_id and it crosses entities, because a job does, so a request carrying it may leave entity_id empty. A request carrying neither is refused. | ### ListToolCallsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `tool_calls` | repeated `ToolCall` | 1 | | | `next_page_token` | `string` | 2 | | ### ToolCall | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `entity_id` | `string` | 2 | | | `tool_slug` | `string` | 3 | | | `connected_account_id` | `string` | 4 | | | `arguments_json` | `string` | 5 | | | `status` | `ToolCallStatus` | 6 | | | `result_json` | `string` | 7 | | | `error_code` | `string` | 8 | "rate_limited", "auth_expired", "invalid_arguments", ... | | `error_detail` | `string` | 9 | | | `started_at` | `google.protobuf.Timestamp` | 10 | | | `finished_at` | `google.protobuf.Timestamp` | 11 | | | `approval_id` | `string` | 12 | The policy approval this call waits on when status is PENDING_APPROVAL, or the approval the call was released by. Empty when no gate applied. | | `result_truncated` | `bool` | 13 | A shaping cap cut data the tool's output schema declared. The proto3 default carries the meaning that matters: absent means complete. | | `resource_urn` | `string` | 110 | The resource this call leased, rendered from the tool's lease_key rule (docs/toolkit-format.md, Resource leases). Empty when the tool declares no rule, which is most of the catalog. A call refused with resource_leased carries it too, so a contention names what it could not have. | | `lease_wait_ms` | `int64` | 111 | How long taking that lease took, in milliseconds. Zero for a call that leased nothing. | | `principal_chain` | repeated `string` | 140 | Who ultimately caused this call, oldest cause first: "key:", "skill:", "job:", "step:", "account:". It is an audit record and never an authorization input. Empty for a row written before chains existed; those rows recorded no cause. | | `job_id` | `string` | 141 | The job step this call ran for, empty for a call made directly over the wire. Both are recorded by the engine and neither is ever read from a request: a caller-supplied job id would let one caller attribute its effects to another's job. | | `step_id` | `string` | 142 | | | `relay_id` | `string` | 150 | The on-prem relay that executed this call, empty for every call the platform sent itself. A row carrying it carries no connected_account_id: a relay call resolves none, because the credential it used never left the customer's network. | ## Enums ### ToolCallStatus | Value | # | Meaning | | --- | --- | --- | | `TOOL_CALL_STATUS_UNSPECIFIED` | 0 | | | `TOOL_CALL_STATUS_RUNNING` | 1 | | | `TOOL_CALL_STATUS_SUCCEEDED` | 2 | | | `TOOL_CALL_STATUS_FAILED` | 3 | | | `TOOL_CALL_STATUS_DENIED` | 4 | blocked by policy before reaching the app | | `TOOL_CALL_STATUS_PENDING_APPROVAL` | 5 | parked on the policy approval gate | --- # JobsService Hand over work that is bigger than one call: analyze ten thousand rows, move forty thousand files, write to a hundred records. You submit a plan and read receipts; the rows themselves stay on our side and never travel back to you. A job outlives the request that started it, so it is submitted once and read back later. Every call is a POST to `https://api.atmon.ai/automaton.v1.JobsService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `SubmitJob` | `SubmitJobRequest` | `SubmitJobResponse` | | | `GetJob` | `GetJobRequest` | `GetJobResponse` | | | `ListJobs` | `ListJobsRequest` | `ListJobsResponse` | | | `ListJobEvents` | `ListJobEventsRequest` | `ListJobEventsResponse` | ListJobEvents reads one job's log in sequence order, each entry naming the chain that caused it. | | `AnswerAsk` | `AnswerAskRequest` | `AnswerAskResponse` | AnswerAsk is how a person answers a question a job addressed to them. | | `ListAsks` | `ListAsksRequest` | `ListAsksResponse` | ListAsks reads the questions a project has addressed to people: what is outstanding, who holds it, what was answered and by whom. | | `GetAsk` | `GetAskRequest` | `GetAskResponse` | GetAsk reads one of them. | ### SubmitJob Request `SubmitJobRequest`, response `SubmitJobResponse`. ```http POST /automaton.v1.JobsService/SubmitJob HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "programJson": "{}", "idempotencyKey": "...", "mode": "...", "simulateFirst": true } ``` The response: ```json { "job": { "id": "...", "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "programJson": "{}", "programHash": "...", "estimate": { ... }, "planApprovalId": "...", "park": { ... }, "stepReceipts": [{ ... }], "receipt": { ... }, "lastError": "...", "createdAt": "2026-01-31T09:15:00Z", "updatedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "pendingAsk": { ... }, "saga": { ... }, "mode": "...", "simulatedFrom": "...", "skillSlug": "..." } } ``` ### GetJob Request `GetJobRequest`, response `GetJobResponse`. ```http POST /automaton.v1.JobsService/GetJob HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json { "job": { "id": "...", "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "programJson": "{}", "programHash": "...", "estimate": { ... }, "planApprovalId": "...", "park": { ... }, "stepReceipts": [{ ... }], "receipt": { ... }, "lastError": "...", "createdAt": "2026-01-31T09:15:00Z", "updatedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "pendingAsk": { ... }, "saga": { ... }, "mode": "...", "simulatedFrom": "...", "skillSlug": "..." }, "simulation": { "id": "...", "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "programJson": "{}", "programHash": "...", "estimate": { ... }, "planApprovalId": "...", "park": { ... }, "stepReceipts": [{ ... }], "receipt": { ... }, "lastError": "...", "createdAt": "2026-01-31T09:15:00Z", "updatedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "pendingAsk": { ... }, "saga": { ... }, "mode": "...", "simulatedFrom": "...", "skillSlug": "..." } } ``` ### ListJobs Request `ListJobsRequest`, response `ListJobsResponse`. ```http POST /automaton.v1.JobsService/ListJobs HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "pageSize": 0, "pageToken": "...", "mode": "...", "skillSlug": "..." } ``` The response: ```json { "jobs": [{ "id": "...", "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "programJson": "{}", "programHash": "...", "estimate": { ... }, "planApprovalId": "...", "park": { ... }, "stepReceipts": [{ ... }], "receipt": { ... }, "lastError": "...", "createdAt": "2026-01-31T09:15:00Z", "updatedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "pendingAsk": { ... }, "saga": { ... }, "mode": "...", "simulatedFrom": "...", "skillSlug": "..." }], "nextPageToken": "..." } ``` ### ListJobEvents ListJobEvents reads one job's log in sequence order, each entry naming the chain that caused it. It is the attribution surface over the engine, and a job of another project reads as missing. Request `ListJobEventsRequest`, response `ListJobEventsResponse`. ```http POST /automaton.v1.JobsService/ListJobEvents HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "jobId": "..." } ``` The response: ```json { "events": [{ "jobId": "...", "seq": 0, "type": "...", "stepId": "...", "at": "2026-01-31T09:15:00Z", "payloadJson": "{}", "principalChain": ["..."] }] } ``` ### AnswerAsk AnswerAsk is how a person answers a question a job addressed to them. Request `AnswerAskRequest`, response `AnswerAskResponse`. ```http POST /automaton.v1.JobsService/AnswerAsk HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "jobId": "...", "requestId": "...", "principalId": "...", "continuation": "...", "overridesJson": "{}", "note": "...", "deliveryId": "..." } ``` The response: ```json { "job": { "id": "...", "status": "JOB_STATUS_PENDING_PLAN_APPROVAL", "programJson": "{}", "programHash": "...", "estimate": { ... }, "planApprovalId": "...", "park": { ... }, "stepReceipts": [{ ... }], "receipt": { ... }, "lastError": "...", "createdAt": "2026-01-31T09:15:00Z", "updatedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "pendingAsk": { ... }, "saga": { ... }, "mode": "...", "simulatedFrom": "...", "skillSlug": "..." } } ``` ### ListAsks ListAsks reads the questions a project has addressed to people: what is outstanding, who holds it, what was answered and by whom. Until it existed an ask was visible only by listing jobs and reading pending_ask, so an answered one was visible only by reading a log. Request `ListAsksRequest`, response `ListAsksResponse`. ```http POST /automaton.v1.JobsService/ListAsks HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "state": "ASK_STATE_PENDING", "principalId": "...", "jobId": "...", "standingProgramId": "...", "pageSize": 0, "pageToken": "..." } ``` The response: ```json { "asks": [{ "pointId": "...", "principalId": "...", "hopIndex": 0, "deliveredPrincipals": ["..."], "toolSlug": "...", "entityId": "...", "toolCallId": "...", "deliveredAt": "2026-01-31T09:15:00Z", "deadlineAt": "2026-01-31T09:15:00Z", "approvalId": "...", "exhausted": true, "jobId": "...", "requestId": "...", "state": "ASK_STATE_PENDING", "question": "...", "answerSchemaJson": "{}", "continuations": ["..."], "escalation": [{ ... }], "askedAt": "2026-01-31T09:15:00Z", "decision": { ... }, "stepId": "...", "delivery": "..." }], "nextPageToken": "..." } ``` ### GetAsk GetAsk reads one of them. Request `GetAskRequest`, response `GetAskResponse`. ```http POST /automaton.v1.JobsService/GetAsk HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "jobId": "...", "requestId": "..." } ``` The response: ```json { "ask": { "pointId": "...", "principalId": "...", "hopIndex": 0, "deliveredPrincipals": ["..."], "toolSlug": "...", "entityId": "...", "toolCallId": "...", "deliveredAt": "2026-01-31T09:15:00Z", "deadlineAt": "2026-01-31T09:15:00Z", "approvalId": "...", "exhausted": true, "jobId": "...", "requestId": "...", "state": "ASK_STATE_PENDING", "question": "...", "answerSchemaJson": "{}", "continuations": ["..."], "escalation": [{ ... }], "askedAt": "2026-01-31T09:15:00Z", "decision": { ... }, "stepId": "...", "delivery": "..." } } ``` ## Messages ### AnswerAskRequest AnswerAskRequest lands one principal's answer to an outstanding ask. The answer selects a continuation the program declared and nothing else, and the answering key comes from the authenticated request: an approval ask resolves its approval through policy, and policy refuses a key without the approver role and the very key whose plan parked there. | Field | Type | # | Notes | | --- | --- | --- | --- | | `job_id` | `string` | 1 | | | `request_id` | `string` | 2 | request_id is optional. Naming it refuses an answer to a question the job has already moved past. | | `principal_id` | `string` | 3 | principal_id is the contact answering. An answer from anyone the ask has not reached is refused. | | `continuation` | `string` | 4 | continuation is one of proceed, retry-with, skip, or abort, and it must be one the judgment point declared. | | `overrides_json` | `string` | 5 | overrides_json carries a retry-with answer's changes, in the shape the point's answer schema declares. | | `note` | `string` | 6 | | | `delivery_id` | `string` | 210 | delivery_id names the webhook delivery this answer came back on, taken from the X-Automaton-Delivery-Id header of the ask_raised delivery. It is recorded as the answer's surface and authorizes nothing: the answer is authorized by the presented key either way. Empty for an answer given directly, which records the surface as "api". Delivery is at-least-once and answering is at-most-once, so the same question can arrive twice. A second answer to an answered ask is refused, recorded on the job's log as an answer-refused entry, and moves nothing. | ### AnswerAskResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `job` | `Job` | 1 | | ### Ask Ask is one outstanding question addressed to a named principal: who holds it now, the channel call that carried it, and when the engine stops waiting. An ask is a judgment point whose handler is a principal, not a separate mechanism, so what releases the job is still an answer naming one of the continuations the program declared. The question text is not repeated here. It is on the pending judgment this ask belongs to, and a job carries one of each. | Field | Type | # | Notes | | --- | --- | --- | --- | | `point_id` | `string` | 1 | point_id is the judgment point the program declared, or "approval" when the ask is an approval gate the engine addressed to a person. | | `principal_id` | `string` | 2 | principal_id is the contact who holds the question now, which is the last hop the escalation ladder reached. | | `hop_index` | `int32` | 3 | | | `delivered_principals` | repeated `string` | 4 | delivered_principals lists every contact the ask has reached, in hop order. Only these may answer it. | | `tool_slug` | `string` | 5 | | | `entity_id` | `string` | 6 | | | `tool_call_id` | `string` | 7 | tool_call_id is the ledger row the delivery was recorded as. | | `delivered_at` | `google.protobuf.Timestamp` | 8 | | | `deadline_at` | `google.protobuf.Timestamp` | 9 | deadline_at is when this hop's timeout fires. Unset means the ask waits without a deadline, which is what an approval ask does: no clock approves. | | `approval_id` | `string` | 10 | approval_id is set when the ask is a policy approval gate. Answering it resolves that approval through policy, where separation of duty is enforced. | | `exhausted` | `bool` | 11 | exhausted reports an escalation ladder that ran out with no declared default: nothing more is delivered and a person owns the question. | | `job_id` | `string` | 200 | job_id and request_id are how an answer names this ask: AnswerAsk takes both, and request_id refuses an answer to a question the job has moved past. They and the fields below them are the read surface's (ListAsks, GetAsk); a job's pending_ask leaves them empty, because a job read carries the question on park.judgment beside it. | | `request_id` | `string` | 201 | | | `state` | `AskState` | 202 | | | `question` | `string` | 203 | question and answer_schema_json are the judgment point's own, repeated here because a reader of an ask list has no judgment beside it to read them from. A form is generated from the schema, so a program declaring a new question shape needs no client change. | | `answer_schema_json` | `string` | 204 | | | `continuations` | repeated `string` | 205 | continuations is what an answer may pick: proceed, retry-with, skip, abort, and only the ones the point declared. | | `escalation` | repeated `AskHopSpec` | 206 | escalation is the declared ladder, whether or not it has been climbed, so a reader sees who this question reaches next and when. | | `asked_at` | `google.protobuf.Timestamp` | 207 | | | `decision` | `AskDecision` | 208 | decision is set once the ask was settled, by a person or by the clock. | | `step_id` | `string` | 209 | | | `delivery` | `string` | 210 | delivery is what became of this ask's publication onto the registered ask endpoints: "delivered", "pending", or "dead-letter". Empty means nothing was published to an endpoint, which is what a project that registered none reads on every ask, and the channel call in tool_call_id is a separate surface either way. It is here so an ask that defaulted on its timeout with nobody reached reads differently from one a person did not answer. | ### AskDecision AskDecision is the recorded answer to an ask: which continuation it picked, who picked it, and where the answer arrived from. Every surface records the same entry, which is what keeps replay from a recorded answer a pure function. | Field | Type | # | Notes | | --- | --- | --- | --- | | `continuation` | `string` | 1 | continuation is one of proceed, retry-with, skip, or abort. | | `principal_id` | `string` | 2 | principal_id is the contact addressed that answered. Empty when the clock decided. | | `key_id` | `string` | 3 | key_id is the API key that presented the answer. A contact id is an address and authorizes nobody; the key is what was authorized, so an audit of "who decided" reads both. Empty when the clock decided, and on answers recorded before this field existed. | | `surface` | `string` | 4 | surface is where the answer arrived from: "api" for an answer presented on AnswerAsk, "timeout" for the declared default the clock applied. | | `note` | `string` | 5 | | | `hop_index` | `int32` | 6 | hop_index is the rung that answered, so "who decided" answers with the hop as well as the principal. | | `at` | `google.protobuf.Timestamp` | 7 | | ### AskHopSpec AskHopSpec is one rung of a declared escalation ladder: who it reaches and how long they hold the question. The arguments template is not on the wire, since it is the delivery's business and not the reader's. | Field | Type | # | Notes | | --- | --- | --- | --- | | `principal_id` | `string` | 1 | | | `tool_slug` | `string` | 2 | | | `timeout_seconds` | `int32` | 3 | | ### CompensationReceipt CompensationReceipt is one act step's unwind, told honestly: how many of its committed rows the saga tried to undo, how many it undid, and the ones it could not, named. A stranded row is an external resource with no owner until a human takes it, which is why the count is never rounded away. | Field | Type | # | Notes | | --- | --- | --- | --- | | `attempted` | `int32` | 1 | | | `compensated` | `int32` | 2 | | | `stranded` | `int32` | 3 | | | `stranded_row_keys` | repeated `string` | 4 | | ### Cost Cost is what a step or a job spent. Model spend is metered in micro-USD (one millionth of a dollar) because one row's map call costs less than a cent, and action_count is external effects, the unit an action budget is written in. | Field | Type | # | Notes | | --- | --- | --- | --- | | `model_micro_usd` | `int64` | 1 | | | `action_count` | `int32` | 2 | | | `discarded_model_micro_usd` | `int64` | 3 | discarded_model_micro_usd is the part of model_micro_usd that prices work an attempt paid for and could not report: a step killed between checkpoints spends money nothing durable records, and the resumed attempt charges the rows it re-does twice rather than leaving that spend off the receipt. It is therefore a bound, not a measurement, and model_micro_usd is an upper bound of what the project paid whenever it is above zero. | ### CostEstimate CostEstimate is the projection a human approves. | Field | Type | # | Notes | | --- | --- | --- | --- | | `rows` | `int32` | 1 | | | `model_micro_usd` | `int64` | 2 | | | `action_count` | `int32` | 3 | | | `effects` | repeated `ProjectedEffect` | 4 | | ### GetAskRequest GetAskRequest reads one ask. request_id is optional: naming no request reads the ask the job is waiting on now. | Field | Type | # | Notes | | --- | --- | --- | --- | | `job_id` | `string` | 1 | | | `request_id` | `string` | 2 | | ### GetAskResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `ask` | `Ask` | 1 | | ### GetJobRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### GetJobResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `job` | `Job` | 1 | | | `simulation` | `Job` | 94 | simulation is the job named by job.simulated_from, receipts included, so an approver reads the measured diff beside the projected estimate in one call. Absent when the plan was not simulated first. | ### Job Job is the folded state of one job's event log. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `status` | `JobStatus` | 2 | | | `program_json` | `string` | 3 | the plan verbatim, as submitted | | `program_hash` | `string` | 4 | | | `estimate` | `CostEstimate` | 5 | | | `plan_approval_id` | `string` | 6 | set when the plan parked before step one | | `park` | `Park` | 7 | set while status is PARKED | | `step_receipts` | repeated `StepReceipt` | 8 | | | `receipt` | `Receipt` | 9 | the fold; complete is false until every step reported | | `last_error` | `string` | 10 | | | `created_at` | `google.protobuf.Timestamp` | 11 | | | `updated_at` | `google.protobuf.Timestamp` | 12 | | | `finished_at` | `google.protobuf.Timestamp` | 13 | | | `pending_ask` | `Ask` | 60 | pending_ask is the question this job owes an answer to, when it parked on one. It mirrors park.judgment's ask so a reader learns who was asked without walking into the park. | | `saga` | `SagaState` | 70 | saga is what became of the effects this job landed, set once an unwind has compensated anything. A job parked with stranded rows names them here. | | `mode` | `string` | 92 | mode is what this submission was allowed to touch: "run" (the default), "simulate" (act steps projected instead of called) or "shadow" (every external call answered from recorded state). Every receipt of a measuring run carries simulated, and a simulated receipt never folds into a real one. | | `simulated_from` | `string` | 93 | simulated_from names the simulation this plan was measured by, set when the submission asked to be simulated first. Its receipts are the diff an approver reads beside the estimate. | | `skill_slug` | `string` | 80 | skill_slug names the promoted skill whose call submitted this job, empty when a caller submitted the program directly. Everything else about the job is the same either way. | ### JobEvent JobEvent is one entry of a job's append-only log, which is the audit answer to "what did this job do". Rows never cross this surface: payload_json is the entry's own payload (a checkpoint, a receipt, a park reason), and the rows a step read or wrote live in collections. | Field | Type | # | Notes | | --- | --- | --- | --- | | `job_id` | `string` | 1 | | | `seq` | `int64` | 2 | seq is dense and 1-based per job, so the order of the page is the order the job walked and a gap means a lost write. | | `type` | `string` | 3 | type is the log's closed vocabulary: submitted, plan-approved, step-started, checkpoint, step-completed, parked, judgment-requested, ask-delivered, judgment-recorded, answer-refused, compensation-applied, resumed, finished. | | `step_id` | `string` | 4 | | | `at` | `google.protobuf.Timestamp` | 5 | | | `payload_json` | `string` | 6 | | | `principal_chain` | repeated `string` | 140 | Who caused this entry, oldest cause first: "key:", "skill:", "job:". It is an audit record and never an authorization input. | ### ListAsksRequest ListAsksRequest pages one project's asks. Every filter narrows; an empty one does not. Asks of another project are absent rather than forbidden, which is the read rule every surface here follows. | Field | Type | # | Notes | | --- | --- | --- | --- | | `state` | `AskState` | 1 | state lists only asks in one state. Unspecified lists every state. | | `principal_id` | `string` | 2 | principal_id lists only the asks this contact has been reached by, which is the inbox read: a question delivered to somebody else is not theirs to answer and is not theirs to see in this list. | | `job_id` | `string` | 3 | | | `standing_program_id` | `string` | 4 | standing_program_id lists the asks raised by the jobs a standing program fired. | | `page_size` | `int32` | 5 | | | `page_token` | `string` | 6 | | ### ListAsksResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `asks` | repeated `Ask` | 1 | asks, newest question first. | | `next_page_token` | `string` | 2 | | ### ListJobEventsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `job_id` | `string` | 1 | | ### ListJobEventsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `events` | repeated `JobEvent` | 1 | The whole log in sequence order. A job's log is bounded by its own steps and checkpoints, so the page is the log rather than a window on it. | ### ListJobsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `status` | `JobStatus` | 1 | unspecified lists every status | | `page_size` | `int32` | 2 | | | `page_token` | `string` | 3 | | | `mode` | `string` | 95 | mode narrows the page to one submission mode. Empty lists every mode. | | `skill_slug` | `string` | 80 | skill_slug lists only the jobs one skill submitted, which is how "what has this skill done" is answered from the log. Empty lists every job. | ### ListJobsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `jobs` | repeated `Job` | 1 | | | `next_page_token` | `string` | 2 | | ### Park Park is why a running job stopped. approval_id names an approval in the policy queue, judgment the question a job waits on; both are set when the approval was delivered as an ask. | Field | Type | # | Notes | | --- | --- | --- | --- | | `step_id` | `string` | 1 | | | `reason` | `string` | 2 | | | `approval_id` | `string` | 3 | | | `judgment` | `PendingJudgment` | 4 | | | `at` | `google.protobuf.Timestamp` | 5 | | ### PendingJudgment PendingJudgment is a declared pause waiting on an answer. It carries the question and the continuations the program declared, never rows: a decision sees counts and reasons. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `step_id` | `string` | 2 | | | `handler` | `string` | 3 | model, human, or principal | | `question` | `string` | 4 | | | `answer_schema_json` | `string` | 5 | | | `continuations` | repeated `string` | 6 | proceed, retry-with, skip, or abort | | `breaches` | repeated `string` | 7 | the declared bounds this step exceeded, if any | | `asked_at` | `google.protobuf.Timestamp` | 8 | | ### ProjectedEffect ProjectedEffect is one line of what a plan would do to the world, derived statically from the program before anything executes. access_class is the catalog's blast-radius class, and a destructive one is what makes a plan park for approval. | Field | Type | # | Notes | | --- | --- | --- | --- | | `step_id` | `string` | 1 | | | `tool_slug` | `string` | 2 | | | `access_class` | `string` | 3 | | | `count` | `int32` | 4 | | | `compensation` | `bool` | 70 | compensation marks an effect that only happens on an unwind: the inverse the step declared. An approver reads the forward effects as what the plan intends and these as what undoing it would do. | ### Receipt Receipt is the failure-honesty contract: how many rows went in, how many came out, what was dropped and why, whether the producer saw everything it was supposed to, and what it cost. A job receipt is the fold of its step receipts, so it cannot claim more than the steps proved. | Field | Type | # | Notes | | --- | --- | --- | --- | | `rows_in` | `int32` | 1 | rows_in and rows_out are the plan's "in" and "out". They are spelled out here because "in" is a keyword in some generated languages. | | `rows_out` | `int32` | 2 | | | `dropped` | map<`string`, `int32`> | 3 | dropped counts rows by reason. Nothing disappears without a reason. | | `complete` | `bool` | 4 | complete is false when any producer declared a shortfall, and it is the AND over folded receipts. | | `cost` | `Cost` | 5 | | | `simulated` | `bool` | 6 | simulated marks a receipt produced without touching the world. A simulated receipt is never folded into a real one. | ### SagaState SagaState is the same account for the whole job: what its act steps landed and still stands, what was undone, and what is stranded. It is derived from the act steps' outcome collections and the log's compensation entries, and it is unset for a job that never unwound anything. | Field | Type | # | Notes | | --- | --- | --- | --- | | `committed` | `int32` | 1 | committed is how many landed rows still stand, including the rows of act steps that declared no inverse. | | `compensated` | `int32` | 2 | | | `stranded` | `int32` | 3 | | | `stranded_row_keys` | repeated `string` | 4 | | ### StepReceipt StepReceipt is one step's receipt plus what it produced. | Field | Type | # | Notes | | --- | --- | --- | --- | | `step_id` | `string` | 1 | | | `primitive` | `string` | 2 | collect, map, reduce, act, or files | | `receipt` | `Receipt` | 3 | | | `output_handle` | `string` | 4 | the collection this step produced, if any | | `reused` | `bool` | 5 | reused reports that the step did not run: an identical fingerprint already had an output collection, so the content address was served instead. | | `attempt` | `int32` | 6 | | | `started_at` | `google.protobuf.Timestamp` | 7 | | | `finished_at` | `google.protobuf.Timestamp` | 8 | | | `compensation` | `CompensationReceipt` | 70 | compensation is the saga's account of this step's unwind, unset when nothing was unwound. It sits beside the receipt rather than inside it: rows in and rows out count what the step did, and undoing an effect does not touch a row twice. | ### SubmitJobRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `program_json` | `string` | 1 | program_json is the job program. JSON is the surface syntax: SDKs generate it, and any sugar compiles to it. An unknown field is a refusal, so a typo fails at submit time instead of being ignored at hour three. | | `idempotency_key` | `string` | 2 | idempotency_key makes a repeated submission return the original job instead of starting a second run of the same plan. | | `mode` | `string` | 90 | mode is what this submission may touch: "run" (default), "simulate" or "shadow". It rides on the request rather than in the program because the same program has to be runnable both ways: the plan a human approves and the plan that then runs are the same bytes. | | `simulate_first` | `bool` | 91 | simulate_first submits two jobs: a simulation that runs now, and this plan, which parks for approval naming it. Two jobs, because a simulated receipt and a real one must never share a fold. | ### SubmitJobResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `job` | `Job` | 1 | job carries the status the submission reached: running, or pending plan approval with the estimate the approver decides on. | ## Enums ### JobStatus | Value | # | Meaning | | --- | --- | --- | | `JOB_STATUS_UNSPECIFIED` | 0 | | | `JOB_STATUS_PENDING_PLAN_APPROVAL` | 1 | The plan needs a human decision before step one runs. plan_approval_id names the approval and estimate carries the numbers it was granted on. | | `JOB_STATUS_RUNNING` | 2 | | | `JOB_STATUS_PARKED` | 3 | Stopped on an approval or a judgment point. park says which, so a parked job is a visible state with an owner rather than a stall. | | `JOB_STATUS_SUCCEEDED` | 4 | | | `JOB_STATUS_FAILED` | 5 | | | `JOB_STATUS_CANCELED` | 6 | | ### AskState AskState is what an ask is doing now. exhausted is not a state: an ask whose ladder ran out is still pending or escalated, because it still owes a person's answer, and the exhausted flag says nothing more will be delivered. | Value | # | Meaning | | --- | --- | --- | | `ASK_STATE_UNSPECIFIED` | 0 | | | `ASK_STATE_PENDING` | 1 | Outstanding, at the first hop. | | `ASK_STATE_ESCALATED` | 2 | Outstanding, and the ladder has climbed at least one rung. | | `ASK_STATE_ANSWERED` | 3 | A principal answered it. | | `ASK_STATE_TIMED_OUT` | 4 | Nobody answered and the declared default applied. No approval ask reaches this state: an approval carries no default, because no clock approves. | --- # KeysService The project's own API keys: mint one, list them, retire one. Minting and retiring take an admin key, because a key that could mint its own reviewer would staff an approval gate rather than satisfy it. Listing carries no key material and is open to any of the project's keys. Every call is a POST to `https://api.atmon.ai/automaton.v1.KeysService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `CreateKey` | `CreateKeyRequest` | `CreateKeyResponse` | Mints a key for the project and returns its plaintext once. | | `ListKeys` | `ListKeysRequest` | `ListKeysResponse` | Lists the project's keys, newest first, with no key material. | | `RevokeKey` | `RevokeKeyRequest` | `RevokeKeyResponse` | Retires one of the project's keys. | ### CreateKey Mints a key for the project and returns its plaintext once. It requires the admin role, and an expiry already in the past is refused rather than stored, because a key born expired looks like a key that stopped working. Request `CreateKeyRequest`, response `CreateKeyResponse`. ```http POST /automaton.v1.KeysService/CreateKey HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "name": "...", "role": "KEY_ROLE_AGENT", "expiresAt": "2026-01-31T09:15:00Z" } ``` The response: ```json { "key": { "id": "...", "name": "...", "role": "KEY_ROLE_AGENT", "createdAt": "2026-01-31T09:15:00Z", "revokedAt": "2026-01-31T09:15:00Z", "expiresAt": "2026-01-31T09:15:00Z", "createdByUserId": "..." }, "plaintext": "..." } ``` ### ListKeys Lists the project's keys, newest first, with no key material. Open to any of the project's keys. Request `ListKeysRequest`, response `ListKeysResponse`. ```http POST /automaton.v1.KeysService/ListKeys HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json {} ``` The response: ```json { "keys": [{ "id": "...", "name": "...", "role": "KEY_ROLE_AGENT", "createdAt": "2026-01-31T09:15:00Z", "revokedAt": "2026-01-31T09:15:00Z", "expiresAt": "2026-01-31T09:15:00Z", "createdByUserId": "..." }] } ``` ### RevokeKey Retires one of the project's keys. It requires the admin role, and it refuses the key the request arrived on: revoking that would end the session doing the administration, so a key retires itself with `automaton apikey revoke`. Request `RevokeKeyRequest`, response `RevokeKeyResponse`. ```http POST /automaton.v1.KeysService/RevokeKey HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json {} ``` ## Messages ### CreateKeyRequest CreateKeyRequest mints a key for the authenticated project. It names no project, because a key mints only for its own. | Field | Type | # | Notes | | --- | --- | --- | --- | | `name` | `string` | 1 | | | `role` | `KeyRole` | 2 | | | `expires_at` | `google.protobuf.Timestamp` | 3 | expires_at must be in the future when it is set. Absent means the key never expires. | ### CreateKeyResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `key` | `ProjectKey` | 1 | | | `plaintext` | `string` | 2 | plaintext is the only time the key exists outside the caller's hands. It is never stored, never logged, and no later read returns it: a lost key is replaced, not recovered. | ### ListKeysRequest No fields. The call takes its scope from the authenticated project. ### ListKeysResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `keys` | repeated `ProjectKey` | 1 | | ### ProjectKey ProjectKey is one API key as a listing shows it. It carries no key material at all: the plaintext exists once, in CreateKeyResponse, and the server stores only its hash. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `name` | `string` | 2 | name is the label whoever minted it wrote, for their own bookkeeping. | | `role` | `KeyRole` | 3 | | | `created_at` | `google.protobuf.Timestamp` | 4 | | | `revoked_at` | `google.protobuf.Timestamp` | 5 | revoked_at is absent while the key is live. | | `expires_at` | `google.protobuf.Timestamp` | 6 | expires_at is absent when the key never expires. | | `created_by_user_id` | `string` | 7 | created_by_user_id is the console user who minted it, empty for every key minted by the CLI and for every key minted before accounts existed. It is bookkeeping, never an authorization: a key minted by a person is a distinct principal from that person, which is what lets separation of duty hold when an agent acts under a key its own reviewer created. The member-removal dialog reads this field, and it is the reason that dialog can list what a departing person's keys are before deciding which of them keep running. | ### RevokeKeyRequest RevokeKeyRequest retires one of the project's keys. It is idempotent: a key already revoked keeps its original revocation time. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### RevokeKeyResponse No fields. The call takes its scope from the authenticated project. ## Enums ### KeyRole KeyRole is what a key may do. The vocabulary is closed, because a role a key could present that the policy gate has never heard of is not a role, it is a hole. Adding one is a recorded decision rather than a field value. | Value | # | Meaning | | --- | --- | --- | | `KEY_ROLE_UNSPECIFIED` | 0 | KEY_ROLE_UNSPECIFIED reads as KEY_ROLE_AGENT on a mint, which is what an omitted role means and what every key minted before roles existed reads as. | | `KEY_ROLE_AGENT` | 1 | KEY_ROLE_AGENT executes tools, submits jobs, and connects accounts for entities. It is the role an AI holds. | | `KEY_ROLE_APPROVER` | 2 | KEY_ROLE_APPROVER resolves approvals and answers asks addressed to approvers. It is a separation of duty rather than an escalation: it grants review, not execution, and not administration. | | `KEY_ROLE_ADMIN` | 3 | KEY_ROLE_ADMIN mints and revokes keys, writes the policy document and the principal directory, registers private toolkits, storage backends and telemetry destinations, and resolves approvals. It is the top of the vocabulary, and separation of duty still binds on it: an admin that requested a call may not release it. | | `KEY_ROLE_VIEWER` | 4 | KEY_ROLE_VIEWER reads what the governance surfaces show and changes nothing. | --- # RouterService Find the tool for a task by describing the task. You send what the person actually asked for; you get back a short ranked list of the actions that fit, each with a compact argument schema and a flag saying whether that person has connected the app it belongs to. Telling atmon afterwards whether the tool you picked was the right one is what improves the next answer. Every call is a POST to `https://api.atmon.ai/automaton.v1.RouterService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `ResolveTools` | `ResolveToolsRequest` | `ResolveToolsResponse` | Resolves an intent to a small ranked slate of tools with schemas compacted for a model, and returns the resolution_id that identifies the decision. | | `ReportOutcome` | `ReportOutcomeRequest` | `ReportOutcomeResponse` | Reports what happened under a resolution, which is what ranking learns from. | ### ResolveTools Resolves an intent to a small ranked slate of tools with schemas compacted for a model, and returns the resolution_id that identifies the decision. This is what the MCP search_tools meta-tool answers with. Tools the project's entity visibility hides never reach the slate, and by default neither do tools of toolkits the entity has not connected (include_unconnected). Request `ResolveToolsRequest`, response `ResolveToolsResponse`. ```http POST /automaton.v1.RouterService/ResolveTools HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "intent": "...", "contextMessages": ["..."], "toolkitFilter": ["..."], "maxTools": 0, "includeUnconnected": true } ``` The response: ```json { "matches": [{ "toolSlug": "...", "score": 0.0, "compactInputSchemaJson": "{}", "connected": true }], "resolutionId": "...", "connectionScoped": true } ``` ### ReportOutcome Reports what happened under a resolution, which is what ranking learns from. Two limits bound it: the named call must have run one of that resolution's own matches, and a resolution is reportable exactly once, claimed atomically so the first report wins. Request `ReportOutcomeRequest`, response `ReportOutcomeResponse`. ```http POST /automaton.v1.RouterService/ReportOutcome HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "resolutionId": "...", "toolCallId": "...", "outcome": "OUTCOME_SUCCESS", "detail": "..." } ``` The response: ```json {} ``` ## Messages ### ReportOutcomeRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `resolution_id` | `string` | 1 | | | `tool_call_id` | `string` | 2 | empty when no call was made | | `outcome` | `Outcome` | 3 | | | `detail` | `string` | 4 | | ### ReportOutcomeResponse No fields. The call takes its scope from the authenticated project. ### ResolveToolsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | | | `intent` | `string` | 2 | natural-language statement of what the agent needs | | `context_messages` | repeated `string` | 3 | recent turns, most recent last | | `toolkit_filter` | repeated `string` | 4 | empty means the request's whole search scope | | `max_tools` | `int32` | 5 | | | `include_unconnected` | `bool` | 6 | Search the whole catalog instead of the entity's connected surface. By default a resolve searches the toolkits this entity has an active connection for, plus the project's own private toolkits, because a tool the entity cannot call is not an answer. Set this to browse what the project could connect next; the matches then carry connected = false. The default does nothing when the entity has connected nothing: an entity with no connections is looking for what to connect, so its resolve searches the whole catalog either way. | ### ResolveToolsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `matches` | repeated `ToolMatch` | 1 | | | `resolution_id` | `string` | 2 | echo in ReportOutcome to close the loop | | `connection_scoped` | `bool` | 3 | True when the search was bounded to the entity's connected surface. False means the whole catalog was searched, either because include_unconnected was set or because the entity has connected nothing yet. | ### ToolMatch | Field | Type | # | Notes | | --- | --- | --- | --- | | `tool_slug` | `string` | 1 | | | `score` | `double` | 2 | | | `compact_input_schema_json` | `string` | 3 | trimmed for this context | | `connected` | `bool` | 4 | entity holds an active connection for its toolkit | ## Enums ### Outcome | Value | # | Meaning | | --- | --- | --- | | `OUTCOME_UNSPECIFIED` | 0 | | | `OUTCOME_SUCCESS` | 1 | | | `OUTCOME_EXECUTION_ERROR` | 2 | right tool, call failed | | `OUTCOME_WRONG_TOOL` | 3 | agent had to re-route after seeing the result | | `OUTCOME_NO_TOOL_FOUND` | 4 | nothing returned matched the intent | --- # TracesService Read back one decision and everything that ran under it. The id is the one search returned, so code that kept it can read its own turn back later, with what was offered and what was done. Every call is a POST to `https://api.atmon.ai/automaton.v1.TracesService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `ListTraces` | `ListTracesRequest` | `ListTracesResponse` | Lists traces newest first, optionally narrowed to one entity, one toolkit's tools, or a time window. | | `GetTrace` | `GetTraceRequest` | `GetTraceResponse` | Reads one decision and the ledger rows that ran under it. | ### ListTraces Lists traces newest first, optionally narrowed to one entity, one toolkit's tools, or a time window. Request `ListTracesRequest`, response `ListTracesResponse`. ```http POST /automaton.v1.TracesService/ListTraces HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "entityId": "...", "toolkitSlug": "...", "from": "2026-01-31T09:15:00Z", "to": "2026-01-31T09:15:00Z", "pageSize": 0, "pageToken": "..." } ``` The response: ```json { "traces": [{ "id": "...", "entityId": "...", "intent": "...", "matchedToolSlugs": ["..."], "createdAt": "2026-01-31T09:15:00Z", "reportedOutcome": "OUTCOME_SUCCESS", "reportedDetail": "...", "reportedAt": "2026-01-31T09:15:00Z", "callCount": 0 }], "nextPageToken": "..." } ``` ### GetTrace Reads one decision and the ledger rows that ran under it. This is the view that separates "the router offered the wrong tool" from "the agent called the right tool badly". Request `GetTraceRequest`, response `GetTraceResponse`. ```http POST /automaton.v1.TracesService/GetTrace HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json { "trace": { "id": "...", "entityId": "...", "intent": "...", "matchedToolSlugs": ["..."], "createdAt": "2026-01-31T09:15:00Z", "reportedOutcome": "OUTCOME_SUCCESS", "reportedDetail": "...", "reportedAt": "2026-01-31T09:15:00Z", "callCount": 0 }, "toolCalls": [{ "id": "...", "entityId": "...", "toolSlug": "...", "connectedAccountId": "...", "argumentsJson": "{}", "status": "TOOL_CALL_STATUS_RUNNING", "resultJson": "{}", "errorCode": "...", "errorDetail": "...", "startedAt": "2026-01-31T09:15:00Z", "finishedAt": "2026-01-31T09:15:00Z", "approvalId": "...", "resultTruncated": true, "resourceUrn": "...", "leaseWaitMs": 0, "principalChain": ["..."], "jobId": "...", "stepId": "...", "relayId": "..." }] } ``` ## Messages ### GetTraceRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### GetTraceResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `trace` | `Trace` | 1 | | | `tool_calls` | repeated [`ToolCall`](./execution.md#toolcall) | 2 | linked ledger rows, oldest first | ### ListTracesRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `entity_id` | `string` | 1 | empty lists every entity in the project | | `toolkit_slug` | `string` | 2 | keep traces that matched a tool of this toolkit | | `from` | `google.protobuf.Timestamp` | 3 | inclusive lower bound on created_at | | `to` | `google.protobuf.Timestamp` | 4 | exclusive upper bound on created_at | | `page_size` | `int32` | 5 | | | `page_token` | `string` | 6 | | ### ListTracesResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `traces` | repeated `Trace` | 1 | newest first | | `next_page_token` | `string` | 2 | | ### Trace | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | the resolution id from ResolveTools | | `entity_id` | `string` | 2 | | | `intent` | `string` | 3 | | | `matched_tool_slugs` | repeated `string` | 4 | ranked best first | | `created_at` | `google.protobuf.Timestamp` | 5 | | | `reported_outcome` | [`Outcome`](./router.md#outcome) | 6 | OUTCOME_UNSPECIFIED until the agent reports | | `reported_detail` | `string` | 7 | | | `reported_at` | `google.protobuf.Timestamp` | 8 | absent until the agent reports | | `call_count` | `int32` | 9 | ledger rows linked to this trace | ## Types from other calls These are described on another page of this reference, so there is one description of each. | Type | Described under | | --- | --- | | [`Outcome`](./router.md#outcome) | RouterService | | [`ToolCall`](./execution.md#toolcall) | ExecutionService | --- # TriggersService The inbound direction: an external app has something happen, and you hear about it. Subscribe an address, read what arrived, and replay a delivery your side missed. Delivery is at least once, with retries and a dead state, and every attempt is readable here. Every call is a POST to `https://api.atmon.ai/automaton.v1.TriggersService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `CreateSubscription` | `CreateSubscriptionRequest` | `CreateSubscriptionResponse` | Registers an endpoint for delivery and returns its signing secret once. | | `ListSubscriptions` | `ListSubscriptionsRequest` | `ListSubscriptionsResponse` | Lists the project's subscriptions. | | `DeleteSubscription` | `DeleteSubscriptionRequest` | `DeleteSubscriptionResponse` | Deletes one subscription. | | `ListEvents` | `ListEventsRequest` | `ListEventsResponse` | Lists the normalized events received for this project, filtered by toolkit, trigger, and time. | | `GetEvent` | `GetEventRequest` | `GetEventResponse` | Reads one event with its normalized payload. | | `ListDeliveries` | `ListDeliveriesRequest` | `ListDeliveriesResponse` | Lists delivery attempts for an event, a subscription, or both. | | `ReplayEvent` | `ReplayEventRequest` | `ReplayEventResponse` | Queues fresh deliveries for an event to the currently matching active subscriptions, leaving the original attempt history in place so the record of what failed stays readable. | | `ListIngestEndpoints` | `ListIngestEndpointsRequest` | `ListIngestEndpointsResponse` | Lists every toolkit that declares an inbound webhook, with the path a provider posts to and whether this deployment holds the secret that verifies it. | ### CreateSubscription Registers an endpoint for delivery and returns its signing secret once. HTTPS is required outside loopback. Request `CreateSubscriptionRequest`, response `CreateSubscriptionResponse`. ```http POST /automaton.v1.TriggersService/CreateSubscription HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "endpointUrl": "...", "toolkitSlug": "...", "triggerSlug": "..." } ``` The response: ```json { "subscription": { "id": "...", "endpointUrl": "...", "toolkitSlug": "...", "triggerSlug": "...", "active": true }, "signingSecret": "..." } ``` ### ListSubscriptions Lists the project's subscriptions. A signing secret is never returned again, so a lost secret means a new subscription. Request `ListSubscriptionsRequest`, response `ListSubscriptionsResponse`. ```http POST /automaton.v1.TriggersService/ListSubscriptions HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json {} ``` The response: ```json { "subscriptions": [{ "id": "...", "endpointUrl": "...", "toolkitSlug": "...", "triggerSlug": "...", "active": true }] } ``` ### DeleteSubscription Deletes one subscription. Events already stored are unaffected. Request `DeleteSubscriptionRequest`, response `DeleteSubscriptionResponse`. ```http POST /automaton.v1.TriggersService/DeleteSubscription HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json {} ``` ### ListEvents Lists the normalized events received for this project, filtered by toolkit, trigger, and time. Request `ListEventsRequest`, response `ListEventsResponse`. ```http POST /automaton.v1.TriggersService/ListEvents HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "toolkitSlug": "...", "triggerSlug": "...", "since": "2026-01-31T09:15:00Z", "until": "2026-01-31T09:15:00Z", "pageSize": 0, "pageToken": "..." } ``` The response: ```json { "events": [{ "id": "...", "toolkitSlug": "...", "triggerSlug": "...", "entityId": "...", "payloadJson": "{}", "occurredAt": "2026-01-31T09:15:00Z", "receivedAt": "2026-01-31T09:15:00Z" }], "nextPageToken": "..." } ``` ### GetEvent Reads one event with its normalized payload. Request `GetEventRequest`, response `GetEventResponse`. ```http POST /automaton.v1.TriggersService/GetEvent HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "id": "..." } ``` The response: ```json { "event": { "id": "...", "toolkitSlug": "...", "triggerSlug": "...", "entityId": "...", "payloadJson": "{}", "occurredAt": "2026-01-31T09:15:00Z", "receivedAt": "2026-01-31T09:15:00Z" } } ``` ### ListDeliveries Lists delivery attempts for an event, a subscription, or both. Delivery is at-least-once: pending, delivered, or dead_letter after the last retry. Request `ListDeliveriesRequest`, response `ListDeliveriesResponse`. ```http POST /automaton.v1.TriggersService/ListDeliveries HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "eventId": "...", "subscriptionId": "..." } ``` The response: ```json { "deliveries": [{ "id": "...", "eventId": "...", "subscriptionId": "...", "state": "...", "attempts": 0, "lastError": "...", "lastAttemptAt": "2026-01-31T09:15:00Z" }] } ``` ### ReplayEvent Queues fresh deliveries for an event to the currently matching active subscriptions, leaving the original attempt history in place so the record of what failed stays readable. Request `ReplayEventRequest`, response `ReplayEventResponse`. ```http POST /automaton.v1.TriggersService/ReplayEvent HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "eventId": "..." } ``` The response: ```json { "deliveries": [{ "id": "...", "eventId": "...", "subscriptionId": "...", "state": "...", "attempts": 0, "lastError": "...", "lastAttemptAt": "2026-01-31T09:15:00Z" }] } ``` ### ListIngestEndpoints Lists every toolkit that declares an inbound webhook, with the path a provider posts to and whether this deployment holds the secret that verifies it. A read of deployment configuration rather than of project rows: the answer is the same for every project on the node, and it is here because the page that asks "what starts this" is the page that has to say when the answer is nothing. Request `ListIngestEndpointsRequest`, response `ListIngestEndpointsResponse`. ```http POST /automaton.v1.TriggersService/ListIngestEndpoints HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json {} ``` The response: ```json { "endpoints": [{ "toolkitSlug": "...", "path": "...", "scheme": "...", "secretEnv": "...", "secretSet": true }] } ``` ## Messages ### CreateSubscriptionRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `endpoint_url` | `string` | 1 | | | `toolkit_slug` | `string` | 2 | empty subscribes to every toolkit | | `trigger_slug` | `string` | 3 | requires toolkit_slug; empty takes every trigger | ### CreateSubscriptionResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `subscription` | `Subscription` | 1 | | | `signing_secret` | `string` | 2 | signing_secret is returned once, at creation. atmon signs every delivery body with it (X-Automaton-Signature: sha256=); a lost secret is replaced by a new subscription, never recovered. | ### DeleteSubscriptionRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### DeleteSubscriptionResponse No fields. The call takes its scope from the authenticated project. ### Delivery Delivery is one attempt series: one event to one subscription. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `event_id` | `string` | 2 | | | `subscription_id` | `string` | 3 | | | `state` | `string` | 4 | pending \| delivered \| dead_letter | | `attempts` | `int32` | 5 | | | `last_error` | `string` | 6 | | | `last_attempt_at` | `google.protobuf.Timestamp` | 7 | | ### Event Event is one normalized inbound event, stored per receiving project. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `toolkit_slug` | `string` | 2 | | | `trigger_slug` | `string` | 3 | the toolkit's kind: trigger tool | | `entity_id` | `string` | 4 | empty when the payload does not resolve one | | `payload_json` | `string` | 5 | normalized body per the trigger's output schema | | `occurred_at` | `google.protobuf.Timestamp` | 6 | provider time when present, else received_at | | `received_at` | `google.protobuf.Timestamp` | 7 | | ### GetEventRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | ### GetEventResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `event` | `Event` | 1 | | ### IngestEndpoint IngestEndpoint is one toolkit's inbound path and whether this deployment can verify a post to it. The secret itself is never on this message and there is no RPC that returns it. What crosses is whether the environment variable naming it holds a value, because that single bit decides between a post being accepted and a post being refused, and an operator has no other way to read it. | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkit_slug` | `string` | 1 | | | `path` | `string` | 2 | The path a provider posts to, which is /webhooks/ plus the toolkit slug. | | `scheme` | `string` | 3 | The verification scheme the toolkit declares, from the catalog's closed vocabulary: hmac_sha256, slack_v0, token_query. | | `secret_env` | `string` | 4 | The name of the process environment variable holding the shared secret. The name is configuration and is safe to render; the value is not returned. | | `secret_set` | `bool` | 5 | False means every post to this path is refused rather than trusted, so the toolkit's triggers fire nothing until the deployment sets the variable. | ### ListDeliveriesRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `event_id` | `string` | 1 | filter by event, subscription, or both | | `subscription_id` | `string` | 2 | | ### ListDeliveriesResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `deliveries` | repeated `Delivery` | 1 | | ### ListEventsRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `toolkit_slug` | `string` | 1 | empty lists every toolkit | | `trigger_slug` | `string` | 2 | empty lists every trigger | | `since` | `google.protobuf.Timestamp` | 3 | inclusive lower bound on received_at | | `until` | `google.protobuf.Timestamp` | 4 | exclusive upper bound on received_at | | `page_size` | `int32` | 5 | | | `page_token` | `string` | 6 | | ### ListEventsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `events` | repeated `Event` | 1 | | | `next_page_token` | `string` | 2 | | ### ListIngestEndpointsRequest No fields. The call takes its scope from the authenticated project. ### ListIngestEndpointsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `endpoints` | repeated `IngestEndpoint` | 1 | | ### ListSubscriptionsRequest No fields. The call takes its scope from the authenticated project. ### ListSubscriptionsResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `subscriptions` | repeated `Subscription` | 1 | | ### ReplayEventRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `event_id` | `string` | 1 | | ### ReplayEventResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `deliveries` | repeated `Delivery` | 1 | The freshly enqueued deliveries. Replay leaves the original attempt history in place and queues new deliveries to the currently matching active subscriptions. | ### Subscription Subscription is one project endpoint that wants events delivered. | Field | Type | # | Notes | | --- | --- | --- | --- | | `id` | `string` | 1 | | | `endpoint_url` | `string` | 2 | HTTPS required outside loopback | | `toolkit_slug` | `string` | 3 | empty matches all toolkits | | `trigger_slug` | `string` | 4 | empty matches all triggers of the toolkit | | `active` | `bool` | 5 | | --- # UsageService What this project has used, aggregated from the same receipts everything else reads. Scoped to the key's own project; a request cannot ask about another one. Every call is a POST to `https://api.atmon.ai/automaton.v1.UsageService/` with a JSON body, and authenticates with `Authorization: Bearer `. Field names in JSON are lowerCamelCase, so the field written `tool_slug` below is `toolSlug` on the wire. [How to call the API](./index.md) has the whole convention. ## Calls | Call | Request | Response | Summary | | --- | --- | --- | --- | | `GetUsage` | `GetUsageRequest` | `GetUsageResponse` | Aggregates the project's ledger rows over a window, grouped by day, toolkit, or tool. | ### GetUsage Aggregates the project's ledger rows over a window, grouped by day, toolkit, or tool. Every recorded call counts, whatever its status, so the succeeded, failed, and denied columns partition the total. Request `GetUsageRequest`, response `GetUsageResponse`. ```http POST /automaton.v1.UsageService/GetUsage HTTP/1.1 Host: api.atmon.ai Authorization: Bearer amk_your_project_key Content-Type: application/json { "from": "2026-01-31T09:15:00Z", "to": "2026-01-31T09:15:00Z", "groupBy": "..." } ``` The response: ```json { "rows": [{ "key": "...", "calls": 0, "succeeded": 0, "failed": 0, "denied": 0 }], "totalCalls": 0 } ``` ## Messages ### GetUsageRequest | Field | Type | # | Notes | | --- | --- | --- | --- | | `from` | `google.protobuf.Timestamp` | 1 | inclusive; zero means 30 days before to | | `to` | `google.protobuf.Timestamp` | 2 | exclusive; zero means now | | `group_by` | `string` | 3 | "day" \| "toolkit" \| "tool" (default "day") | ### GetUsageResponse | Field | Type | # | Notes | | --- | --- | --- | --- | | `rows` | repeated `UsageRow` | 1 | ordered by key; for "day" that is chronological | | `total_calls` | `int64` | 2 | | ### UsageRow | Field | Type | # | Notes | | --- | --- | --- | --- | | `key` | `string` | 1 | the group value: "2026-07-30", "github", "github.create_issue" | | `calls` | `int64` | 2 | all recorded calls, whatever their status | | `succeeded` | `int64` | 3 | | | `failed` | `int64` | 4 | | | `denied` | `int64` | 5 | refused by the mutation gate or by policy | --- # API stability This page says which parts of atmon your code may depend on, what we promise about them, and what we do before any of it changes. It is written for somebody who has shipped against the API and needs to know whether next month's release can break them. ## What the promise covers The public API is `automaton.v1`, and it is exactly the surface documented in the [API reference](./api/index.md): | Surface | What your code does with it | |---|---| | Router | Describes a task and gets back a ranked slate of tools, then reports which one worked. | | Catalog | Reads what exists: apps, their tools, one tool's full definition. | | Execution | Runs one tool and reads the receipt it leaves. | | Jobs | Hands over work bigger than one call, then reads it back and answers what it asks. | | Connections | Connects an account on behalf of one of your users, reads its state, disconnects it. | | Keys | Mints, lists, and retires the project's own API keys. | | Triggers | Subscribes an address to an app's events, reads what arrived, replays a delivery. | | Traces | Reads back one decision and everything that ran under it. | | Usage | Reads what this project has used. | The rule is one sentence: if a call is not in the API reference, it is not part of this promise. The rest of the tree serves the console and our own components, it changes when the console changes, and no notice is given for it. That is not a hidden boundary. Every contract names its audience on its own declaration, and a build check holds the set marked public equal to the set the reference documents, so the two cannot drift apart without failing. ## The promise **Changes are additive.** A field, a call, or a service can be added. An existing one keeps its name, its number, its type, and its meaning. A client generated against `automaton.v1` today keeps compiling and keeps returning the same answers as the surface grows around it. **A machine enforces it, not a habit.** Every change to the contracts runs a compatibility check against the contracts already published. A change that would break a generated client fails the build, so it cannot reach a release by being missed in review. **Error codes only grow.** The `error_code` on a receipt is a string you may switch on. Codes are added; an existing code is never repurposed to mean something else. Write your handling so an unfamiliar code falls through to the same path as a failure you cannot classify, and it will keep working when the list grows. The full list, and what to do about each code, is in [Error codes](./errors.md). **Nothing is removed without a replacement and a date.** A call or field we intend to remove is first marked deprecated in the reference, with the call that replaces it named and a sunset date given. The old shape keeps answering until that date. There is no removal without both. **A change we cannot make additively becomes a new version.** It would ship as `automaton.v2` beside `automaton.v1`, not as an edit to `v1`. ## Where the beta changes this atmon is in beta, and the honest statement of that is short: the promise above starts now, and while the service is in beta a breaking change is possible. If we have to make one, you get told before it ships, the replacement is named, and the old shape keeps answering for a stated window. What we will not do is claim the promise is absolute today and then revert it, because a promise that gets withdrawn is worth less than one that was stated accurately in the first place. When the beta ends, this section is what changes. The rest of the page is already the way we work. ## What the promise does not cover **The catalog's contents.** Which apps and tools exist is data, and it moves as those apps move: tools are added, descriptions are tuned, an app's own API changes under us. The shape of the calls you make is covered here; the list of what you can call through them is not. Resolve tools by describing the task rather than by hard-coding a slug you read once. **Operational settings.** Rate ceilings, page sizes, timeouts, and quotas are tuned as the service is run. The current values are in [Limits](./limits.md), and a call that meets one gets an error code from the list above rather than a surprise. **The console.** What a screen shows, where a setting lives, and what the console's own calls look like are all free to change. **Anything behind a flag we tell you is unreleased.** If a call is not in the reference, it is not released, whatever a response happens to contain. --- # Error codes `error_code` on a tool call is a stable public contract. Codes are added, never repurposed, so a client may switch on the string and keep working across releases. A refused call is a successful RPC. `ExecuteTool` answers with a `ToolCall` whose `status` and `error_code` say what happened; it does not raise a transport error. Read the code and act on it rather than retrying blindly. | Code | When it fires | | --- | --- | | `rate_limited` | Upstream answered HTTP 429, or the toolkit's declared rate_limit budget was empty when the call arrived. Transient: retry after the window refills. The detail ends in a retry_after_seconds value when a budget refusal or a provider Retry-After named one. | | `auth_expired` | The connected account is expired, refresh failed, or upstream answered 401 or 403. The end user has to authorize again. | | `invalid_arguments` | The tool slug is unknown, arguments_json will not unmarshal, a required path parameter is missing, or upstream answered 400 or 422. Fix the arguments; a retry of the same call fails the same way. | | `not_connected` | The entity holds no active connected account for the tool's toolkit. Start a connection before calling again. | | `denied` | The mutation gate refused a destructive tool for want of confirm: true. Tell the user what would be removed, then call again with "confirm": true inside arguments_json, beside the tool's own fields. This message declares no confirm field of its own, and one sent at the request level is ignored. | | `upstream_error` | A transport error, a body-read error, upstream 5xx, or an unhandled status code. Transient for read tools, which execution retries. | | `upstream_timeout` | The external app did not answer inside the timeout, or answered HTTP 408. | | `internal` | atmon failed on its own side: a store read errored, an authorization error was unrecognized, the policy gate itself errored, or an on-prem relay answered an error code this taxonomy does not carry, which means the relay is built against another vocabulary. In the relay case the detail quotes the code the relay sent, so the row still says what happened even though the code is this platform's. | | `policy_denied` | A policy rule refused the call, or entity visibility hides the tool from this entity. Final for the arguments as given. | | `velocity_exceeded` | A policy velocity limit had no token left. Worth retrying once the window refills. | | `approval_pending` | A policy approval gate parked the call. The response carries status TOOL_CALL_STATUS_PENDING_APPROVAL and an approval_id; retry with that id once a human approves it. | | `outcome_unknown` | An earlier attempt claimed this call's idempotency key and never recorded an outcome, which is what a process death between the two leaves behind. The call was not repeated, because repeating it could double an effect the app already applied. Account the row as unresolved: it is never a success and never a clean failure. | | `resource_leased` | Another caller holds the resource this call would mutate. The detail names the holder and the expiry, so a retry is worth making once the hold ends. | | `connection_incomplete` | The toolkit's base_url names account variables (a subdomain, an application id, a cluster address) and this account carries no value for one of them, so the connection cannot say where its calls go. The account exists and may hold a live credential, so not_connected would be wrong. Reconnect with the missing value; no retry helps. | | `budget_exhausted` | The project's provider quota for the toolkit is spent for the window. Distinct from rate_limited, which is a pace the provider or the client sets: this window is a budget a person wrote, so the call comes back when the window rolls or when someone raises the number. The detail names the scope whose ceiling refused it and the instant the window resets. | | `tool_not_in_snapshot` | The project's pinned catalog snapshot does not carry this tool. Routing searches the latest catalog, so a slate can name a tool a pinned environment cannot run. Promote a snapshot that holds it rather than retrying. | | `provider_unavailable` | The deployment holds an open incident for this tool's provider, because enough calls to one toolkit at one resolved host failed, from enough separate projects, that the provider rather than any one tenant's connection is the cause. The call was refused before it took a lease, spent a quota or resolved a credential, so its idempotency key is still free. No operator action releases it: probe traffic reopens the path as soon as the provider answers. The detail names the incident. | | `relay_unavailable` | This tool runs on an on-prem relay, because its toolkit declares execution: relay, and the platform could not hand the call to one. No relay is connected for the selector, the relay was revoked, its work stream dropped mid-call, or the deployment connects no relay at all. The call is refused rather than queued, because a customer network this deployment cannot dial is not a wait anyone can bound. The detail names the selector and the last heartbeat, so the answer says how long the host has been gone. Nothing was sent, so the idempotency key is still free and the retry once the relay dials back in runs the call for real. | ## What to do with each Every code carries its own answer, from the same source as the table above. A code that appears there and not here would be a code nobody said what to do with. | Code | Retry the same call? | | --- | --- | | `rate_limited` | Yes, once the window refills. The retry_after_seconds value in the detail says when. | | `auth_expired` | No. The end user has to authorize the account again first. | | `invalid_arguments` | No. Fix the arguments; the same call fails the same way. | | `not_connected` | No. Connect an account for this entity and toolkit first. | | `denied` | Only with "confirm": true in the call's own arguments, after telling the user what would change. | | `upstream_error` | Yes. A read tool was already retried up to three times before this answer came back; retry a write yourself only under the same idempotency key. | | `upstream_timeout` | Yes, on the same terms as upstream_error. | | `internal` | Once. If it repeats, the fault is on our side rather than in the call. | | `policy_denied` | No. A rule refused these arguments; different arguments may pass. | | `velocity_exceeded` | Yes, once the limit's window refills. | | `approval_pending` | Yes, carrying the approval_id, once a human approves it. | | `outcome_unknown` | Not blindly. The effect may already have landed, so read the app's own state before deciding whether to call again. | | `resource_leased` | Yes, once the hold the detail names expires. | | `connection_incomplete` | No. Reconnect the account with the missing value first. | | `budget_exhausted` | No, until the window the detail names rolls or somebody raises the ceiling. | | `tool_not_in_snapshot` | No. Promote a catalog snapshot that carries the tool. | | `provider_unavailable` | Yes, and nothing has to be done first: probe traffic reopens the path as soon as the provider answers. | | `relay_unavailable` | Yes, once the relay dials back in. Nothing was sent, so the call runs for real. | ## Statuses `ToolCall.status` is coarser than the code and is what a UI shows: | Status | Meaning | | --- | --- | | `TOOL_CALL_STATUS_SUCCEEDED` | The external app answered and the result is shaped to the output schema. | | `TOOL_CALL_STATUS_FAILED` | The call ran and failed. `error_code` says how. | | `TOOL_CALL_STATUS_DENIED` | Refused before any credential was resolved: the mutation gate or a policy rule. | | `TOOL_CALL_STATUS_PENDING_APPROVAL` | Parked on a human. `approval_id` is what releases it. | | `TOOL_CALL_STATUS_RUNNING` | In flight. | Every one of these is recorded. A denial and a park leave a receipt exactly like a success, which is what makes the record of what happened complete rather than best-effort. The limits behind four of these codes, and what to do about each, are in [Limits](./limits.md). What an assistant should say when it meets one is in [The four verbs](../for-your-ai/the-four-verbs.md). --- # Limits Several things bound a call. Some are yours, written in the policy document; the rest come from the app you are calling. All of them are refusals rather than errors: the call comes back as a normal answer with a code on it, nothing was half done, and the refusal is recorded like any other outcome. | Limit | Code | Set by | When it fires | | --- | --- | --- | --- | | Velocity limit | `velocity_exceeded` | You, in the policy document | A token bucket you wrote is empty. Buckets are per project, per toolkit, or per tool, and every applicable one is tested before any token is spent. | | Approval gate | `approval_pending` | You, in the policy document | The call matches a rule that says a person releases it. The call is parked, not failed. | | Spend ceiling | `budget_exhausted` | You, in the policy document | The project or the entity has spent its budget for the current window. | | Resource lease | `resource_leased` | atmon | Another caller holds the resource this tool declares, so two callers cannot write the same thing at once. | | Provider budget | `rate_limited` | The external app | The provider's own declared request budget for this project and toolkit is spent. The table below lists the budgets that are declared. | | Provider incident | `provider_unavailable` | The external app | The provider is in a known incident, so calls to it are refused rather than sent into a failure. | ## What to do about each | Code | What to do | | --- | --- | | `velocity_exceeded` | Retry once the window refills. The detail names the wait in seconds. Nothing was sent and no credential was resolved. | | `approval_pending` | Retry with the `approval_id` once a person approves it in the console inbox. An approval that nobody answers expires rather than running late. | | `budget_exhausted` | The detail names the instant the window resets. Raise the ceiling or wait for it. | | `resource_leased` | Retry. The detail names the holder and when the lease expires. | | `rate_limited` | Retry after the wait the detail names. atmon never sleeps and never queues on your behalf, so your own timeout budget stays yours. | | `provider_unavailable` | Retry once the incident closes; the detail names it. | Every code, including the ones that are not limits, is in [Error codes](./errors.md). ## Declared provider budgets A toolkit may declare the request budget the external app publishes, and atmon paces against it with one bucket per project and toolkit. A connector that declares none is not unlimited: it is one where we have not written the number down, and a call that exceeds the provider's own limit comes back as `rate_limited` from the provider instead. | Toolkit | Budget | Counted per | | --- | --- | --- | | [Airtable](./toolkits/airtable.md) | 5 requests per 1s | `account` | | [Box](./toolkits/box.md) | 1000 requests per 1m | `account` | | [ClickUp](./toolkits/clickup.md) | 100 requests per 1m | `account` | | [GitHub](./toolkits/github.md) | 5000 requests per 1h | `account` | | [GitLab](./toolkits/gitlab.md) | 2000 requests per 1m | `account` | | [Google Sheets](./toolkits/google_sheets.md) | 60 requests per 1m | `account` | | [QuickBooks](./toolkits/quickbooks.md) | 500 requests per 1m | `account` | | [Shopify](./toolkits/shopify.md) | 40 requests per 20s | `account` | | [Slack](./toolkits/slack.md) | 50 requests per 1m | `account` | | [Stripe](./toolkits/stripe.md) | 100 requests per 1s | `account` | | [Webflow](./toolkits/webflow.md) | 60 requests per 1m | `account` | | [Xero](./toolkits/xero.md) | 60 requests per 1m | `account` | Toolkits not listed here declare no budget. Every connector, listed or not, is in the [app reference](./toolkits/index.md). ## Sizes and shapes Two limits are not refusals and are worth knowing before you meet them. - A ranked slate from a search is at most eight tools. Asking for more returns eight. - A tool result is shaped to the tool's output schema before you see it, so a provider answering with a large document does not arrive in your context whole. --- # Toolkit pages not inlined here - [Adyen](https://atmon.ai/docs/reference/toolkits/adyen_checkout.md): Online payments. - [Airtable](https://atmon.ai/docs/reference/toolkits/airtable.md): Spreadsheet-style databases. - [Algolia Ingestion](https://atmon.ai/docs/reference/toolkits/algolia_ingestion.md): Feeding a search index. - [Algolia Search](https://atmon.ai/docs/reference/toolkits/algolia_search.md): Hosted site search. - [Amplitude](https://atmon.ai/docs/reference/toolkits/amplitude.md): Product analytics. - [Asana](https://atmon.ai/docs/reference/toolkits/asana.md): Work management. - [atmon](https://atmon.ai/docs/reference/toolkits/atmon.md): This project's own atmon node. - [Box](https://atmon.ai/docs/reference/toolkits/box.md): Cloud files. - [Calendly](https://atmon.ai/docs/reference/toolkits/calendly.md): Meeting scheduling. - [ClickUp](https://atmon.ai/docs/reference/toolkits/clickup.md): Project and task tracking. - [Confluence](https://atmon.ai/docs/reference/toolkits/confluence.md): Team wiki. - [Discord](https://atmon.ai/docs/reference/toolkits/discord.md): Community chat. - [DocuSign](https://atmon.ai/docs/reference/toolkits/docusign.md): Electronic signatures. - [Dropbox](https://atmon.ai/docs/reference/toolkits/dropbox.md): Cloud files. - [Dropbox Sign](https://atmon.ai/docs/reference/toolkits/dropbox_sign.md): Electronic signatures, formerly HelloSign. - [Figma](https://atmon.ai/docs/reference/toolkits/figma.md): Design files. - [Freshdesk](https://atmon.ai/docs/reference/toolkits/freshdesk.md): Customer support desk. - [Front](https://atmon.ai/docs/reference/toolkits/front.md): Shared team inbox. - [GitHub](https://atmon.ai/docs/reference/toolkits/github.md): Code hosting. - [GitLab](https://atmon.ai/docs/reference/toolkits/gitlab.md): Source control and CI/CD. - [Gmail](https://atmon.ai/docs/reference/toolkits/gmail.md): Email. - [Google Calendar](https://atmon.ai/docs/reference/toolkits/google_calendar.md): Calendars. - [Google Docs](https://atmon.ai/docs/reference/toolkits/google_docs.md): Documents. - [Google Drive](https://atmon.ai/docs/reference/toolkits/google_drive.md): Cloud files. - [Google Maps Platform](https://atmon.ai/docs/reference/toolkits/google_maps.md): Maps and places. - [Google Sheets](https://atmon.ai/docs/reference/toolkits/google_sheets.md): Spreadsheets. - [HubSpot](https://atmon.ai/docs/reference/toolkits/hubspot.md): CRM and sales pipeline. - [Intercom](https://atmon.ai/docs/reference/toolkits/intercom.md): Customer messaging. - [Jira](https://atmon.ai/docs/reference/toolkits/jira.md): Issue tracking. - [Linear](https://atmon.ai/docs/reference/toolkits/linear.md): Issue tracking for software teams. - [Mailchimp](https://atmon.ai/docs/reference/toolkits/mailchimp.md): Email marketing. - [Mailgun](https://atmon.ai/docs/reference/toolkits/mailgun.md): Transactional email. - [Microsoft Outlook](https://atmon.ai/docs/reference/toolkits/microsoft_outlook.md): Outlook mail on Microsoft 365. - [Microsoft Teams](https://atmon.ai/docs/reference/toolkits/microsoft_teams.md): Chat and meetings. - [monday.com](https://atmon.ai/docs/reference/toolkits/monday.md): Work management on monday.com. - [Notion](https://atmon.ai/docs/reference/toolkits/notion.md): Notes, wikis, and databases. - [OpenAI](https://atmon.ai/docs/reference/toolkits/openai.md): OpenAI models. - [Ory Hydra](https://atmon.ai/docs/reference/toolkits/ory_hydra.md): OAuth and OpenID Connect server. - [Ory Identities](https://atmon.ai/docs/reference/toolkits/ory_kratos.md): User identity and login. - [PagerDuty](https://atmon.ai/docs/reference/toolkits/pagerduty.md): On-call and incidents. - [PayPal](https://atmon.ai/docs/reference/toolkits/paypal.md): Payments and billing. - [Pipedrive](https://atmon.ai/docs/reference/toolkits/pipedrive.md): Sales CRM. - [Plaid](https://atmon.ai/docs/reference/toolkits/plaid.md): Bank connections. - [Qdrant](https://atmon.ai/docs/reference/toolkits/qdrant.md): Vector database. - [QuickBooks](https://atmon.ai/docs/reference/toolkits/quickbooks.md): Accounting. - [Salesforce](https://atmon.ai/docs/reference/toolkits/salesforce.md): CRM for sales and service. - [Segment](https://atmon.ai/docs/reference/toolkits/segment.md): Customer data pipelines. - [SendGrid](https://atmon.ai/docs/reference/toolkits/sendgrid.md): Email delivery. - [SendGrid Suppressions](https://atmon.ai/docs/reference/toolkits/sendgrid_suppressions.md): Email suppression lists. - [Shopify](https://atmon.ai/docs/reference/toolkits/shopify.md): Online store. - [Slack](https://atmon.ai/docs/reference/toolkits/slack.md): Team messaging. - [Spotify](https://atmon.ai/docs/reference/toolkits/spotify.md): Music streaming. - [Square](https://atmon.ai/docs/reference/toolkits/square.md): Point of sale and commerce. - [Stripe](https://atmon.ai/docs/reference/toolkits/stripe.md): Payments and billing. - [Telegram](https://atmon.ai/docs/reference/toolkits/telegram.md): Messaging bots. - [Todoist](https://atmon.ai/docs/reference/toolkits/todoist.md): Task lists. - [Trello](https://atmon.ai/docs/reference/toolkits/trello.md): Boards, lists, and cards. - [Twilio](https://atmon.ai/docs/reference/toolkits/twilio.md): Text messages and calls. - [Typeform](https://atmon.ai/docs/reference/toolkits/typeform.md): Forms and surveys. - [Typesense](https://atmon.ai/docs/reference/toolkits/typesense.md): Open-source search. - [Webflow](https://atmon.ai/docs/reference/toolkits/webflow.md): Sites and CMS. - [Xero](https://atmon.ai/docs/reference/toolkits/xero.md): Small business accounting. - [YouTube](https://atmon.ai/docs/reference/toolkits/youtube.md): Video. - [Zendesk](https://atmon.ai/docs/reference/toolkits/zendesk.md): Support ticketing. - [Zoom](https://atmon.ai/docs/reference/toolkits/zoom.md): Meetings and webinars.