# EightyOne: full documentation Every guide and reference page of https://docs.eightyone.app, concatenated. Generated: don't edit by hand. # EightyOne developer docs EightyOne runs factories and field-service companies. Everything that happens there is a task: machines, work orders, stock, quality, maintenance. These docs are for people and AI agents who build on it: system integrators who set up and run client factories, and teams who connect their own systems. ## Ways in | | For | Start with | | --- | --- | --- | | **REST API** (`/api/v1`) | Integrations, scripts, ERP and MES connectors | [Getting started](getting-started.md), then the [API reference](../reference/capabilities.md) | | **MCP server** (`/mcp`) | AI agents: Claude, Cursor, any MCP client | [Agents and MCP](agents-and-mcp.md) | | **The `eightyone` skill** | Agents that set up or run a factory | [Agents and MCP](agents-and-mcp.md#the-skill) | | **Webhooks** | Being told when something happens | [Webhooks](webhooks.md), then the [event reference](../reference/events.md) | | **Factory specs** | Setting up a whole company from one file | [Commissioning](commissioning.md) | The REST API and the MCP server are generated from one registry of capabilities. The same operation has the same name, input, rules and permissions on both, and both are logged the same way. ## Concepts - [Tasks: everything is a task](tasks.md) - [Production: work orders, routings, counts, lots](production.md) - [Inventory: storage, bins, moves, counts](inventory.md) - [Quality, maintenance and downtime](quality-and-maintenance.md) - [Factory or managed services](factory-and-managed-services.md) ## Reference (generated) - [Capabilities](../reference/capabilities.md): every operation, with its input, output, permission and REST path - [OpenAPI 3.1](../reference/openapi.json): the REST API and its webhooks, machine-readable - [Webhook events](../reference/events.md) - [Errors](../reference/errors.md) - [Permissions](../reference/permissions.md) - [Factory spec schema](../reference/factory-spec.schema.json) - [MCP tools](../skill/eightyone/reference/tools.md) ## Two rules that shape everything - **No financial data.** EightyOne holds no prices, costs or rates: quantities and hours only. Integrations with accounting systems exchange ids, products and quantities. - **An agent or a key acts as a person.** It has that person's permissions in one company, it is logged as them, and it can never do more than they could in the dashboard. --- # Getting started ## Accounts and companies - **One company is one tenant:** a factory or a managed-services business, with its own people, machines, stock and settings. - **Companies are created by EightyOne staff only.** To get one for a client, ask EightyOne. - A person can belong to several companies, with different permissions in each. ## Credentials | | Use it for | How | | --- | --- | --- | | **API key** | Integrations and headless agents | A signed-in user creates it under **Platform → API → Keys**. It acts as that user, in one company, and can be narrowed to fewer permissions than the user has. Send it as `Authorization: Bearer `. | | **OAuth sign-in** | Interactive agents (Claude, Cursor) | The MCP client opens an EightyOne sign-in page; the user approves. | | **Service account** | Integrations that shouldn't act as a named person | A non-human user in a permission group (**Platform → API → Service accounts**), with its own keys. | EightyOne stores no copy of your keys: they're issued and checked by EightyOne's identity provider. A key shows once, when it's created. A revoked key stops working at once. ## Your first call ```bash curl -H "Authorization: Bearer $EIGHTYONE_KEY" https://api.eightyone.app/api/v1/me ``` `me` returns the company the key acts in, the user, and their permissions. Every other call is scoped to that company: you never pass a company id. ```bash curl -H "Authorization: Bearer $EIGHTYONE_KEY" "https://api.eightyone.app/api/v1/tasks?limit=20" ``` ## Conventions - **Ids** are external ids: short strings such as `rpbVkyAl`. Products are addressed by SKU. Database numbers never appear in the API, except where a resource has no other id (reason codes, downtime events, purchase order lines). Each capability's reference says which. - **Paged lists** take `limit` and `offset` and return `{ data, pagination: { limit, offset, total, hasMore } }`. - **Errors** are [problem details](../reference/errors.md). Branch on `code`. - **Retries:** send an `Idempotency-Key` header on writes. A retry with the same key and body returns the first answer, for 24 hours. - **Dry runs:** `Prefer: dry-run` (REST) or `dryRun: true` (MCP) previews a write. It answers what would change and what it would set off, and changes nothing. Every write an agent can call can be previewed. - **Rate limits** answer `429` with `rate_limited`. Back off and retry. - **Tracing:** send `X-EightyOne-Transaction-ID` to tie your logs to ours. Each response carries one either way. ## Logging and audit Every write is recorded under the person the key or agent acts as, in the company's **Audit Logs**. EightyOne also keeps a call log of every API and MCP call: who, which capability, the result and the duration. For writes, it also keeps the input (secrets redacted) for 90 days. --- # Agents and MCP EightyOne runs an [MCP](https://modelcontextprotocol.io) server. Any MCP client, such as Claude, Cursor or your own agent, can read a company's data and work in it, as the person who connected it. ## Connecting - **URL:** `https://api.eightyone.app/mcp`. One connection is one company: if the person belongs to several, add `?company=`. - **Sign in (OAuth):** interactive clients add the URL as a custom connector. EightyOne's sign-in page opens, and the person approves. - **API key:** headless agents send a key as `Authorization: Bearer ` ([Getting started](getting-started.md#credentials)). Start with `me_get`: it shows the company, the person and their permissions. Tools the person can't use aren't listed. ## What an agent can do - **Read:** tasks, machines and their status, work orders, downtime, analytics, storylines (a machine's or an order's history), inventory, quality, partner handoffs. - **Set up a company:** `config_export`, `config_validate`, `config_plan`, `config_apply` ([Commissioning](commissioning.md)). - **Run the floor:** - release work orders, and move, assign and complete tasks, filling in their forms; - record production counts; - open NCRs and disposition them; - report and classify machine stops, and enter meter readings; - move, receive, ship and count stock; - send work to partner companies. The [tool list](../skill/eightyone/reference/tools.md) has the details. Anything not listed as a tool can still be reached with `capabilities_search` and `capabilities_call`. ## The rules agents work under - **Preview, then confirm.** Every write an agent can call takes `dryRun: true`. The answer says whether the change would be accepted, what it changes, and what it sets off (stock posted, tasks created, notifications). The skill tells agents to show that to the person and wait for their OK before the real call. - **Never destructive.** Deleting anything, cancelling a work order, withdrawing a handoff and pruning a factory spec are not available over MCP. An agent tells the person where to do them. - **People give what only people can give:** - photos, signatures and scans are filled in in the app; - stop reasons, dispositions and count decisions come from a person, never from the agent's guess. - **Company guidance.** Administrators can tell every agent working in their company something (**Settings → AI guidance**), and agents read it with `guide_get` topic `company`. ## The skill The `eightyone` skill teaches an agent the domain and the jobs: commissioning a factory from a template, modelling a production line, configuring task types and forms, and running a shift. It's an [Agent Skill](https://agentskills.io): a `SKILL.md` with playbooks, a factory template and the generated tool reference. - **Claude Code:** `/plugin marketplace add eightyone-app/eightyone`, then `/plugin install eightyone@eightyone`. The plugin also connects the MCP server. - **Claude apps and other agents:** download `eightyone-skill-.zip` from [Downloads](downloads.md) and upload it as a custom skill. The skill is versioned with the API: each release's tool reference matches the server it was published from. --- # Tasks: everything is a task In EightyOne, every piece of work is a **task**: a work-order operation, a repair, a preventive service, an inspection, an NCR, a stock move, a count, an approval, a request to a partner. What kind of task it is comes from its **task type**. ## Task types A task type defines a kind of work: - **Its form:** the fields people fill in, ordered. Field types include text, number (with target and limits), checkbox, select, date and time, photo, signature, location, barcode scan, products, storage and machine status. A field can be **required**, and it can be shown only when another field has a given answer. - **Its rules:** - **SLAs:** the due time per priority. - **Gates:** what must be true to enter a status, such as fields filled in, a signature, or a member of certain groups. - **Failure coding:** a problem, a cause and a remedy, required before completion on maintenance types. - **Production:** the product it makes, standard setup and run minutes, the work center and the parameter set. - **Who:** a team, a required skill and a minimum rank. - **Automation:** - the machine status it sets when a task opens or completes; - **flows** (when a task of this type reaches a status, create a task of another type); - recurrence (time-based preventive maintenance). ## Statuses `open` → `preparation` → `ready` → `dispatched` → `submitted` (when the type is reviewed) → `done`. - **`incomplete`:** not finished, with a reason. It can create a follow-up task for the rest. - **`declined`:** refused, with a reason. Status changes follow the rules people get in the dashboard and the app: - a blocked task can't move on; - changing a status needs **Change task status from mobile**, though people can always complete their own tasks; - reassigning needs **Reassign tasks**. ## Forms and completion Completing a task is filling in its form and setting it to done. Through the API or an agent, that's `tasks.submit_form` with the answers and the status (`tasks.get_form` lists the fields first). - **Required fields** must be filled. The server checks every completed form, from any client. - **Measurements outside their limits** fail the check. The completed task then opens an NCR of the company's NCR type, linked to it. - **Machine-status fields** set the machine's status. - **Inventory tasks** post their stock moves. - **Producing tasks** post their output and consume their recipe. - **Photos, signatures and scans** are given by people in the app. A task that requires one waits for them. ## Connections Tasks link to each other: - **dependency:** the second task waits for the first. It can't progress until the first is done, and goes back to open if it was already ready or dispatched. - **follow:** the second follows the first (follow-ups use this). - **created from:** the second was made from the first (NCRs from a failed check, kitting from a task with products, rework from a disposition). `tasks.create` takes `after: { taskId, type }` to create a task already linked. ## Who sees what Members see their own tasks. Seeing every task needs **See all tasks**, and team members see their teams' work. The API and agents see exactly what the person they act as sees. --- # Production: work orders, routings, counts, lots ## The model - **Products** have an item class (raw, wip, finished, operations, spare part), a unit, and a tracking mode: none, lot, or serial. A product is either **bought** (it has vendors) or **made** (it has a **recipe**: the components one unit consumes, in whole units). - **Work centers** group machines that can run the same operations. - **Task types** are the operations: cut, form, weld, inspect. Each has its standard setup and run minutes, the work center it runs in, its parameter set (machine settings, versioned) and its form. - **Routings** list the steps a product goes through, each a task type. A step can be a **gate**: the steps after it wait until it's done. Changing a routing creates a new version; released orders keep the version they started with. - **Work orders** ask for a quantity of a product. Status: `draft` → `released` → `in progress` → `completed` → `closed` (or `cancelled`). ## Release Releasing a work order (`work_orders.release`, which needs **Release work orders**) creates one operation task per routing step: - on the step's machine, when its work center has exactly one; - with the parameter set's released version pinned; - each step after a gate waiting on it, through a dependency. Its preview lists each operation it would create. Work orders usually come from an ERP: `work_orders.upsert` and `work_orders.batch_upsert` create and update draft orders by your system's id. ## Counts - **During a run,** `tasks.record_counts` adds good and scrap quantities. Counts add up and feed OEE. - **At completion,** a producing task needs its counts. They come either with the completion or from the counts recorded during the run. - **Completing posts the output:** - the good quantity goes into stock; - the recipe is consumed (backflushed) from the order's kit cart, then the machine's line-side storage, then any goods bin, earliest lot expiry first. - A work order completes by itself when its last operation is done. ## Lots and genealogy - Receiving a lot-tracked product records its lot. - Completing a work order for a lot-tracked product creates the produced lot. - Consumption links one to the other, so a finished lot traces back to its supplier lots. - Blocked and quarantined lots are never consumed or shipped. ## Downtime and OEE Machines report stops from telemetry, or people report them. Stops are classified with reason codes. `analytics.oee`, `analytics.downtime` and `downtime_events.summary` turn counts, stops and standard times into availability, performance and quality. --- # Inventory: storage, bins, moves, counts ## Where stock is - **Storage sites** hold **storage units** (a shelf, a rack, a bay, a van). - Whose a site is decides what happens to the stock in it: - a company warehouse or a floor's storage: the company's own; - a client's site; - a technician's; - a customer's consigned stock, which can only ship back to that customer; - a partner company's custody. - Every machine (SSU) has its own unit too. - Stock sits in a unit **in a bin:** - **goods** (usable); - **bads** (damaged or scrapped); - **quarantine** (waiting for inspection); - **installed** (fitted into a machine); - **transit** (on the way). - Only goods count as available. - A **line item** is one product in one unit, in one bin, of one lot (or one serial). - `storage_units.get` shows what a unit holds; `inventory.stock_levels` gives goods on hand per product and site. ## Moving stock | | Capability | Notes | | --- | --- | --- | | Move between units or bins | `stock.transfer` | Never more than the line holds. Name the lot or serial when the product is tracked. | | Receive a delivery | `purchase_orders.receive` | Per PO line, into a unit. A lot-tracked product needs its lot. An inspection plan for the product or vendor quarantines the receipt and opens an inspection task. Never more than was ordered. | | Ship to a customer | `shipments.create`, then `shipments.ship` | A draft holds no stock. Shipping takes goods out, checking the lot, the stock and consignment. A customer that's an EightyOne company receives them in its transit. Factories only. | | Partner goods | `partner_deliveries.receive` | Goods a partner company sent you. | Inventory tasks (moves done in the app) post the same way when completed. Purchase orders are quantities only: EightyOne holds no prices. ## Cycle counts 1. `inventory_counts.create`: a count of one unit. It snapshots what's expected, and creates the count task for operators when the company has an inventory task type. 2. Counts come from that task in the app, or from `inventory_counts.record_lines`. 3. `inventory_counts.finalize` freezes the expected quantities as they are now, works out each difference, and adds anything on hand the count never saw (counted 0). It flags lines whose stock moved while the count was open. 4. `inventory_counts.reconcile` posts the decisions, which needs **Adjust inventory**: - **accepted:** stock becomes the counted quantity; - **adjusted:** stock becomes a corrected quantity; - **ignored:** stock stays as it is. Every write-off needs an adjustment reason. The preview lists each addition, write-off and bin move. ## Reordering A stock policy per product (and optionally per site) sets a minimum, or a reorder point. The reorder point is computed from lead time × measured consumption. When stock falls to it, `stock.reorder_triggered` fires once, and the product appears in `inventory.reorder_suggestions`. It re-arms when stock recovers. --- # Quality, maintenance and downtime ## Quality - **Inspections** are tasks of inspection types. They're created: - by a routing step; - by an inspection plan on receiving (the receipt is quarantined until the inspection passes); - by people. - **In-line checks:** a number field with limits (`lsl`, `usl`) is a measurement. A completed task with a measurement outside its limits opens an **NCR** of the company's NCR type, linked to it and carrying its machine and work order. - **NCRs** are tasks too: - `ncrs.open` raises one from the task where a problem was found, with the affected lot or product; - `ncrs.disposition` records the decision, once per NCR, and needs **Disposition NCRs**. - **Dispositions:** - **use as is:** recorded only; - **rework:** creates a rework task; - **scrap:** moves the quantity of the lot to the bads bin and blocks the lot; - **return:** blocks the lot for return to the vendor. ## Maintenance - **Corrective work** is a task of a maintenance type, opened by a person, an issue report (from the app or a QR code), or a condition rule on machine data. - **Failure coding:** maintenance types can require a problem, a cause and a remedy code to complete (`tasks.update` or `tasks.submit_form` with `problemCodeId`, `causeCodeId`, `remedyCodeId`). `analytics.failure_pareto` ranks failure modes. - **Preventive maintenance** has three triggers: - **time-based:** a task type's recurrence schedules the next task when one completes; - **usage-based:** a meter (run hours, strokes, cycles) with a rule "every N" opens a task each time the meter advances by N; - **condition-based:** a rule on a live signal (temperature above 80) opens a task when it's crossed. - **Meters:** manual meters take readings (`meters.record_reading`, the running total), and signal meters count from machine data. A reading's preview says which preventive tasks it would open. - **Spare parts:** each machine lists its spare parts. ## Downtime - **A stop** is one downtime event: when the machine stopped, when it ran again, and the reason it was given. - Machines with telemetry report their stops themselves (`auto`). - For others, people report them (`downtime_events.open` and `.close`). - **Classifying** a stop (`downtime_events.classify`) gives it a downtime reason code. Agents classify only with the reason a person gives: an unclassified stop is a gap to report, not a guess to fill. - **Analysis:** - `downtime_events.summary` gives stopped time per machine and reason; - `analytics.downtime` measures time spent in a DOWN or MAINTENANCE status; - `analytics.reliability` gives MTBF and MTTR. --- # Factory or managed services A company is one of two kinds. Its kind decides what clients, sites and shipping mean. | | Factory | Managed services | | --- | --- | --- | | Clients | One: the company itself | Many: the customers it serves | | Sites | Its floors and areas | Its clients' locations | | SSUs | Its machines, lines and cells | The assets it maintains at clients' sites | | Stock leaves by | Shipping to **customers** (off-system, or other EightyOne companies) | Being moved to client sites (installed), never shipped | | Typical work | Work orders, operations, counts, quality, maintenance | Service and maintenance visits, installs | Both use the same tasks, task types, forms, storage and analytics. Shipments, customers and consigned stock exist only in factories. ## Partners Two EightyOne companies can work together: - **Handoffs:** send a task to a partner; they get their own copy, and its status flows back. - **Partner deliveries:** goods travel with a handoff, or with a shipment to a customer that is an EightyOne company. They arrive in the partner's transit and are received there. - **Data requests:** ask a partner for data (for example, a production record), which they release as a data package. Each company keeps its own data. A partner sees only what was sent to it. --- # Commissioning: a company from one file A **factory spec** is a company's whole setup as one JSON document: plant, supply, work, machine data, quality and people. - Everything in it refers to everything else **by name**, so a spec from one company applies to another. - A section you leave out isn't touched. ## The sections | Stage | Sections | | --- | --- | | Plant | `ssuStatuses`, `reasonCodes`, `workCenters`, `sites` (with their storage and the SSU tree), `warehouses`, `settings` | | Supply | `productCategories`, `vendors`, `products` (by SKU, with vendors, recipe and stock policies) | | Organisation | `permissionGroups`, `roles`, `teams`, `skills` | | Work | `parameterSets`, `taskTypes` (forms, SLAs, gates, flags), `routings` | | Machines and quality | `machineData` (signals, meters, condition rules, spare parts), `flows`, `issues`, `inspectionPlans`, `guidance` | | People | `people`: existing users only, with their roles, teams and skills | The [schema](../reference/factory-spec.schema.json) has every field. ## The calls 1. **`config.export`:** the company's setup as a spec. It's the best example of the format, and the starting point for a change. 2. **`config.validate`:** problems, each with its path in the file (a status that doesn't exist, a gate naming a field the form lacks). 3. **`config.plan`:** what applying would do, item by item: creates, updates field by field, what's blocked by permissions, and what's in the company but not in the spec (left alone). 4. **`config.apply`:** applies the plan through the same rules as the dashboard, audited. If a change fails, the earlier ones stay and the answer names the failure. Fix the file and apply again: unchanged items are skipped. Over REST, `prune` also removes what's in the company but not in the spec. Agents can't prune. ## Working as an SI - **With an agent:** - the SI describes the factory or hands over documents; - the agent, with the `eightyone` skill, starts from a template, builds the spec, and shows the plan; - it applies after the SI's go-ahead, in stages for a large factory. - **With files:** 1. commission one reference factory; 2. export it, and keep the spec in your own repository as your template; 3. for each client, rename what's specific, validate, plan and apply. The skill's Brazos Metalworks template, a sheet-metal fabricator, was made that way. ## What a spec doesn't do - **People:** a spec never invites anyone. Invite them in the dashboard; a spec can then give them roles, teams and skills. - **Permissions:** nobody can grant more than they have themselves, and the company always keeps an administrator. - **Versions:** changing a routing or a parameter set releases a new version, and orders already released keep theirs. - **Companies:** only EightyOne staff create them. - **Prices:** none, anywhere. --- # Webhooks **What it is:** EightyOne tells your systems when something happens, such as a task changing status, a work order released, a machine stopping or a lot created. It does this by sending an HTTPS `POST` to a URL you choose. Each message is signed, so your system can check it really came from EightyOne. **Where:** in the dashboard, **Platform → API → Webhooks**, or the API's `webhooks.*` capabilities. You need **Manage webhooks** to create or change one. ## Setting one up 1. **New webhook:** give it a name, your HTTPS endpoint, and the events it should receive. Private and internal addresses are refused. 2. **Send test:** sends a signed `webhook.test` message to that endpoint only, so you can check your receiver. 3. **Recent deliveries:** each webhook lists what it sent, with the result. Filter by event type or status to find a failure. A delivery your endpoint doesn't answer with a 2xx is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 24 hours. Then it's marked failed, and the webhook counts the failure. ## What a delivery looks like Messages follow the [Standard Webhooks](https://www.standardwebhooks.com/) format: ``` POST /your/endpoint webhook-id: msg_2b9f… webhook-timestamp: 1760860800 webhook-signature: v1a, Content-Type: application/json { "type": "task.status_changed", "timestamp": "2026-10-19T08:00:00.000Z", "data": { … } } ``` - **`type`** is the event; **`data`** is its content. - Ids in `data` are the same ones the API uses: task and machine external ids, SKUs, work order ids. They are never internal database numbers. - **`webhook-id`** stays the same on every retry of a message. Use it to ignore a message you've already handled. - Messages can arrive out of order. Use `timestamp`, or fetch the current state from the API, when order matters. The [event reference](../reference/events.md) lists every event and its fields; the [OpenAPI document](../reference/openapi.json) has them too, in its `webhooks` section. ## Checking a signature EightyOne signs every message with its own key (Ed25519). There is **no secret** to copy or store: you verify with EightyOne's **public key**. - The public key is shown on each webhook. - Your system can fetch it from `https://api.eightyone.app/api/v1/webhook-signing-keys`. That URL needs no login. To check a message: 1. Join `webhook-id`, `webhook-timestamp` and the raw request body with dots: `..`. 2. The `webhook-signature` header holds one or more `v1a,` entries. Accept the message if any of them verifies against any published key. 3. Refuse a timestamp older than a few minutes, so an old message can't be replayed. In Node.js: ```js import { createPublicKey, verify } from "node:crypto"; // Fetch once and cache. During a key change two keys are listed, and both sign. const response = await fetch("https://api.eightyone.app/api/v1/webhook-signing-keys"); const keys = (await response.json()).keys.map((k) => createPublicKey({ // An Ed25519 public key in the SPKI format Node expects: a fixed prefix plus the 32 key bytes. key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(k.publicKey.slice(5), "base64")]), format: "der", type: "spki", }), ); export function isFromEightyOne(headers, rawBody) { const id = headers["webhook-id"]; const timestamp = Number(headers["webhook-timestamp"]); if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // older than 5 minutes const signed = Buffer.from(`${id}.${timestamp}.${rawBody}`); return headers["webhook-signature"].split(" ").some((entry) => { const [version, signature] = entry.split(","); return version === "v1a" && keys.some((key) => verify(null, signed, key, Buffer.from(signature, "base64"))); }); } ``` Verify the **raw** body, exactly as received. Parsing it and serializing it again changes the bytes, and the signature won't match. ## What changed (October 2026) Webhooks moved to Standard Webhooks. If you built a receiver before, update it: - **Signature:** `X-EightyOne-Signature` (HMAC with a per-webhook secret) is replaced by `webhook-signature`, verified with EightyOne's public key. Existing webhook secrets were removed, and none are issued any more. - **Body:** `{ event, eventId, createdAt, data }` is now `{ type, timestamp, data }`. `eventId` is now the `webhook-id` header. - **Content:** events carry the API's ids. For example, `task.created` has `task.id` (the external id) and `task.type: { id, name }`, instead of database ids such as `typeId`. - **Reliability:** a message is recorded with the change that caused it. A change that's undone sends nothing, and a message is never lost when EightyOne restarts. --- # Downloads ## The `eightyone` skill - **Claude Code:** install it as a plugin, which keeps it up to date and connects the MCP server: ``` /plugin marketplace add eightyone-app/eightyone /plugin install eightyone@eightyone ``` - **Claude apps and other agents:** download [eightyone-skill.zip](../downloads/eightyone-skill.zip) (the latest release) and upload it as a custom skill. Each release is also kept as `eightyone-skill-.zip`. The skill's version is in its `SKILL.md` (`metadata.version`). It's released with the API, so its tool reference matches the server. ## Machine-readable docs - [llms.txt](../llms.txt): an index of these docs for agents. - [llms-full.txt](../llms-full.txt): everything in one file. - [OpenAPI 3.1](../reference/openapi.json), [factory spec schema](../reference/factory-spec.schema.json). --- # EightyOne capabilities Every operation EightyOne exposes to code and agents. Each is one capability: the REST API (`/api/v1`, OpenAPI at `/api/v1/openapi.json`) and the MCP server (`/mcp`) are generated from these definitions, so they cannot disagree. - **MCP:** read capabilities only for now. A capability is either a listed tool or reached through `capabilities_search` / `capabilities_call`; an agent sees only what its credential may run. - **Auth:** `Authorization: Bearer ` (or `X-API-Key`). The key's company is the company; a key acts as its owner, narrowed to the key's permissions. - **Errors:** RFC 9457 `application/problem+json` with a stable `code`. - **Writes:** send `Idempotency-Key: ` to make a retry safe (24 h). Where marked, `Prefer: dry-run` runs the write, rolls it back and returns the planned changes. ## Account ### `me.get`: Who am I The caller: the user this credential acts as, the company it is bound to, how it is calling (REST or MCP; human, agent or service account) and the permissions it actually has here (the union of the user's roles' groups ∩ the credential's permissions). Call it first to know what you can do. **REST:** `GET /api/v1/me` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `me_get` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `user` | object | yes | | | `company` | object | yes | | | `actorType` | `human` \| `agent` \| `service` | yes | | | `channel` | string | yes | | | `permissions` | string[] | yes | | ## Guides ### `guide.get`: Read a guide EightyOne concepts and how to read the data: the glossary, downtime, maintenance, production, inventory, quality, commissioning, what deserves attention, and this company's own guidance (topic "company"). Without a topic, lists the topics. Read the glossary before working on an unfamiliar area. **REST:** `GET /api/v1/guides` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `guide_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `topic` | `glossary` \| `downtime` \| `maintenance` \| `production` \| `inventory` \| `quality` \| `commissioning` \| `signals` \| `company` | | "company": what this company's administrators tell agents, including notes on specific machines | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `topic` | string \| null | yes | | | `title` | string | yes | | | `body` | string | yes | Markdown | | `topics` | object[] | yes | | ### `guidance.list`: List company guidance for agents What the company's administrators tell every AI agent: company-wide guidance and notes on specific machines, work centers, products and task types, with any thresholds they set. **REST:** `GET /api/v1/guidance` · **Permission:** any member of the company · **Effect:** read · **MCP:** via `capabilities_call` **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | `company` \| `ssu` \| `work_center` \| `product` \| `task_type` | yes | | | `ref` | string \| null | yes | The SSU / product / task type external id, or the work center name | | `name` | string \| null | yes | What it's about, by name | | `body` | string | yes | | | `thresholds` | object | yes | | | `version` | integer | yes | | | `reviewedAt` | string (date-time) \| null | yes | | | `updatedAt` | string (date-time) | yes | | ### `guidance.set`: Set company guidance for agents Write the guidance agents get for the company, or for one machine, work center, product or task type. Replaces that subject's text; the previous version is kept in the audit log. Every agent connected to the company follows it. **REST:** `PUT /api/v1/guidance` · **Permission:** `ai_guidance:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | `company` \| `ssu` \| `work_center` \| `product` \| `task_type` | yes | | | `ref` | string | | Which one (not for company): SSU external id, work center name, product external id or SKU, task type external id | | `body` | string | yes | Plain text or Markdown. Company: up to 4000 characters; one subject: 1000 | | `thresholds` | object | | | | `reviewed` | boolean \| `true` \| `false` | | Mark it as reviewed now (agents see how fresh it is) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | `company` \| `ssu` \| `work_center` \| `product` \| `task_type` | yes | | | `ref` | string \| null | yes | The SSU / product / task type external id, or the work center name | | `name` | string \| null | yes | What it's about, by name | | `body` | string | yes | | | `thresholds` | object | yes | | | `version` | integer | yes | | | `reviewedAt` | string (date-time) \| null | yes | | | `updatedAt` | string (date-time) | yes | | ### `guidance.delete`: Remove company guidance for agents Remove the guidance for the company or one subject. The audit log keeps what it said. **REST:** `DELETE /api/v1/guidance` · **Permission:** `ai_guidance:manage` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | `company` \| `ssu` \| `work_center` \| `product` \| `task_type` | yes | | | `ref` | string | | | ## Tasks ### `tasks.list`: List tasks Tasks, newest first. Everything in EightyOne is a task (work-order operations, maintenance, inspections, NCRs, approvals…). Open tasks only unless a status is given or includeCompleted=true. Without the 'See all tasks' permission you see only tasks assigned to you, plus your teams' tasks with 'See tasks for my team'. **REST:** `GET /api/v1/tasks` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `tasks_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | | `status` | `open` \| `preparation` \| `ready` \| `dispatched` \| `submitted` \| `incomplete` \| `done` \| `declined` | | | | `typeId` | string | | Task type external id | | `ssuId` | string | | SSU external id | | `includeCompleted` | boolean \| `true` \| `false` | | Include done and declined tasks | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `tasks.get`: Get a task One task by its id. **REST:** `GET /api/v1/tasks/:id` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `tasks_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `tasks.create`: Create a task Create a task of a task type, exactly as the dashboard does: the type's defaults apply (SLA due date, default assignee, transit, created-as-ready), and the task.created webhook fires. Needs 'Create tasks' unless the type is user-generated. **REST:** `POST /api/v1/tasks` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `tasks_create` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `typeId` | string | yes | Task type external id | | `title` | string | yes | | | `details` | string | | | | `status` | `open` \| `preparation` \| `ready` \| `dispatched` \| `submitted` \| `incomplete` \| `done` | | | | `startDate` | string (date-time) \| string (date) | | | | `endDate` | string (date-time) \| string (date) | | | | `dueDate` | string (date-time) \| string (date) | | Defaults to the type's SLA for the priority | | `priorityId` | integer | | | | `ssuId` | string | | SSU external id | | `externalSystemId` | string | | | | `fromStorageUnitId` | integer | | | | `toStorageUnitId` | integer | | | | `fromVendorId` | integer | | | | `toVendorId` | integer | | | | `assigneeIds` | integer[] | | Personnel ids | | `incompleteReason` | string | | Required when status is 'incomplete' | | `after` | object | | Link the new task to an existing one: dependency (it waits for that task) or follow | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `tasks.update`: Update a task Change a task, with the dashboard's rules: status changes need 'Change task status from mobile' (assignees may still complete their own work), reassigning needs 'Reassign tasks', blocked tasks can't progress, producing tasks complete with goodQuantity / scrapQuantity (counts during the run: tasks_record_counts), maintenance types may need failure codes (problemCodeId, causeCodeId, remedyCodeId) to complete, 'incomplete' needs a reason and can create a follow-up (createFollowUp), 'declined' needs a reason. Tasks sent to a partner only accept details, dueDate and priorityId. **REST:** `PATCH /api/v1/tasks/:id` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `tasks_update` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `title` | string | | | | `details` | string | | | | `status` | `open` \| `preparation` \| `ready` \| `dispatched` \| `submitted` \| `incomplete` \| `done` \| `declined` | | | | `startDate` | string (date-time) \| string (date) \| null | | | | `endDate` | string (date-time) \| string (date) \| null | | | | `dueDate` | string (date-time) \| string (date) \| null | | | | `priorityId` | integer | | | | `ssuId` | string \| null | | SSU external id, or null to clear | | `externalSystemId` | string | | | | `assigneeIds` | integer[] | | Personnel ids: replaces the assignees | | `incompleteReason` | string | | | | `declinedReason` | string | | | | `createFollowUp` | boolean | | With status 'incomplete': create a follow-up task for the rest | | `goodQuantity` | number | | | | `scrapQuantity` | number | | | | `scrapReasonCodeId` | integer | | | | `problemCodeId` | integer | | Failure coding (reason_codes_list kind failure_problem); types that require it can't complete without all three | | `causeCodeId` | integer | | Failure coding: kind failure_cause | | `remedyCodeId` | integer | | Failure coding: kind failure_remedy | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `_meta` | object | yes | | ### `tasks.record_counts`: Record production counts Add good and scrap quantities to a running work-order operation or producing task, as operators do from the app. Counts add up and feed OEE; the output posts to stock once, when the task completes. To count at completion instead, pass goodQuantity / scrapQuantity to tasks.update with the status. **REST:** `POST /api/v1/tasks/:id/counts` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `tasks_record_counts` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `goodQuantity` | number | | | | `scrapQuantity` | number | | | | `scrapReasonCodeId` | integer | | A scrap reason (reason_codes_list, kind scrap) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `goodQuantity` | number | yes | | | `scrapQuantity` | number | yes | | | `scrapReasonCodeId` | integer \| null | yes | | ### `tasks.connect`: Connect two tasks Link two tasks. 'dependency': the second waits for the first. It can't progress until the first is done, and goes back to open if it was already ready or dispatched (it becomes ready if the first is already done). 'follow': only records that the second follows the first. Connections are removed in the dashboard. **REST:** `POST /api/v1/tasks/:from/connections` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `tasks_connect` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string | yes | The first task's external id | | `to` | string | yes | The external id of the task that waits for or follows it | | `type` | `dependency` \| `follow` | yes | | | `description` | string | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string | yes | | | `to` | string | yes | | | `type` | string | yes | | | `description` | string \| null | yes | | | `toStatus` | string | yes | The second task's status after connecting | ### `tasks.delete`: Delete a task Delete a task, as the dashboard does: a task you sent to a partner withdraws the handoff; a task a partner sent you can't be deleted (decline it instead). **REST:** `DELETE /api/v1/tasks/:id` · **Permission:** `tasks:delete` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | ### `tasks.get_form`: A task's form The fields a task's form records, in order: type, whether it's required, the choices of a select, the limits of a measurement, whether a condition hides it, and the current answer. Answer them with tasks_submit_form. **REST:** `GET /api/v1/tasks/:id/form` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `tasks_get_form` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `fields` | object[] | yes | | ### `tasks.submit_form`: Fill in a task's form Answer a task's form fields by title (tasks_get_form lists them), and optionally complete the task (status done or submitted) or mark it incomplete, as the app does. Completing needs every required field; measurements outside their limits open an NCR; machine-status fields set the machine's status. Counts and failure codes go with the completion, as in tasks_update. Preview it first. **REST:** `POST /api/v1/tasks/:id/form` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `tasks_submit_form` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `answers` | object[] | yes | Answers by field title; fields left out keep their answer | | `status` | `done` \| `submitted` \| `incomplete` | | Complete it (done, or submitted for review) or mark it incomplete; leave out to only save the answers | | `incompleteReason` | string | | | | `createFollowUp` | boolean | | | | `goodQuantity` | number | | | | `scrapQuantity` | number | | | | `scrapReasonCodeId` | integer | | | | `problemCodeId` | integer | | | | `causeCodeId` | integer | | | | `remedyCodeId` | integer | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `failedChecks` | string[] | yes | | | `ncrId` | string \| null | yes | | ## Task types ### `task_types.list`: List task types The company's task types: the templates every task is created from (work orders' operations, maintenance, NCRs, inspections, approvals…), alphabetically. Use a type's `id` as `typeId` when creating a task. **REST:** `GET /api/v1/task-types` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `task_types_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task type external id | | `typeName` | string | yes | | | `color` | string \| null | yes | | | `isQuote` | boolean | yes | | | `isUserGenerated` | boolean | yes | Field users may create tasks of this type themselves | | `isDataRequest` | boolean | yes | Tasks of this type ask a partner company for traceability data | | `isRemote` | boolean | yes | | | `isInventory` | boolean | yes | | | `defaultUseTransit` | boolean | yes | Inventory tasks stage stock through the transit bin by default | | `isBulk` | boolean | yes | | | `workCenterId` | integer \| null | yes | | | `team` | object \| null | yes | The team whose members see and take this type's tasks. Changing it moves every task of the type | | `parentProductId` | integer \| null | yes | The product a production operation of this type makes | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `task_types.get`: Get a task type One task type by its id. **REST:** `GET /api/v1/task-types/:id` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `task_types_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task type external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task type external id | | `typeName` | string | yes | | | `color` | string \| null | yes | | | `isQuote` | boolean | yes | | | `isUserGenerated` | boolean | yes | Field users may create tasks of this type themselves | | `isDataRequest` | boolean | yes | Tasks of this type ask a partner company for traceability data | | `isRemote` | boolean | yes | | | `isInventory` | boolean | yes | | | `defaultUseTransit` | boolean | yes | Inventory tasks stage stock through the transit bin by default | | `isBulk` | boolean | yes | | | `workCenterId` | integer \| null | yes | | | `team` | object \| null | yes | The team whose members see and take this type's tasks. Changing it moves every task of the type | | `parentProductId` | integer \| null | yes | The product a production operation of this type makes | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ## Teams ### `teams.list`: List teams The company's teams, alphabetically, with their members' personnel ids. A task belongs to its task type's team: team members with 'See tasks for my team' see those tasks and can take them in the field. **REST:** `GET /api/v1/teams` · **Permission:** any member of the company · **Effect:** read · **MCP:** via `capabilities_call` **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Team id | | `name` | string | yes | | | `color` | string | yes | | | `memberIds` | integer[] | yes | Personnel ids of the members | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `teams.create`: Create a team Create a team, optionally with its members. Point task types at it to give it work. **REST:** `POST /api/v1/teams` · **Permission:** `settings:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `color` | string | | Hex color, e.g. #026485 | | `memberIds` | integer[] | | Personnel ids: replaces the members | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Team id | | `name` | string | yes | | | `color` | string | yes | | | `memberIds` | integer[] | yes | Personnel ids of the members | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `teams.update`: Update a team Rename or recolor a team, or replace its members (memberIds). Only the fields sent change. Members see the team's tasks at once. **REST:** `PATCH /api/v1/teams/:id` · **Permission:** `settings:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | | | | `color` | string | | Hex color, e.g. #026485 | | `memberIds` | integer[] | | Personnel ids: replaces the members | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Team id | | `name` | string | yes | | | `color` | string | yes | | | `memberIds` | integer[] | yes | Personnel ids of the members | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `teams.delete`: Delete a team Delete a team and its memberships. Refused while a task type still belongs to it: move those types to another team (or none) first. **REST:** `DELETE /api/v1/teams/:id` · **Permission:** `settings:manage` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | ## Clients ### `clients.list`: List clients The company's clients (the customers whose sites and SSUs work happens at), newest first. Filter by name or active flag. **REST:** `GET /api/v1/clients` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `clients_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | | `search` | string | | Case-insensitive name contains | | `isActive` | boolean \| `true` \| `false` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string \| null | yes | Your id for the client (its external system id). null for clients created in EightyOne without one: they are listed, but can't be fetched or changed by id over the API | | `name` | string | yes | | | `isActive` | boolean | yes | | | `contactPersonName` | string \| null | yes | | | `contactPersonEmail` | string \| null | yes | | | `contactPersonPhone` | string \| null | yes | | | `freeText` | string \| null | yes | | | `color` | string \| null | yes | | | `isDefault` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `clients.get`: Get a client One client by your id for it. **REST:** `GET /api/v1/clients/:id` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `clients_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string \| null | yes | Your id for the client (its external system id). null for clients created in EightyOne without one: they are listed, but can't be fetched or changed by id over the API | | `name` | string | yes | | | `isActive` | boolean | yes | | | `contactPersonName` | string \| null | yes | | | `contactPersonEmail` | string \| null | yes | | | `contactPersonPhone` | string \| null | yes | | | `freeText` | string \| null | yes | | | `color` | string \| null | yes | | | `isDefault` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `clients.create`: Create a client Create a client with your own id for it (used to address it afterwards). Fires the client.created webhook. **REST:** `POST /api/v1/clients` · **Permission:** `clients:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Your id for the client; must be unique in the company | | `name` | string | yes | | | `contactPersonName` | string \| null | | | | `contactPersonEmail` | string (email) \| null | | | | `contactPersonPhone` | string \| null | | | | `freeText` | string \| null | | | | `color` | string | | | | `isDefault` | boolean | | Make this the company's default client (unsets the previous default) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string \| null | yes | Your id for the client (its external system id). null for clients created in EightyOne without one: they are listed, but can't be fetched or changed by id over the API | | `name` | string | yes | | | `isActive` | boolean | yes | | | `contactPersonName` | string \| null | yes | | | `contactPersonEmail` | string \| null | yes | | | `contactPersonPhone` | string \| null | yes | | | `freeText` | string \| null | yes | | | `color` | string \| null | yes | | | `isDefault` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `clients.update`: Update a client Change any of a client's fields; only the fields sent change. **REST:** `PATCH /api/v1/clients/:id` · **Permission:** `clients:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | | | | `isActive` | boolean | | | | `contactPersonName` | string \| null | | | | `contactPersonEmail` | string (email) \| null | | | | `contactPersonPhone` | string \| null | | | | `freeText` | string \| null | | | | `color` | string | | | | `isDefault` | boolean | | Make this the company's default client (unsets the previous default) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string \| null | yes | Your id for the client (its external system id). null for clients created in EightyOne without one: they are listed, but can't be fetched or changed by id over the API | | `name` | string | yes | | | `isActive` | boolean | yes | | | `contactPersonName` | string \| null | yes | | | `contactPersonEmail` | string \| null | yes | | | `contactPersonPhone` | string \| null | yes | | | `freeText` | string \| null | yes | | | `color` | string \| null | yes | | | `isDefault` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `clients.delete`: Delete a client Delete (archive) a client. Its history is kept. **REST:** `DELETE /api/v1/clients/:id` · **Permission:** `clients:manage` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | ## SSUs ### `ssus.list`: List SSUs The company's SSUs (sub-site units: machines, assets, locations within a site), newest first. **REST:** `GET /api/v1/ssus` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `ssus_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | SSU external id (also its QR identity) | | `name` | string | yes | | | `imageUrl` | string \| null | yes | | | `isActive` | boolean | yes | | | `isDefault` | boolean | yes | | | `location` | any \| null | yes | GeoJSON point (PostGIS geometry), or null | | `make` | string \| null | yes | Machine make, from the global machine catalog (factories) | | `model` | string \| null | yes | Machine model, from the global machine catalog | | `sku` | string \| null | yes | Model-level SKU / part number (not a serial number) | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `ssus.get`: Get an SSU One SSU by its external id, with the company's guidance note on it for agents (if any): read it before drawing conclusions about this machine. **REST:** `GET /api/v1/ssus/:id` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `ssus_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | SSU external id (also its QR identity) | | `name` | string | yes | | | `imageUrl` | string \| null | yes | | | `isActive` | boolean | yes | | | `isDefault` | boolean | yes | | | `location` | any \| null | yes | GeoJSON point (PostGIS geometry), or null | | `make` | string \| null | yes | Machine make, from the global machine catalog (factories) | | `model` | string \| null | yes | Machine model, from the global machine catalog | | `sku` | string \| null | yes | Model-level SKU / part number (not a serial number) | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `guidance` | object \| null | yes | What the company's administrators tell agents about this SSU | ### `ssus.update`: Update an SSU Change an SSU's name, image, active flag, default flag or location. Making it the default unsets the default on the other SSUs of its sites. **REST:** `PUT /api/v1/ssus/:id` · **Permission:** `clients:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | | | | `imageUrl` | string | | | | `isActive` | boolean | | | | `isDefault` | boolean | | | | `location` | object | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | SSU external id (also its QR identity) | | `name` | string | yes | | | `imageUrl` | string \| null | yes | | | `isActive` | boolean | yes | | | `isDefault` | boolean | yes | | | `location` | any \| null | yes | GeoJSON point (PostGIS geometry), or null | | `make` | string \| null | yes | Machine make, from the global machine catalog (factories) | | `model` | string \| null | yes | Machine model, from the global machine catalog | | `sku` | string \| null | yes | Model-level SKU / part number (not a serial number) | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ## Inventory ### `line_items.find_ssu_by_barcode`: Find the SSU holding a scanned unit Resolve a scanned line-item barcode to the SSU (machine / location) whose storage unit holds it. 404 when the unit, its storage unit or that storage unit's SSU is unknown. **REST:** `GET /api/v1/line-items/ssu` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `line_items_find_ssu_by_barcode` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `barcode` | string | yes | Line item barcode (code) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `imageUrl` | string \| null | yes | | | `isActive` | boolean | yes | | | `location` | any \| null | yes | GeoJSON point (PostGIS geometry), or null | ### `stock_transactions.list`: Export stock movements The stock movement ledger (consumption, receipts, transfers, shipments), newest first, for pull-based ERP sync. Quantities only. **REST:** `GET /api/v1/stock-transactions` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `stock_transactions_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) | | Movements at or after this time | | `to` | string (date-time) | | Movements before this time | | `limit` | integer | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `time` | string (date-time) | yes | | | `quantity` | number | yes | | | `bin` | string \| null | yes | goods, bads, installed, transit, quarantine | | `itemId` | integer \| null | yes | Line item (unit) id | | `fromStorageUnitId` | integer \| null | yes | | | `toStorageUnitId` | integer \| null | yes | | | `fromVendorId` | integer \| null | yes | | | `toVendorId` | integer \| null | yes | | | `toClientId` | integer \| null | yes | | | `toCustomerId` | integer \| null | yes | | | `taskId` | integer \| null | yes | | | `lotId` | integer \| null | yes | | | `serialId` | integer \| null | yes | | | `reasonCodeId` | integer \| null | yes | | | `stockTransactionGroupId` | integer \| null | yes | | ### `inventory.stock_levels`: Stock on hand Goods on hand per product and storage site (up to 2000 rows), by the same rule reordering uses: good stock only (not the bads bin), not counting stock held by a partner. Filter by product id, SKU or storage site. **REST:** `GET /api/v1/inventory/stock-levels` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `inventory_stock_levels` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `productId` | string | | Product external id | | `sku` | string | | Product SKU (case-insensitive) | | `storageSiteId` | string | | Storage site external id | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `productId` | string | yes | | | `sku` | string \| null | yes | | | `name` | string | yes | | | `uom` | string \| null | yes | | | `itemClass` | string | yes | raw, wip, finished, operations or spare_part | | `storageSiteId` | string \| null | yes | | | `storageSiteName` | string \| null | yes | | | `onHand` | number | yes | | ### `inventory.reorder_suggestions`: Reorder suggestions Products below their minimum or at their dynamic reorder point (lead time × measured consumption), with the suggested quantity and vendor. Quantities and days only. **REST:** `GET /api/v1/inventory/reorder-suggestions` · **Permission:** `products:view` · **Effect:** read · **MCP:** tool `inventory_reorder_suggestions` **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `productName` | string | yes | | | `sku` | string \| null | yes | | | `onHand` | number | yes | | | `minQty` | number | yes | | | `suggestedQty` | number | yes | | | `suggestedVendorName` | string \| null | yes | | | `vendorLeadTime` | number \| null | yes | Days | | `dailyConsumption` | number \| null | yes | | | `reorderPoint` | number \| null | yes | | | `triggerReason` | `below_min` \| `below_reorder_point` | yes | | ### `storage_units.list`: List storage units Where stock sits: each storage unit (shelf, rack, bay, dock…) with its storage site and whose it is: warehouse or site (the company's own), client, personnel (a technician's van), customer_consigned, partner. **REST:** `GET /api/v1/storage-units` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `storage_units_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `storageSiteId` | string | | Storage site external id | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Storage unit external id | | `name` | string | yes | | | `site` | object \| null | yes | | ### `storage_units.get`: What's in a storage unit A storage unit and everything in it: per product, bin (goods / bads / quarantine), lot and serial. **REST:** `GET /api/v1/storage-units/:id` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `storage_units_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Storage unit external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Storage unit external id | | `name` | string | yes | | | `site` | object \| null | yes | | | `contents` | object[] | yes | | ### `stock.transfer`: Move stock Move stock between storage units, or between bins in one unit, as the dashboard's transfers do: name the product by SKU, and the lot or serial when it's tracked. It can't move more than is there. Needs 'Manage inventory'. **REST:** `POST /api/v1/stock/transfers` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `stock_transfer` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `sku` | string | yes | | | `quantity` | number | yes | | | `from` | string | yes | Storage unit external id it leaves | | `to` | string | yes | Storage unit external id it goes to | | `bin` | `goods` \| `bads` \| `quarantine` | | The bin it's taken from | | `toBin` | `goods` \| `bads` \| `quarantine` | | The bin it lands in (default: the same) | | `lotCode` | string | | Which lot, for lot-tracked products | | `serialCode` | string | | Which unit, for serialized products | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `sku` | string | yes | | | `quantity` | number | yes | | | `from` | object | yes | | | `to` | object | yes | | | `bin` | string | yes | | | `toBin` | string | yes | | | `lotCode` | string \| null | yes | | ### `purchase_orders.list`: List purchase orders Purchase orders (purchase requests): vendor, status, and each line's product, quantity ordered and received. Quantities only: EightyOne holds no prices. **REST:** `GET /api/v1/purchase-orders` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `purchase_orders_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | | e.g. open, partially_received, received | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `name` | string \| null | yes | | | `status` | string | yes | | | `vendor` | string \| null | yes | | | `externalSystemId` | string \| null | yes | | | `lines` | object[] | yes | | ### `purchase_orders.receive`: Receive against a purchase order Book what arrived: per PO line, the quantity and the storage unit it goes to (with the lot for lot-tracked products). An inspection plan for the product or vendor puts it in quarantine and opens an inspection task. Can't receive more than ordered. Needs 'Receive inventory'. Preview it first. **REST:** `POST /api/v1/purchase-orders/:id/receive` · **Permission:** `inventory:receive` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `purchase_orders_receive` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Purchase order id | | `lines` | object[] | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `name` | string \| null | yes | | | `status` | string | yes | | | `vendor` | string \| null | yes | | | `externalSystemId` | string \| null | yes | | | `lines` | object[] | yes | | ### `customers.list`: List customers A factory's customers, who shipments go to: name, reference and ship-to address. **REST:** `GET /api/v1/customers` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `customers_list` **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `externalRef` | string \| null | yes | | | `shipToAddress` | string \| null | yes | | | `isEightyOneCompany` | boolean | yes | Shipments arrive in their EightyOne stock | ### `shipments.list`: List shipments Shipments to customers, newest first: draft (being prepared), shipped or cancelled. **REST:** `GET /api/v1/shipments` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `shipments_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft` \| `shipped` \| `cancelled` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `code` | string | yes | | | `status` | string | yes | draft / shipped / cancelled | | `customer` | object \| null | yes | | | `shipToAddress` | string \| null | yes | | | `trackingReference` | string \| null | yes | | | `notes` | string \| null | yes | | | `shippedAt` | string (date-time) \| null | yes | | | `partnerDeliveryState` | string \| null | yes | For a customer that is an EightyOne company | | `lines` | object[] | yes | | ### `shipments.get`: Get a shipment A shipment with its lines. **REST:** `GET /api/v1/shipments/:id` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `shipments_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Shipment external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `code` | string | yes | | | `status` | string | yes | draft / shipped / cancelled | | `customer` | object \| null | yes | | | `shipToAddress` | string \| null | yes | | | `trackingReference` | string \| null | yes | | | `notes` | string \| null | yes | | | `shippedAt` | string (date-time) \| null | yes | | | `partnerDeliveryState` | string \| null | yes | For a customer that is an EightyOne company | | `lines` | object[] | yes | | ### `shipments.create`: Prepare a shipment Draft a shipment to a customer: per line the SKU, quantity and the storage unit it ships from (and the lot for lot-tracked products). Nothing leaves stock until shipments_ship. Factories only. Needs 'Manage inventory'. **REST:** `POST /api/v1/shipments` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `shipments_create` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `customerId` | string | yes | Customer external id (customers_list) | | `lines` | object[] | yes | | | `shipToAddress` | string | | Defaults to the customer's | | `code` | string | | | | `trackingReference` | string | | | | `notes` | string | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `code` | string | yes | | | `status` | string | yes | draft / shipped / cancelled | | `customer` | object \| null | yes | | | `shipToAddress` | string \| null | yes | | | `trackingReference` | string \| null | yes | | | `notes` | string \| null | yes | | | `shippedAt` | string (date-time) \| null | yes | | | `partnerDeliveryState` | string \| null | yes | For a customer that is an EightyOne company | | `lines` | object[] | yes | | ### `shipments.ship`: Ship it Confirm a draft shipment left: each line's goods come out of stock, written to the customer. For a customer that is an EightyOne company, the goods arrive in their transit. Refused if any line lacks the stock, names a lot that isn't available, or takes another customer's consigned stock. Needs 'Manage inventory'. Preview it first. **REST:** `POST /api/v1/shipments/:id/ship` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `shipments_ship` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Shipment external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `code` | string | yes | | | `status` | string | yes | draft / shipped / cancelled | | `customer` | object \| null | yes | | | `shipToAddress` | string \| null | yes | | | `trackingReference` | string \| null | yes | | | `notes` | string \| null | yes | | | `shippedAt` | string (date-time) \| null | yes | | | `partnerDeliveryState` | string \| null | yes | For a customer that is an EightyOne company | | `lines` | object[] | yes | | ### `shipments.cancel`: Cancel a draft shipment Cancel a shipment that hasn't shipped. Nothing moves: a draft holds no stock. Needs 'Manage inventory'. **REST:** `POST /api/v1/shipments/:id/cancel` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `shipments_cancel` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Shipment external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `code` | string | yes | | | `status` | string | yes | draft / shipped / cancelled | | `customer` | object \| null | yes | | | `shipToAddress` | string \| null | yes | | | `trackingReference` | string \| null | yes | | | `notes` | string \| null | yes | | | `shippedAt` | string (date-time) \| null | yes | | | `partnerDeliveryState` | string \| null | yes | For a customer that is an EightyOne company | | `lines` | object[] | yes | | ### `inventory_counts.list`: List cycle counts Cycle counts, newest first: which storage unit, status (draft, in_progress, completed = finalized and waiting to be reconciled, reconciled) and totals. inventory_counts_get shows the lines and discrepancies. **REST:** `GET /api/v1/inventory-counts` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `inventory_counts_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft` \| `in_progress` \| `completed` \| `reconciled` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | ### `inventory_counts.get`: Get a cycle count A cycle count with its lines: expected (on hand by the system), counted, the discrepancy once finalized, and whether stock moved while it was open (a reason to recount rather than adjust). **REST:** `GET /api/v1/inventory-counts/:id` · **Permission:** `inventory:view` · **Effect:** read · **MCP:** tool `inventory_counts_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `inventory_counts.create`: Start a cycle count Open a count of one storage unit. It snapshots what the system expects there, and when the company has an inventory task type it also creates the count task operators fill in from the app. Needs 'Manage inventory'. **REST:** `POST /api/v1/inventory-counts` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `inventory_counts_create` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `storageUnitId` | string | yes | Storage unit external id (storage_units_list) | | `title` | string | | The count task's title | | `dueDate` | string (date-time) \| string (date) | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `inventory_counts.record_lines`: Record counted quantities Add what someone counted to an open count, by SKU (and bin, lot or serial where it matters). Quantities add to what was already counted for that line, as in the dashboard. Then finalize it. Needs 'Manage inventory'. **REST:** `POST /api/v1/inventory-counts/:id/lines` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `inventory_counts_record_lines` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Count id | | `lines` | object[] | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `inventory_counts.finalize`: Finish a cycle count Close counting: each line's expected quantity becomes what's on hand now, discrepancies are computed, and anything on hand the count never saw becomes a line counted 0. Nothing is posted to stock until it's reconciled. Needs 'Manage inventory'. **REST:** `POST /api/v1/inventory-counts/:id/finalize` · **Permission:** `inventory:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `inventory_counts_finalize` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `inventory_counts.reconcile`: Post a cycle count to stock Decide each line of a finalized count and post it. accepted: stock becomes the counted quantity; adjusted: becomes adjustedQuantity; ignored (and any line not listed): stock stays as the system has it. Every write-off needs an adjustment reasonCodeId (reason_codes_list kind adjustment). Needs 'Manage inventory' and 'Adjust inventory'. Preview it first: it lists each addition, write-off and bin move. **REST:** `POST /api/v1/inventory-counts/:id/reconcile` · **Permission:** `inventory:manage` + `inventory:adjust` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `inventory_counts_reconcile` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `decisions` | object[] | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `storageUnit` | object \| null | yes | | | `status` | string | yes | draft / in_progress / completed (finalized, awaiting reconciliation) / reconciled | | `countType` | string \| null | yes | | | `totalExpected` | number \| null | yes | | | `totalCounted` | number \| null | yes | | | `taskId` | string \| null | yes | The count task operators fill in, if any | | `dueDate` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `reconciledAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ## Production ### `work_orders.list`: List work orders The company's work orders, newest first, optionally by status. Use for ERP pull-sync or to see what production is working on. **REST:** `GET /api/v1/work-orders` · **Permission:** `production:view` · **Effect:** read · **MCP:** tool `work_orders_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `draft` \| `released` \| `in_progress` \| `completed` \| `closed` \| `cancelled` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Work order external id | | `code` | string | yes | Human code, e.g. WO-1009 | | `externalSystemId` | string \| null | yes | Your (ERP) id for the order | | `source` | string | yes | dashboard or erp | | `status` | `draft` \| `released` \| `in_progress` \| `completed` \| `closed` \| `cancelled` | yes | | | `product` | object \| null | yes | | | `quantity` | number | yes | | | `uom` | string \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `routing` | object \| null | yes | | | `releasedAt` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `work_orders.get`: Get a work order One work order with its operations (the traveler's tasks) and their status. **REST:** `GET /api/v1/work-orders/:externalId` · **Permission:** `production:view` · **Effect:** read · **MCP:** tool `work_orders_get` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | Work order external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Work order external id | | `code` | string | yes | Human code, e.g. WO-1009 | | `externalSystemId` | string \| null | yes | Your (ERP) id for the order | | `source` | string | yes | dashboard or erp | | `status` | `draft` \| `released` \| `in_progress` \| `completed` \| `closed` \| `cancelled` | yes | | | `product` | object \| null | yes | | | `quantity` | number | yes | | | `uom` | string \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `routing` | object \| null | yes | | | `releasedAt` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `operations` | object[] | yes | | ### `work_orders.release`: Release a work order Release a draft work order to the floor: one operation task per routing step, each on the only machine of its work center when there is just one, with the type's parameters pinned and later steps waiting for gates. Needs 'Release work orders'. Preview it with dryRun first: it lists the operations it creates. **REST:** `POST /api/v1/work-orders/:externalId/release` · **Permission:** `work_orders:release` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `work_orders_release` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | Work order external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Work order external id | | `code` | string | yes | Human code, e.g. WO-1009 | | `externalSystemId` | string \| null | yes | Your (ERP) id for the order | | `source` | string | yes | dashboard or erp | | `status` | `draft` \| `released` \| `in_progress` \| `completed` \| `closed` \| `cancelled` | yes | | | `product` | object \| null | yes | | | `quantity` | number | yes | | | `uom` | string \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `routing` | object \| null | yes | | | `releasedAt` | string (date-time) \| null | yes | | | `completedAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `work_orders.upsert`: Create or update a work order (ERP orders in) Idempotent on externalSystemId: a new id creates a draft order; re-sending updates it while it is still a draft and is a no-op afterwards. Lands on the product's released routing when there is exactly one. Release it with work_orders.release. **REST:** `POST /api/v1/work-orders` · **Permission:** `production:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalSystemId` | string | yes | Your (ERP) id for the order: the upsert key | | `productId` | integer | | EightyOne product id (or give productSku) | | `productSku` | string | | Product SKU (case-insensitive) | | `quantity` | number | | | | `uom` | string | | | | `dueDate` | string (date-time) \| string (date) | | | | `code` | string | | Human code; defaults to the next WO-n | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `workOrder` | object | yes | | | `created` | boolean | yes | | ### `work_orders.batch_upsert`: Create or update many work orders Upsert up to 500 orders; each succeeds or fails on its own (results in input order). **REST:** `POST /api/v1/work-orders/batch` · **Permission:** `production:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `orders` | any[] | yes | Each item is a work_orders.upsert input; an invalid item fails on its own | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | object[] | yes | | ## Webhooks ### `webhooks.list`: List webhooks The company's webhook subscriptions. **REST:** `GET /api/v1/webhooks` · **Permission:** `api_keys:view` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | | `isActive` | boolean \| `true` \| `false` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `targetUrl` | string | yes | | | `eventTypes` | string[] | yes | | | `isActive` | boolean | yes | | | `failureCount` | integer | yes | | | `disabledAt` | string (date-time) \| null | yes | Set when repeated failures switched the webhook off | | `headers` | object \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `webhooks.event_types`: List webhook event types Every event a webhook can subscribe to, with what triggers it. **REST:** `GET /api/v1/webhooks/events` · **Permission:** `api_keys:view` · **Effect:** read · **MCP:** via `capabilities_call` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `eventTypes` | object[] | yes | | ### `webhooks.create`: Create a webhook Subscribe a URL to events. Deliveries are Standard Webhooks messages signed with EightyOne's Ed25519 key: verify them with the public keys at /api/v1/webhook-signing-keys. There is no secret to store. **REST:** `POST /api/v1/webhooks` · **Permission:** `webhooks:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `targetUrl` | string (uri) | yes | HTTPS endpoint; private and internal addresses are refused | | `eventTypes` | `task.status_changed` \| `task.created` \| `client.created` \| `work_order.released` \| `work_order.completed` \| `downtime.started` \| `downtime.ended` \| `lot.created` \| `stock.reorder_triggered` \| `handoff.received` \| `handoff.status_changed` \| `handoff.declined` \| `handoff.deleted` \| `partner_delivery.incoming` \| `partner_delivery.received` \| `partner_delivery.shortfall` \| `data_package.received`[] | yes | | | `headers` | object | | Extra headers sent with each delivery | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `targetUrl` | string | yes | | | `eventTypes` | string[] | yes | | | `isActive` | boolean | yes | | | `failureCount` | integer | yes | | | `disabledAt` | string (date-time) \| null | yes | Set when repeated failures switched the webhook off | | `headers` | object \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `verification` | object | yes | Verify each delivery's signature with EightyOne's public key | ### `webhooks.get`: Get a webhook One webhook subscription. **REST:** `GET /api/v1/webhooks/:id` · **Permission:** `api_keys:view` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `targetUrl` | string | yes | | | `eventTypes` | string[] | yes | | | `isActive` | boolean | yes | | | `failureCount` | integer | yes | | | `disabledAt` | string (date-time) \| null | yes | Set when repeated failures switched the webhook off | | `headers` | object \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `webhooks.update`: Update a webhook Change a webhook's name, URL, events, headers or active flag. **REST:** `PATCH /api/v1/webhooks/:id` · **Permission:** `webhooks:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | | | | `targetUrl` | string (uri) | | | | `eventTypes` | `task.status_changed` \| `task.created` \| `client.created` \| `work_order.released` \| `work_order.completed` \| `downtime.started` \| `downtime.ended` \| `lot.created` \| `stock.reorder_triggered` \| `handoff.received` \| `handoff.status_changed` \| `handoff.declined` \| `handoff.deleted` \| `partner_delivery.incoming` \| `partner_delivery.received` \| `partner_delivery.shortfall` \| `data_package.received`[] | | | | `isActive` | boolean | | | | `headers` | object | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `targetUrl` | string | yes | | | `eventTypes` | string[] | yes | | | `isActive` | boolean | yes | | | `failureCount` | integer | yes | | | `disabledAt` | string (date-time) \| null | yes | Set when repeated failures switched the webhook off | | `headers` | object \| null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ### `webhooks.delete`: Delete a webhook Remove a webhook subscription. **REST:** `DELETE /api/v1/webhooks/:id` · **Permission:** `webhooks:manage` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | ### `webhooks.delivery_logs`: Webhook delivery log Recent deliveries of one webhook with their status, response and timing. **REST:** `GET /api/v1/webhooks/:id/logs` · **Permission:** `api_keys:view` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `limit` | integer | | Page size (1–100, default 50) | | `offset` | integer | | Rows to skip | | `status` | `pending` \| `success` \| `failed` \| `retrying` | | | | `eventType` | `task.status_changed` \| `task.created` \| `client.created` \| `work_order.released` \| `work_order.completed` \| `downtime.started` \| `downtime.ended` \| `lot.created` \| `stock.reorder_triggered` \| `handoff.received` \| `handoff.status_changed` \| `handoff.declined` \| `handoff.deleted` \| `partner_delivery.incoming` \| `partner_delivery.received` \| `partner_delivery.shortfall` \| `data_package.received` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `eventType` | string | yes | | | `eventId` | string | yes | | | `payload` | any | yes | | | `status` | string | yes | | | `attemptCount` | integer | yes | | | `responseStatusCode` | integer \| null | yes | | | `responseBody` | string \| null | yes | | | `responseTimeMs` | integer \| null | yes | | | `errorMessage` | string \| null | yes | | | `nextRetryAt` | string (date-time) \| null | yes | | | `deliveredAt` | string (date-time) \| null | yes | | | `createdAt` | string (date-time) | yes | | ### `webhooks.test`: Send a test event Queue a test delivery to THIS webhook only: a signed message of type webhook.test, to check your endpoint and its signature verification. **REST:** `POST /api/v1/webhooks/:id/test` · **Permission:** `webhooks:manage` · **Effect:** write · **Idempotency-Key:** not accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | string | yes | | | `webhookId` | string | yes | | ### `webhooks.retry_delivery`: Retry a delivery Re-send one logged delivery of this webhook. **REST:** `POST /api/v1/webhooks/:id/logs/:logId/retry` · **Permission:** `webhooks:manage` · **Effect:** write · **Idempotency-Key:** not accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `logId` | integer | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | string | yes | | | `deliveryLogId` | integer | yes | | ## Partner network ### `handoffs.send`: Send a task to a partner company Hand one of your tasks to another EightyOne company (by its short name, or a partner subcontractor). The partner gets its own copy; your people come off the task. Goods can travel with it (items), and a data-request task carries the scopes it asks for. **REST:** `POST /api/v1/handoffs` · **Permission:** `handoffs:send` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `handoffs_send` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `taskId` | string | yes | Your task's id | | `recipientShortName` | string | | | | `subContractorId` | integer | | A subcontractor that is an EightyOne company | | `items` | object[] | | | | `dataRequest` | object | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string | yes | This company's own task (never the partner's) | | `status` | string | yes | | | `declinedReason` | string \| null | yes | | | `requestedItems` | object[] | yes | | | `dataRequest` | object \| null | yes | | | `deliveries` | object[] | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | ### `handoffs.list`: List handoffs Tasks you sent to, or received from, partner companies (newest first, up to 500). **REST:** `GET /api/v1/handoffs` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `direction` | `incoming` \| `outgoing` \| `all` | | | | `state` | `active` \| `withdrawn` \| `deleted` | | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string | yes | This company's own task (never the partner's) | | `status` | string | yes | | | `declinedReason` | string \| null | yes | | | `requestedItems` | object[] | yes | | | `dataRequest` | object \| null | yes | | | `deliveries` | object[] | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | ### `handoffs.get`: Get a handoff One handoff from your side. **REST:** `GET /api/v1/handoffs/:id` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string | yes | This company's own task (never the partner's) | | `status` | string | yes | | | `declinedReason` | string \| null | yes | | | `requestedItems` | object[] | yes | | | `dataRequest` | object \| null | yes | | | `deliveries` | object[] | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | ### `handoffs.timeline`: Handoff timeline What happened on a handoff, in order; your own people are named, the partner's are not. **REST:** `GET /api/v1/handoffs/:id/timeline` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string | yes | | | `at` | string (date-time) | yes | | | `by` | `you` \| `partner` | yes | | | `actorName` | string \| null | yes | Only your own people are named | | `payload` | any | yes | | ### `handoffs.decline`: Decline a received handoff Decline a task a partner sent you, with a reason they will see. **REST:** `POST /api/v1/handoffs/:id/decline` · **Permission:** `handoffs:send` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `handoffs_decline` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `reason` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string | yes | This company's own task (never the partner's) | | `status` | string | yes | | | `declinedReason` | string \| null | yes | | | `requestedItems` | object[] | yes | | | `dataRequest` | object \| null | yes | | | `deliveries` | object[] | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | ### `handoffs.withdraw`: Withdraw a sent handoff Take back an active handoff you sent; the partner's copy is removed. **REST:** `DELETE /api/v1/handoffs/:id` · **Permission:** `handoffs:send` · **Effect:** destructive · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | ### `partner_deliveries.list`: List partner deliveries Goods sent to or received from partner companies, from your side. **REST:** `GET /api/v1/partner-deliveries` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `direction` | `incoming` \| `outgoing` \| `all` | | | | `state` | `in_transit` \| `partially_received` \| `shortfall` \| `received` \| `reversed` \| `refused` \| `unmapped` \| `held` \| `settled` | | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `kind` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string \| null | yes | | | `handoffId` | string \| null | yes | | | `shipmentCode` | string \| null | yes | | | `returnOf` | string \| null | yes | | | `receivingStorageUnit` | object \| null | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `partner_deliveries.get`: Get a partner delivery One delivery from your side. **REST:** `GET /api/v1/partner-deliveries/:id` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `kind` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string \| null | yes | | | `handoffId` | string \| null | yes | | | `shipmentCode` | string \| null | yes | | | `returnOf` | string \| null | yes | | | `receivingStorageUnit` | object \| null | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `partner_deliveries.receive`: Receive a partner delivery Book goods a partner sent you into stock, line by line (damaged quantities go to the bads bin), optionally reporting the rest missing. **REST:** `POST /api/v1/partner-deliveries/:id/receive` · **Permission:** `handoffs:send` + `inventory:receive` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `partner_deliveries_receive` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `lines` | object[] | | | | `reportRestMissing` | boolean | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `kind` | string | yes | | | `direction` | `incoming` \| `outgoing` | yes | | | `state` | string | yes | | | `partner` | object | yes | | | `taskId` | string \| null | yes | | | `handoffId` | string \| null | yes | | | `shipmentCode` | string \| null | yes | | | `returnOf` | string \| null | yes | | | `receivingStorageUnit` | object \| null | yes | | | `linkActive` | boolean | yes | | | `createdAt` | string (date-time) | yes | | | `lines` | object[] | yes | | ### `data_packages.list`: List data packages Traceability data packages you released to, or received from, partners. **REST:** `GET /api/v1/data-packages` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `role` | `released` \| `received` | | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `role` | string | yes | | | `createdAt` | string (date-time) | yes | | | `from` | any | yes | | | `object` | any | yes | | ### `data_packages.get`: Get a data package A data package's content, with short-lived links to its photos. **REST:** `GET /api/v1/data-packages/:id` · **Permission:** `handoffs:send` · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `role` | string | yes | | | `createdAt` | string (date-time) | yes | | | `content` | any | yes | | | `files` | object[] | yes | | ## Analytics ### `analytics.overview`: Factory performance overview Headline numbers for a window: total downtime, failures, average availability and OEE, and how many machines are monitored. `unavailableMetrics` names OEE factors with no source data yet. Start here, then drill into analytics.downtime / analytics.reliability. **REST:** `GET /api/v1/analytics/overview` · **Permission:** `insights:view` · **Effect:** read · **MCP:** tool `analytics_overview` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `range` | object | yes | | | `downtimeSeconds` | number | yes | | | `downtimeHours` | number | yes | | | `failures` | integer | yes | | | `avgAvailability` | number \| null | yes | 0–1 | | `avgOee` | number \| null | yes | 0–1 | | `monitoredSsus` | integer | yes | | | `unavailableMetrics` | string[] | yes | | ### `analytics.downtime`: Downtime by machine Time each machine (SSU) spent DOWN or in MAINTENANCE in the window, most first, with how many times it entered that state. For the reasons, follow up with downtime_events.list (reason-coded stoppages) or storylines.ssu (the machine's story). **REST:** `GET /api/v1/analytics/downtime` · **Permission:** `insights:view` · **Effect:** read · **MCP:** tool `analytics_downtime` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string \| null | yes | SSU external id | | `ssuName` | string \| null | yes | | | `category` | string | yes | DOWN or MAINTENANCE | | `seconds` | number | yes | | | `hours` | number | yes | | | `transitions` | integer | yes | Times the machine entered this state | ### `analytics.reliability`: Machine reliability (MTTR / MTBF) Per machine: failures (entries into DOWN), mean time to repair and mean time between failures, in seconds, most failures first. **REST:** `GET /api/v1/analytics/reliability` · **Permission:** `insights:view` · **Effect:** read · **MCP:** tool `analytics_reliability` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string \| null | yes | SSU external id | | `ssuName` | string \| null | yes | | | `failures` | integer | yes | | | `mttrSeconds` | number \| null | yes | | | `mtbfSeconds` | number \| null | yes | | ### `analytics.oee`: OEE by machine Overall equipment effectiveness per machine: availability, performance and quality (0–1) with good / scrap quantities. A factor is null when its source data is missing (no routed work, no counts). **REST:** `GET /api/v1/analytics/oee` · **Permission:** `insights:view` · **Effect:** read · **MCP:** tool `analytics_oee` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string \| null | yes | SSU external id | | `ssuName` | string \| null | yes | | | `plannedSeconds` | number | yes | | | `downtimeSeconds` | number | yes | | | `availability` | number \| null | yes | | | `performance` | number \| null | yes | Can exceed 1 when the line beats its standards | | `quality` | number \| null | yes | | | `goodQuantity` | number | yes | | | `scrapQuantity` | number | yes | | | `oee` | number \| null | yes | | ### `analytics.failure_pareto`: Failure modes (Pareto) What breaks most: completed failure-coded tasks grouped by problem code, most frequent first (top 25), with the machine that suffers each mode most. **REST:** `GET /api/v1/analytics/failure-pareto` · **Permission:** `insights:view` · **Effect:** read · **MCP:** tool `analytics_failure_pareto` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `problemCodeId` | integer | yes | | | `code` | string \| null | yes | | | `label` | string | yes | | | `count` | integer | yes | | | `worstSsuName` | string \| null | yes | | ## Machines ### `downtime_events.list`: List machine stoppages Individual downtime events (newest first, up to 500): when each machine stopped, for how long, and the reason code someone classified it with. Use it to explain the totals from analytics.downtime. Filter by machine and window; `unclassifiedOnly` lists stops still waiting for a reason. **REST:** `GET /api/v1/downtime-events` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `downtime_events_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string | | SSU external id | | `from` | string (date-time) \| string (date) | | Stops that started at or after this time | | `to` | string (date-time) \| string (date) | | Stops that started before this time | | `unclassifiedOnly` | boolean \| `true` \| `false` | | | | `openOnly` | boolean \| `true` \| `false` | | Only machines still stopped | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `startedAt` | string (date-time) | yes | | | `endedAt` | string (date-time) \| null | yes | null while the machine is still stopped | | `durationSeconds` | number \| null | yes | | | `source` | `auto` \| `manual` | yes | auto: detected from machine telemetry; manual: reported by a person | | `reason` | object \| null | yes | The downtime reason code, once someone classified the stop | | `note` | string \| null | yes | | ### `downtime_events.classify`: Give a stop its reason Classify a machine stoppage with a downtime reason code (reason_codes_list, kind downtime), optionally with a note. Re-classifying replaces the reason. Find stops waiting for one with downtime_events_list and unclassifiedOnly. **REST:** `POST /api/v1/downtime-events/:id/classify` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `downtime_events_classify` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Downtime event id | | `reasonCodeId` | integer | yes | A downtime reason code id | | `note` | string | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `startedAt` | string (date-time) | yes | | | `endedAt` | string (date-time) \| null | yes | null while the machine is still stopped | | `durationSeconds` | number \| null | yes | | | `source` | `auto` \| `manual` | yes | auto: detected from machine telemetry; manual: reported by a person | | `reason` | object \| null | yes | The downtime reason code, once someone classified the stop | | `note` | string \| null | yes | | ### `downtime_events.open`: Report a machine stopped Record that a machine stopped now, for machines without telemetry (those report stops themselves). Close it with downtime_events_close when it runs again, and classify it with a reason. Needs 'Manage machine data'. **REST:** `POST /api/v1/downtime-events` · **Permission:** `machine_data:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `downtime_events_open` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string | yes | SSU external id | | `note` | string | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `startedAt` | string (date-time) | yes | | | `endedAt` | string (date-time) \| null | yes | null while the machine is still stopped | | `durationSeconds` | number \| null | yes | | | `source` | `auto` \| `manual` | yes | auto: detected from machine telemetry; manual: reported by a person | | `reason` | object \| null | yes | The downtime reason code, once someone classified the stop | | `note` | string \| null | yes | | ### `downtime_events.close`: Report a machine running again End a stoppage now. Closing one that already ended changes nothing. Needs 'Manage machine data'. **REST:** `POST /api/v1/downtime-events/:id/close` · **Permission:** `machine_data:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `downtime_events_close` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Downtime event id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `startedAt` | string (date-time) | yes | | | `endedAt` | string (date-time) \| null | yes | null while the machine is still stopped | | `durationSeconds` | number \| null | yes | | | `source` | `auto` \| `manual` | yes | auto: detected from machine telemetry; manual: reported by a person | | `reason` | object \| null | yes | The downtime reason code, once someone classified the stop | | `note` | string \| null | yes | | ### `downtime_events.summary`: Downtime by machine and reason Recorded stoppages per machine in a window (default: the last 30 days), most stopped time first, each broken down by reason code: the direct answer to "which machines were down most, and why?". Counts only the part of each stop inside the window; a stop still open counts up to the window end. Complements analytics.downtime, which measures time in a DOWN / MAINTENANCE status. **REST:** `GET /api/v1/downtime-events/summary` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `downtime_events_summary` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date-time) \| string (date) | | Window start (ISO 8601). Default: 30 days before `to` | | `to` | string (date-time) \| string (date) | | Window end, exclusive (ISO 8601). Default: now | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `seconds` | number | yes | | | `hours` | number | yes | | | `stops` | integer | yes | | | `openStops` | integer | yes | Stops not yet ended | | `byReason` | object[] | yes | | ### `reason_codes.list`: List reason codes The company's reason codes: why machines stop (downtime), failure problem / cause / remedy codes on maintenance tasks, scrap and stock-adjustment reasons. **REST:** `GET /api/v1/reason-codes` · **Permission:** any member of the company · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `downtime` \| `failure_problem` \| `failure_cause` \| `failure_remedy` \| `scrap` \| `adjustment` | | | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `kind` | string | yes | | | `code` | string \| null | yes | | | `label` | string | yes | | | `parentId` | integer \| null | yes | | | `isActive` | boolean | yes | | ### `meters.list`: List machine meters Usage meters on machines (run hours, strokes, cycles…) and their current readings. Usage-based maintenance rules open a task each time a meter advances by their interval. Filter by machine. **REST:** `GET /api/v1/meters` · **Permission:** `maintenance:view` · **Effect:** read · **MCP:** tool `meters_list` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `ssuId` | string | | SSU external id | **Output:** `{ data, pagination }`, each item: | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `key` | string | yes | | | `name` | string | yes | | | `uom` | string \| null | yes | | | `source` | `manual` \| `signal` | yes | signal: counted from machine data; manual: people enter readings | | `currentValue` | number | yes | | ### `meters.record_reading`: Record a meter reading Enter a meter's current total (not the change), as a technician reads it off the machine. It can open usage-based maintenance tasks: preview it with dryRun first to see which. Needs 'Manage maintenance'. **REST:** `POST /api/v1/meters/:id/readings` · **Permission:** `maintenance:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `meters_record_reading` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | Meter id (meters_list) | | `value` | number | yes | The meter's reading: its running total | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `ssuId` | string | yes | SSU external id | | `ssuName` | string \| null | yes | | | `key` | string | yes | | | `name` | string | yes | | | `uom` | string \| null | yes | | | `source` | `manual` \| `signal` | yes | signal: counted from machine data; manual: people enter readings | | `currentValue` | number | yes | | ### `machine_catalog.search_makes`: Search machine makes Search the global catalog of machine manufacturers (e.g. 'Trumpf'). Use the existing spelling when it matches: everyone shares one catalog. **REST:** `GET /api/v1/machine-catalog/makes` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `machine_catalog_search_makes` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `q` | string | | Part of the make's name; empty lists common makes | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `name` | string | yes | | | `matchType` | `exact` \| `prefix` \| `contains` \| `similar` \| null | yes | similar = a 'did you mean' suggestion for a likely misspelling | ### `machine_catalog.search_models`: Search machine models Search the models of one make in the global catalog. **REST:** `GET /api/v1/machine-catalog/models` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `machine_catalog_search_models` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `makeId` | integer | yes | Make id from machine_catalog.search_makes | | `q` | string | | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | yes | | | `name` | string | yes | | | `matchType` | `exact` \| `prefix` \| `contains` \| `similar` \| null | yes | similar = a 'did you mean' suggestion for a likely misspelling | ### `machine_catalog.lookup_sku`: Find a machine by SKU The make and model registered for a model-level SKU / part number (not a serial number). **REST:** `GET /api/v1/machine-catalog/sku-lookup` · **Permission:** any member of the company · **Effect:** read · **MCP:** via `capabilities_call` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `sku` | string | | | **Output:** an array of | Field | Type | Required | Description | | --- | --- | --- | --- | | `make` | object | yes | | | `model` | object | yes | | ## Quality ### `ncrs.open`: Open an NCR Open a nonconformance report: a task of the company's NCR task type (Settings), linked to the task it was found on and carrying its machine and work order, with the affected lot or product recorded on it. A failed in-line check opens one by itself; use this for what people find. Disposition it with ncrs_disposition. **REST:** `POST /api/v1/ncrs` · **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** preview (checks the change and lists its effects, without making it) · **MCP:** tool `ncrs_open` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | What's wrong, in a line | | `details` | string | | What was found: measurements, quantities, how it was noticed | | `foundOn` | string | | External id of the task it was found on (an operation or inspection): the NCR takes its machine and work order | | `ssuId` | string | | SSU external id, when not taken from foundOn | | `workOrderId` | string | | Work order external id, when not taken from foundOn | | `lotCode` | string | | The affected lot | | `productSku` | string | | The affected product | | `priorityId` | integer | | | | `assigneeIds` | integer[] | | Personnel ids | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Task external id | | `title` | string | yes | | | `details` | string \| null | yes | | | `status` | string | yes | | | `startDate` | string (date-time) \| null | yes | | | `endDate` | string (date-time) \| null | yes | | | `dueDate` | string (date-time) \| null | yes | | | `priorityId` | integer | yes | | | `typeId` | string \| null | yes | Task type external id | | `team` | object \| null | yes | The team whose members see and take this task: its task type's team (change it on the type) | | `ssuId` | string \| null | yes | SSU external id | | `externalSystemId` | string \| null | yes | Your id for the task | | `incompleteReason` | string \| null | yes | | | `declinedReason` | string \| null | yes | | | `isPartialCompletion` | boolean | yes | Completed in part; a follow-up task carries the rest | | `handoffRole` | `proxy` \| `mirror` \| null | yes | proxy: you sent it to a partner; mirror: a partner sent it to you | | `problemCodeId` | integer \| null | yes | Failure coding: what was wrong | | `causeCodeId` | integer \| null | yes | Failure coding: why | | `remedyCodeId` | integer \| null | yes | Failure coding: what fixed it | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | | `foundOn` | string \| null | yes | | | `lotCode` | string \| null | yes | | | `productSku` | string \| null | yes | | ### `ncrs.disposition`: Disposition an NCR Decide what happens to nonconforming material, once per NCR. use_as_is: recorded only. rework: creates a rework task (of reworkTaskTypeId, or the NCR's own type). scrap: moves `quantity` of the lot to the bads bin and blocks the lot. return: blocks the lot for return to the vendor. Needs 'Disposition NCRs'. **REST:** `POST /api/v1/ncrs/:id/disposition` · **Permission:** `ncr:disposition` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** supported · **MCP:** tool `ncrs_disposition` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | The NCR's task external id | | `disposition` | `use_as_is` \| `rework` \| `scrap` \| `return` | yes | | | `lotCode` | string | | The affected lot (scrap and return act on it) | | `quantity` | number | | For scrap: how much of the lot | | `reworkTaskTypeId` | string | | For rework: the task type's external id | | `notes` | string | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `disposition` | `use_as_is` \| `rework` \| `scrap` \| `return` | yes | | | `lot` | object \| null | yes | | | `reworkTaskId` | string \| null | yes | The rework task's external id | ## Storylines ### `storylines.ssu`: A machine's story What happened on one machine (SSU) in a window (default: the last 7 days), as a chronological narrative: orders run, stops and their reasons, maintenance done, scrap and NCRs, meter readings, plus headline numbers. The best way to explain a machine's downtime. **REST:** `GET /api/v1/storylines/ssus/:id` · **Permission:** `maintenance:view` · **Effect:** read · **MCP:** tool `storylines_ssu` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | SSU external id | | `from` | string (date-time) \| string (date) | | | | `to` | string (date-time) \| string (date) | | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | object | yes | | | `plotPoints` | object | yes | The story's headline numbers | | `chapters` | object[] | yes | | ### `storylines.work_order`: A work order's story The life of one work order as a narrative: release, each operation started and finished, what was consumed and produced, scrap, quality checks and NCRs, shipment; with lead time and yield. **REST:** `GET /api/v1/storylines/work-orders/:id` · **Permission:** `production:view` · **Effect:** read · **MCP:** tool `storylines_work_order` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | Work order external id | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `subject` | object | yes | | | `plotPoints` | object | yes | The story's headline numbers | | `chapters` | object[] | yes | | ## Users ### `invitations.revoke`: Revoke an invitation Withdraw a pending invitation: it is revoked on WorkOS (the emailed link stops working) and the invitee is removed from the company, so the same address can be invited again. **Permission:** `users:manage` · **Effect:** destructive · **Idempotency-Key:** not accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `userId` | string (uuid) | yes | The invited user's id (from the company's invitee list) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | yes | | ### `invitations.resend`: Resend an invitation Email the invitee a new invitation with a fresh expiry, for one that expired or got lost. The previous invitation is revoked if it is still pending; the invitee keeps their role. **Permission:** `users:manage` · **Effect:** write · **Idempotency-Key:** not accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `userId` | string (uuid) | yes | The invited user's id (from the company's invitee list) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | yes | | | `expiresAt` | string | yes | | ## Issues ### `issues.list`: List reportable issues The issues a member can report (e.g. Breakdown, Safety hazard). Each one is a form that files a task of its configured type on the chosen SSU. **Permission:** any member of the company · **Effect:** read · **MCP:** no **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `issues` | object[] | yes | | ### `issues.get_form`: Get an issue's report form The fields of an issue's report form, in order, as configured in the form builder: the same form the issue's QR code opens. **Permission:** any member of the company · **Effect:** read · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `issueId` | string | yes | The issue's id (from issues.list) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `items` | object[] | yes | | ### `issues.upload_url`: Get an upload URL for an issue photo A short-lived URL to upload one photo for an issue report, stored where the QR flow stores its photos. PUT the file to it, then submit the URL without its query string as the answer. **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** not accepted · **Dry run:** not supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `issueId` | string | yes | The issue's id (from issues.list) | | `fileName` | string | yes | | | `contentType` | string | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `uploadUrl` | string | yes | | ### `issues.report`: Report an issue File an issue report on an SSU, as its QR code would: creates a task of the issue's type with the answers attached, and applies any SSU status the form's machine-status fields set. **Permission:** any member of the company · **Effect:** write · **Idempotency-Key:** not accepted · **Dry run:** supported · **MCP:** no **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `issueId` | string | yes | The issue's id (from issues.list) | | `ssuId` | string | yes | The SSU's id | | `answers` | object[] | yes | | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `taskId` | string | yes | | ## Commissioning ### `config.export`: Export the factory setup The company's setup as a factory spec: settings, SSU statuses, reason codes, work centers, sites with their storage units and SSU trees, warehouses, product categories, vendors, products by SKU with their vendors, recipe and stock policies, parameter sets, task types with their forms, SLAs and gates, routings, per-machine signals, meters, condition rules and spare parts, flows, issue reports, inspection plans, and people (permission groups, roles, teams, skills, existing people by email, subcontractors). Everything is referenced by name, so the spec can be applied to another company as a template. Start here before changing setup: edit the export, then config.plan and config.apply. **REST:** `GET /api/v1/config/export` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `config_export` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `version` | number | yes | | | `settings` | object | | | | `ssuStatuses` | object[] | | | | `reasonCodes` | object[] | | | | `workCenters` | object[] | | | | `sites` | object[] | | | | `warehouses` | object[] | | | | `productCategories` | object[] | | | | `vendors` | object[] | | | | `products` | object[] | | Products by SKU, with their vendors, recipe and stock policies. No prices or costs | | `parameterSets` | object[] | | | | `taskTypes` | object[] | | Kinds of work, each with its form, SLAs and gates. People (default assignee, skills) come later | | `routings` | object[] | | Step sequences that work orders follow | | `machineData` | object[] | | Per machine: signals, meters, condition rules and spare parts | | `flows` | object[] | | Automations: a task's status change creates the next task | | `issues` | object[] | | Issue reports people can file (from the app or a QR code) | | `permissionGroups` | object[] | | | | `roles` | object[] | | What people are; each role grants one permission group | | `teams` | object[] | | | | `skills` | object[] | | | | `people` | object[] | | Users already in the company, by email: their roles, teams, skills. Specs never invite | | `subcontractors` | object[] | | | | `inspectionPlans` | object[] | | Which receipts or products must be inspected, and how | | `guidance` | object[] | | What the company tells every AI agent: company-wide and on specific machines, work centers, products, task types | ### `config.validate`: Validate a factory spec Checks a spec beyond its schema: duplicate names or SKUs, references to statuses, work centers, reason codes, storage, categories, vendors, SKUs, task types, parameter sets or permission groups that don't exist, ambiguous names, a product both bought and made, gates naming fields the form doesn't have. Doesn't change anything. **REST:** `POST /api/v1/config/validate` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `config_validate` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `spec` | object | yes | The factory spec (export one with config.export to see the shape) | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `valid` | boolean | yes | | | `problems` | object[] | yes | | ### `config.plan`: Preview applying a factory spec What config.apply would do, change by change: create, update (with each field's current and new value), unchanged, and extra (in the company but not in the spec). Doesn't change anything. Show this plan to the user before applying. **REST:** `POST /api/v1/config/plan` · **Permission:** any member of the company · **Effect:** read · **MCP:** tool `config_plan` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `spec` | object | yes | The factory spec (export one with config.export to see the shape) | | `prune` | boolean \| `true` \| `false` | | Also remove what the spec leaves out of the sections it includes. Records with history (sites, SSUs, reason codes, products) are deactivated, not deleted; vendors, categories, task types and routings are removed only when nothing uses them; parameter sets are never removed. REST only | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `valid` | boolean | yes | | | `problems` | object[] | yes | | | `summary` | object | yes | | | `changes` | object[] | yes | | ### `config.apply`: Apply a factory spec Converges the company's setup to the spec, through the same rules as the dashboard; every change is audited. Safe to re-run: unchanged items are skipped. Not one transaction: if a change fails, the ones before it stay and the result says where it stopped; fix the cause and apply again. Always run config.plan first and get the user's go-ahead. **REST:** `POST /api/v1/config/apply` · **Permission:** `settings:manage` or `task_types:manage` or `clients:manage` or `inventory:manage` or `products:manage` or `production:manage` or `mobile_builder:manage` or `machine_data:manage` or `maintenance:manage` or `flows:manage` or `quality:manage` or `permissions:manage` or `personnel:manage` or `subcontractors:manage` or `ai_guidance:manage` · **Effect:** write · **Idempotency-Key:** accepted · **Dry run:** not supported · **MCP:** tool `config_apply` **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `spec` | object | yes | The factory spec (export one with config.export to see the shape) | | `prune` | boolean \| `true` \| `false` | | Also remove what the spec leaves out of the sections it includes. Records with history (sites, SSUs, reason codes, products) are deactivated, not deleted; vendors, categories, task types and routings are removed only when nothing uses them; parameter sets are never removed. REST only | **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `complete` | boolean | yes | | | `applied` | object[] | yes | | | `failed` | object | | | | `skipped` | integer | yes | Changes not attempted after a failure | --- # EightyOne webhook events Generated from the event catalog (`src/webhooks/events/catalog.ts`, `pnpm docs:api`): don't edit by hand. Every delivery is a [Standard Webhooks](https://www.standardwebhooks.com/) message, `POST`ed as JSON: ```json { "type": "task.status_changed", "timestamp": "2026-10-19T08:00:00.000Z", "data": { ... } } ``` Ids in `data` are the API's own: external ids, SKUs, and the numeric ids the API itself uses. ## Verifying a delivery - `webhook-id`: The message id: the same for every retry of this event (de-duplicate on it) - `webhook-timestamp`: Unix seconds when this attempt was sent (reject old ones to stop replays) - `webhook-signature`: Space-separated "v1a," over ".."; verify with a key from /api/v1/webhook-signing-keys EightyOne signs with its Ed25519 key (scheme `v1a`); there is no shared secret. Fetch the public keys from `GET /api/v1/webhook-signing-keys` (no authentication; `whpk_` + base64 of the 32-byte key) and accept a delivery when any listed signature verifies with any listed key. During a key rotation two keys are listed and both sign. A delivery answered with anything but 2xx is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 24 hours, with the same `webhook-id`. ## Events ### `client.created` A client was created. ```json { "type": "object", "properties": { "client": { "type": "object", "properties": { "id": { "description": "Your id for the client (its external system id)", "type": [ "string", "null" ] }, "name": { "type": "string" }, "isActive": { "type": "boolean" }, "contactPersonName": { "type": [ "string", "null" ] }, "contactPersonEmail": { "type": [ "string", "null" ] }, "contactPersonPhone": { "type": [ "string", "null" ] }, "color": { "type": [ "string", "null" ] }, "createdAt": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" } }, "required": [ "id", "name", "isActive", "contactPersonName", "contactPersonEmail", "contactPersonPhone", "color", "createdAt" ], "additionalProperties": false } }, "required": [ "client" ], "additionalProperties": false } ``` ### `data_package.received` A partner released the data package you requested. ```json { "type": "object", "properties": { "dataPackage": { "type": "object", "properties": { "id": { "type": "string" }, "dataRequestId": { "type": "string" }, "from": { "type": "object", "properties": { "name": { "type": "string" }, "shortName": { "type": "string" } }, "required": [ "name", "shortName" ], "additionalProperties": false } }, "required": [ "id", "dataRequestId", "from" ], "additionalProperties": false } }, "required": [ "dataPackage" ], "additionalProperties": false } ``` ### `downtime.ended` A machine stoppage ended. ```json { "type": "object", "properties": { "downtime": { "type": "object", "properties": { "id": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "Downtime event id, as downtime_events.list shows it" }, "ssu": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string", "description": "SSU external id" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "source": { "type": "string", "enum": [ "auto", "manual" ] }, "startedAt": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, "endedAt": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] }, "durationSeconds": { "type": [ "number", "null" ] } }, "required": [ "id", "ssu", "source", "startedAt", "endedAt", "durationSeconds" ], "additionalProperties": false } }, "required": [ "downtime" ], "additionalProperties": false } ``` ### `downtime.started` A machine stopped: detected from telemetry (auto) or reported by a person (manual). ```json { "type": "object", "properties": { "downtime": { "type": "object", "properties": { "id": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "Downtime event id, as downtime_events.list shows it" }, "ssu": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string", "description": "SSU external id" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "source": { "type": "string", "enum": [ "auto", "manual" ] }, "startedAt": { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, "endedAt": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] }, "durationSeconds": { "type": [ "number", "null" ] } }, "required": [ "id", "ssu", "source", "startedAt", "endedAt", "durationSeconds" ], "additionalProperties": false } }, "required": [ "downtime" ], "additionalProperties": false } ``` ### `handoff.declined` A handed-over task was declined (sent to both sides). ```json { "type": "object", "properties": { "handoff": { "type": "object", "properties": { "id": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" }, "status": { "type": "string" } }, "required": [ "id", "direction", "state", "status" ], "additionalProperties": {} } }, "required": [ "handoff" ], "additionalProperties": false } ``` ### `handoff.deleted` A handoff was withdrawn or deleted (sent to both sides). ```json { "type": "object", "properties": { "handoff": { "type": "object", "properties": { "id": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" }, "status": { "type": "string" } }, "required": [ "id", "direction", "state", "status" ], "additionalProperties": {} } }, "required": [ "handoff" ], "additionalProperties": false } ``` ### `handoff.received` A partner company sent you a task. ```json { "type": "object", "properties": { "handoff": { "type": "object", "properties": { "id": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" }, "status": { "type": "string" } }, "required": [ "id", "direction", "state", "status" ], "additionalProperties": {} } }, "required": [ "handoff" ], "additionalProperties": false } ``` ### `handoff.status_changed` A task handed between partner companies changed status (sent to both sides). ```json { "type": "object", "properties": { "handoff": { "type": "object", "properties": { "id": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" }, "status": { "type": "string" } }, "required": [ "id", "direction", "state", "status" ], "additionalProperties": {} } }, "required": [ "handoff" ], "additionalProperties": false } ``` ### `lot.created` A lot was created: received from a vendor, produced by a work order, or entered by hand. ```json { "type": "object", "properties": { "lot": { "type": "object", "properties": { "code": { "type": "string" }, "product": { "anyOf": [ { "type": "object", "properties": { "sku": { "type": [ "string", "null" ] }, "name": { "type": "string" } }, "required": [ "sku", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "status": { "type": "string" }, "workOrderId": { "description": "The work order that produced it", "type": [ "string", "null" ] }, "vendor": { "description": "The vendor it came from", "type": [ "string", "null" ] } }, "required": [ "code", "product", "status", "workOrderId", "vendor" ], "additionalProperties": false } }, "required": [ "lot" ], "additionalProperties": false } ``` ### `partner_delivery.incoming` A partner company sent you goods: they are in transit to you. ```json { "type": "object", "properties": { "delivery": { "type": "object", "properties": { "id": { "type": "string" }, "kind": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" } }, "required": [ "id", "kind", "direction", "state" ], "additionalProperties": {} } }, "required": [ "delivery" ], "additionalProperties": false } ``` ### `partner_delivery.received` A partner received goods you sent. ```json { "type": "object", "properties": { "delivery": { "type": "object", "properties": { "id": { "type": "string" }, "kind": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" } }, "required": [ "id", "kind", "direction", "state" ], "additionalProperties": {} } }, "required": [ "delivery" ], "additionalProperties": false } ``` ### `partner_delivery.shortfall` A partner reported goods you sent as missing or damaged. ```json { "type": "object", "properties": { "delivery": { "type": "object", "properties": { "id": { "type": "string" }, "kind": { "type": "string" }, "direction": { "type": "string" }, "state": { "type": "string" } }, "required": [ "id", "kind", "direction", "state" ], "additionalProperties": {} } }, "required": [ "delivery" ], "additionalProperties": false } ``` ### `stock.reorder_triggered` A product's stock fell to its reorder point (lead time × measured consumption). Sent once per crossing; it re-arms when stock recovers. ```json { "type": "object", "properties": { "product": { "anyOf": [ { "type": "object", "properties": { "sku": { "type": [ "string", "null" ] }, "name": { "type": "string" } }, "required": [ "sku", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "onHand": { "type": "number" }, "reorderPoint": { "type": "number" }, "dailyConsumption": { "type": "number" }, "leadTimeDays": { "type": "number" }, "suggestedQuantity": { "type": "number" }, "storageSiteId": { "description": "Storage site external id, when the policy is per site", "type": [ "string", "null" ] } }, "required": [ "product", "onHand", "reorderPoint", "dailyConsumption", "leadTimeDays", "suggestedQuantity", "storageSiteId" ], "additionalProperties": false } ``` ### `task.created` A task was created: by a person, the API, an agent, a flow, a schedule or a work order's release. ```json { "type": "object", "properties": { "task": { "type": "object", "properties": { "id": { "type": "string", "description": "Task external id" }, "title": { "type": "string" }, "status": { "type": "string" }, "priorityId": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Priority level, as the API uses it" }, "type": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "dueDate": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] }, "workOrderId": { "description": "Work order external id, for an operation", "type": [ "string", "null" ] } }, "required": [ "id", "title", "status", "priorityId", "type", "dueDate", "workOrderId" ], "additionalProperties": false }, "ssu": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string", "description": "SSU external id" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] } }, "required": [ "task", "ssu" ], "additionalProperties": false } ``` ### `task.status_changed` A task moved to another status (open, preparation, ready, dispatched, submitted, done, incomplete, declined). ```json { "type": "object", "properties": { "task": { "type": "object", "properties": { "id": { "type": "string", "description": "Task external id" }, "title": { "type": "string" }, "status": { "type": "string" }, "priorityId": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Priority level, as the API uses it" }, "type": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "dueDate": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] }, "workOrderId": { "description": "Work order external id, for an operation", "type": [ "string", "null" ] } }, "required": [ "id", "title", "status", "priorityId", "type", "dueDate", "workOrderId" ], "additionalProperties": false }, "ssu": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string", "description": "SSU external id" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "previousStatus": { "type": [ "string", "null" ] }, "status": { "type": "string" } }, "required": [ "task", "ssu", "previousStatus", "status" ], "additionalProperties": false } ``` ### `work_order.completed` A work order's last operation was done. ```json { "type": "object", "properties": { "workOrder": { "type": "object", "properties": { "id": { "type": "string", "description": "Work order external id" }, "code": { "type": [ "string", "null" ] }, "product": { "anyOf": [ { "type": "object", "properties": { "sku": { "type": [ "string", "null" ] }, "name": { "type": "string" } }, "required": [ "sku", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "quantity": { "type": "number" }, "completedAt": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] } }, "required": [ "id", "code", "product", "quantity", "completedAt" ], "additionalProperties": false } }, "required": [ "workOrder" ], "additionalProperties": false } ``` ### `work_order.released` A work order went to the floor: its operations were created. ```json { "type": "object", "properties": { "workOrder": { "type": "object", "properties": { "id": { "type": "string", "description": "Work order external id" }, "code": { "type": [ "string", "null" ] }, "product": { "anyOf": [ { "type": "object", "properties": { "sku": { "type": [ "string", "null" ] }, "name": { "type": "string" } }, "required": [ "sku", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "quantity": { "type": "number" }, "dueDate": { "anyOf": [ { "anyOf": [ { "type": "string", "format": "date-time" }, { "type": "string" } ], "description": "ISO 8601" }, { "type": "null" } ] } }, "required": [ "id", "code", "product", "quantity", "dueDate" ], "additionalProperties": false }, "operations": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "How many operation tasks the release created" } }, "required": [ "workOrder", "operations" ], "additionalProperties": false } ``` --- # Errors Generated from the problem catalog (`src/capabilities/problem-catalog.ts`): don't edit by hand. Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`application/problem+json`). Branch on `code`: it's stable. `detail` is written for a person and may change. `type` links to the code's page. ```json { "type": "https://docs.eightyone.app/api/problems/missing_permission", "title": "forbidden", "status": 403, "detail": "Needs inventory:adjust", "code": "missing_permission" } ``` | Code | Status | Meaning | | --- | --- | --- | | [`bad_request`](problems/bad_request.md) | 400 | The request was understood, but EightyOne refused it: a business rule says no. `detail` says which rule. | | [`invalid_input`](problems/invalid_input.md) | 400 | The request doesn't match the capability's input schema. | | [`unauthenticated`](problems/unauthenticated.md) | 401 | No valid credential: the API key or token is missing, expired or revoked. | | [`forbidden`](problems/forbidden.md) | 403 | The caller is known but may not do this in this company. | | [`missing_permission`](problems/missing_permission.md) | 403 | The user (or the key, narrowed to fewer permissions) lacks a permission this capability needs. `detail` names it. | | [`not_found`](problems/not_found.md) | 404 | Nothing with that id in this company, or nothing the caller may see. | | [`unknown_capability`](problems/unknown_capability.md) | 404 | `capabilities_call` named a capability that doesn't exist, isn't available over MCP, or the caller can't run. | | [`conflict`](problems/conflict.md) | 409 | The request clashes with the current state (for example, something already exists). | | [`idempotency_in_progress`](problems/idempotency_in_progress.md) | 409 | A request with the same `Idempotency-Key` is still running. | | [`idempotency_conflict`](problems/idempotency_conflict.md) | 409 | The `Idempotency-Key` was used for a different capability or caller. | | [`unprocessable`](problems/unprocessable.md) | 422 | The request is well-formed but can't be done as asked. | | [`idempotency_key_reused`](problems/idempotency_key_reused.md) | 422 | The `Idempotency-Key` was sent again with a different body. | | [`idempotency_unsupported`](problems/idempotency_unsupported.md) | 422 | This capability doesn't take an `Idempotency-Key`: reads are safe to repeat, and a few writes can't be replayed. | | [`dry_run_unsupported`](problems/dry_run_unsupported.md) | 422 | `Prefer: dry-run` (or `dryRun: true`) was asked of a capability that can't be previewed. | | [`rate_limited`](problems/rate_limited.md) | 429 | Too many requests from this caller. | | [`internal_error`](problems/internal_error.md) | 500 | Something failed on EightyOne's side. It's logged with the request's `X-EightyOne-Transaction-ID`. | --- # Permissions Generated from the permission vocabulary and the capability registry: don't edit by hand. A user's permissions are the union of their roles' permission groups (Settings → Permission groups). An API key acts as its owner, optionally narrowed to fewer permissions; an agent connected over MCP acts as the user who connected it. Capabilities marked *any member* need no permission beyond belonging to the company; some of them check more as they run (a task's type, its assignee). ## `tasks:view_all` See all tasks (not only own) ## `tasks:view_my_teams` See tasks for my teams ## `tasks:view_pre_active` See pre-active tasks on mobile ## `tasks:create` Create tasks ## `tasks:delete` Delete tasks Capabilities: `tasks.delete` ## `tasks:reassign` Reassign tasks ## `tasks:change_status_mobile` Change task status from mobile ## `task_types:view` View Task types & work centers ## `task_types:manage` Manage Task types & work centers Capabilities: `config.apply` ## `mobile_builder:view` View Mobile Builder ## `mobile_builder:manage` Manage Mobile Builder Capabilities: `config.apply` ## `flows:view` View Flows ## `flows:manage` Manage Flows Capabilities: `config.apply` ## `clients:view` View Clients, sites & SSUs ## `clients:manage` Manage Clients, sites & SSUs Capabilities: `clients.create`, `clients.delete`, `clients.update`, `config.apply`, `ssus.update` ## `geographic:view` View Geographic ## `geographic:manage` Manage Geographic ## `personnel:view` View Personnel & skills ## `personnel:manage` Manage Personnel & skills Capabilities: `config.apply` ## `subcontractors:view` View Subcontractors ## `subcontractors:manage` Manage Subcontractors Capabilities: `config.apply` ## `users:view` View Users & invitations ## `users:manage` Manage Users & invitations Capabilities: `invitations.resend`, `invitations.revoke` ## `access_requests:approve` Approve access requests ## `permissions:view` View Permission groups ## `permissions:manage` Manage Permission groups (Manage makes an administrator) Capabilities: `config.apply` ## `settings:view` View Company settings & integrations ## `settings:manage` Manage Company settings & integrations Capabilities: `config.apply`, `teams.create`, `teams.delete`, `teams.update` ## `api_keys:view` View API keys & webhooks Capabilities: `webhooks.delivery_logs`, `webhooks.event_types`, `webhooks.get`, `webhooks.list` ## `api_keys:manage` Manage API keys & webhooks ## `webhooks:manage` Manage webhooks Capabilities: `webhooks.create`, `webhooks.delete`, `webhooks.retry_delivery`, `webhooks.test`, `webhooks.update` ## `audit_logs:view` View audit logs ## `data:export` Export data ## `data:import` Import data (CSV) ## `products:view` View Products, vendors & purchasing Capabilities: `inventory.reorder_suggestions` ## `products:manage` Manage Products, vendors & purchasing Capabilities: `config.apply` ## `inventory:view` View Inventory Capabilities: `customers.list`, `inventory.stock_levels`, `inventory_counts.get`, `inventory_counts.list`, `shipments.get`, `shipments.list`, `stock_transactions.list`, `storage_units.get`, `storage_units.list` ## `inventory:manage` Manage Inventory Capabilities: `config.apply`, `inventory_counts.create`, `inventory_counts.finalize`, `inventory_counts.reconcile`, `inventory_counts.record_lines`, `shipments.cancel`, `shipments.create`, `shipments.ship`, `stock.transfer` ## `inventory:adjust` Adjust inventory (post counts) Capabilities: `inventory_counts.reconcile` ## `inventory:receive` Receive goods (POs, partner deliveries) Capabilities: `partner_deliveries.receive`, `purchase_orders.receive` ## `production:view` View Production Capabilities: `storylines.work_order`, `work_orders.get`, `work_orders.list` ## `production:manage` Manage Production Capabilities: `config.apply`, `work_orders.batch_upsert`, `work_orders.upsert` ## `work_orders:release` Release work orders Capabilities: `work_orders.release` ## `machine_data:view` View Machine data ## `machine_data:manage` Manage Machine data Capabilities: `config.apply`, `downtime_events.close`, `downtime_events.open` ## `maintenance:view` View Maintenance Capabilities: `meters.list`, `storylines.ssu` ## `maintenance:manage` Manage Maintenance Capabilities: `config.apply`, `meters.record_reading` ## `quality:view` View Quality ## `quality:manage` Manage Quality Capabilities: `config.apply` ## `ncr:disposition` Disposition NCRs Capabilities: `ncrs.disposition` ## `documents:view` View Documents ## `documents:manage` Manage Documents ## `documents:publish` Publish documents ## `insights:view` View insights Capabilities: `analytics.downtime`, `analytics.failure_pareto`, `analytics.oee`, `analytics.overview`, `analytics.reliability` ## `handoffs:send` Send tasks to partner companies Capabilities: `data_packages.get`, `data_packages.list`, `handoffs.decline`, `handoffs.get`, `handoffs.list`, `handoffs.send`, `handoffs.timeline`, `handoffs.withdraw`, `partner_deliveries.get`, `partner_deliveries.list`, `partner_deliveries.receive` ## `partner_data:request` Request data from partners ## `partner_data:release` Release data to partners ## `ai_guidance:manage` Edit AI guidance (what agents are told) Capabilities: `config.apply`, `guidance.delete`, `guidance.set` --- --- name: eightyone description: Work with EightyOne (factory and field-service operations) through its MCP server. Commission a factory from a template, model production lines, configure task types and forms, and run the floor day to day (release work, complete tasks with their forms, NCRs, stops, stock). Also answer questions about machines, downtime, maintenance, work orders, inventory and quality. Use it whenever the user mentions EightyOne, or asks to set up or analyze a factory that is connected to it. --- # EightyOne EightyOne runs factories and field-service companies. Everything that happens is a **task** of a **task type**. Machines, lines and assets are **SSUs** on **sites**. Work orders follow **routings**. Stock sits in **storage units**. Call `guide_get` with topic `glossary` for the full vocabulary before you draw conclusions about an area you haven't read up on. ## Connecting - **MCP server:** `https:///mcp`. Sign in with OAuth, or send an EightyOne API key as a bearer token. - **One connection is one company.** If the user belongs to several, add `?company=` to the URL. - **Start with `me_get`.** It returns the company and the user's permissions. You can only do what that user can do; tools they can't use aren't listed. ## Rules 1. **Read before you change.** - `guide_get` explains each area (topics: glossary, downtime, maintenance, production, inventory, quality, commissioning, signals). - Topic `company` returns what this company's administrators told agents. Follow it unless it conflicts with these rules or the user's request. 2. **Setup changes go through a factory spec,** never one record at a time: 1. `config_export` to see the current setup; 2. edit the spec; 3. `config_validate`; 4. `config_plan`; 5. show the user the plan and wait for an explicit go-ahead; 6. `config_apply`. Never apply without the go-ahead. 3. **Day-to-day changes need a preview and an OK too.** This covers updating a task, recording counts and the like. 1. Say what you'll do. 2. Call the tool with `dryRun: true`. It answers whether the change would be accepted, what it changes, and what it sets off (stock posted, follow-up tasks, machine status, flows). 3. Show that to the user. 4. Make the real call only after their OK. For a batch, preview each change and confirm them together. You can't delete anything: tell the user where to do it. 4. **No prices or costs.** EightyOne holds none: don't ask for them or put them anywhere. 5. **People are never invented.** - A spec never invites anyone. `people` only covers users already in the company. - Companies are created by EightyOne staff only. If the user has no company yet, tell them to ask EightyOne. 6. **Report gaps as gaps.** Unclassified stops, missing counts and machines with no data are findings, not something to guess around. 7. **You can't remove things.** Deleting, and applying a spec with prune, are REST- and dashboard-only. If the user wants something removed, tell them where to do it. ## Day-to-day work Preview each of these first (rule 3): | Job | Tool | | --- | --- | | Release a draft work order to the floor | `work_orders_release`: the preview lists the operations, their machines and gates | | Assign, dispatch, start, complete or decline a task | `tasks_update` | | Mark a task incomplete and create a follow-up for the rest | `tasks_update` with `incompleteReason` and `createFollowUp` | | Record good and scrap counts during a run | `tasks_record_counts`: counts add up | | Record the final counts as the task completes | `tasks_update` with the status and `goodQuantity` / `scrapQuantity` | | Make a task wait for another | `tasks_connect` with type `dependency` | | Create a task, optionally waiting for another | `tasks_create` (with `after`) | | See a task's form; fill it in and complete the task | `tasks_get_form`; `tasks_submit_form` | | Give a machine stop its reason (only the one a person gives you) | `downtime_events_classify` | | Report a machine without telemetry stopped, or running again | `downtime_events_open`, `downtime_events_close` | | Enter a meter reading (its running total) | `meters_record_reading`: the preview says which PM tasks it opens | | Record what failed, why and what fixed it, on maintenance work | `tasks_update` with `problemCodeId`, `causeCodeId`, `remedyCodeId` | | Raise a nonconformance | `ncrs_open`, with `foundOn` the task it was found on | | Decide on an NCR: use as is, rework, scrap, return (once) | `ncrs_disposition` | | See what a storage unit holds; move stock | `storage_units_get`; `stock_transfer` | | Receive a delivery against a purchase order | `purchase_orders_receive` | | Ship to a customer | `shipments_create` (draft), then `shipments_ship` | | Hand a task to a partner company; decline one sent to you; receive a partner's goods | `handoffs_send`; `handoffs_decline`; `partner_deliveries_receive` | | Run a cycle count | `inventory_counts_create`, `inventory_counts_record_lines`, `inventory_counts_finalize`, then `inventory_counts_reconcile` with the user's decision per discrepancy | A work order completes by itself when its last operation is done. Photos, signatures and scans are given by people in the app: a task that requires one waits for them. ## Playbooks Read the one that matches the job before you start: | Job | Playbook | | --- | --- | | Set up a new factory, from a template or from scratch | [playbooks/commission-a-factory.md](playbooks/commission-a-factory.md) | | Add or change a production line: machines, operations, routing, machine data | [playbooks/model-a-production-line.md](playbooks/model-a-production-line.md) | | Create or change task types, their forms, SLAs, gates and flows | [playbooks/configure-task-types-and-forms.md](playbooks/configure-task-types-and-forms.md) | | Run the floor: release and dispatch work, complete tasks with their forms, quality, stops, stock | [playbooks/run-a-shift.md](playbooks/run-a-shift.md) | | Set up preventive maintenance: time, usage and condition triggers | [playbooks/set-up-preventive-maintenance.md](playbooks/set-up-preventive-maintenance.md) | | Find where production time is lost, and why | [playbooks/triage-downtime.md](playbooks/triage-downtime.md) | | Make the SSU Status page a live factory display (andon) | [playbooks/set-up-a-factory-display.md](playbooks/set-up-a-factory-display.md) | ## Templates - [templates/brazos-metalworks.json](templates/brazos-metalworks.json): Brazos Metalworks, a sheet-metal fabrication factory. - Structure: 9 areas, 39 machines, 49 products with recipes and vendors, 14 task types with forms, 2 routings, machine signals and maintenance rules, 5 roles. - It's a complete factory spec: copy it, rename what's specific, remove what doesn't apply, and plan it into the new company. ## Reference - [reference/tools.md](reference/tools.md): every MCP tool and what it does (generated). - **The spec format:** `config_export` from any company is the best example. Each tool's input schema documents every field. --- # EightyOne MCP tools Generated from the capability registry (`pnpm docs:api`): don't edit by hand. Every tool acts as the connected user, in one company, with that user's permissions (`me_get` shows them). Tools the user may not use aren't listed by the server. ## Tools - **`analytics_downtime`**: Time each machine (SSU) spent DOWN or in MAINTENANCE in the window, most first, with how many times it entered that state. For the reasons, follow up with downtime_events.list (reason-coded stoppages) or storylines.ssu (the machine's story). - **`analytics_failure_pareto`**: What breaks most: completed failure-coded tasks grouped by problem code, most frequent first (top 25), with the machine that suffers each mode most. - **`analytics_oee`**: Overall equipment effectiveness per machine: availability, performance and quality (0–1) with good / scrap quantities. A factor is null when its source data is missing (no routed work, no counts). - **`analytics_overview`**: Headline numbers for a window: total downtime, failures, average availability and OEE, and how many machines are monitored. `unavailableMetrics` names OEE factors with no source data yet. Start here, then drill into analytics.downtime / analytics.reliability. - **`analytics_reliability`**: Per machine: failures (entries into DOWN), mean time to repair and mean time between failures, in seconds, most failures first. - **`clients_get`**: One client by your id for it. - **`clients_list`**: The company's clients (the customers whose sites and SSUs work happens at), newest first. Filter by name or active flag. - **`config_apply`** (changes data): Converges the company's setup to the spec, through the same rules as the dashboard; every change is audited. Safe to re-run: unchanged items are skipped. Not one transaction: if a change fails, the ones before it stay and the result says where it stopped; fix the cause and apply again. Always run config.plan first and get the user's go-ahead. - **`config_export`**: The company's setup as a factory spec: settings, SSU statuses, reason codes, work centers, sites with their storage units and SSU trees, warehouses, product categories, vendors, products by SKU with their vendors, recipe and stock policies, parameter sets, task types with their forms, SLAs and gates, routings, per-machine signals, meters, condition rules and spare parts, flows, issue reports, inspection plans, and people (permission groups, roles, teams, skills, existing people by email, subcontractors). Everything is referenced by name, so the spec can be applied to another company as a template. Start here before changing setup: edit the export, then config.plan and config.apply. - **`config_plan`**: What config.apply would do, change by change: create, update (with each field's current and new value), unchanged, and extra (in the company but not in the spec). Doesn't change anything. Show this plan to the user before applying. - **`config_validate`**: Checks a spec beyond its schema: duplicate names or SKUs, references to statuses, work centers, reason codes, storage, categories, vendors, SKUs, task types, parameter sets or permission groups that don't exist, ambiguous names, a product both bought and made, gates naming fields the form doesn't have. Doesn't change anything. - **`customers_list`**: A factory's customers, who shipments go to: name, reference and ship-to address. - **`downtime_events_classify`** (changes data): Classify a machine stoppage with a downtime reason code (reason_codes_list, kind downtime), optionally with a note. Re-classifying replaces the reason. Find stops waiting for one with downtime_events_list and unclassifiedOnly. - **`downtime_events_close`** (changes data): End a stoppage now. Closing one that already ended changes nothing. Needs 'Manage machine data'. - **`downtime_events_list`**: Individual downtime events (newest first, up to 500): when each machine stopped, for how long, and the reason code someone classified it with. Use it to explain the totals from analytics.downtime. Filter by machine and window; `unclassifiedOnly` lists stops still waiting for a reason. - **`downtime_events_open`** (changes data): Record that a machine stopped now, for machines without telemetry (those report stops themselves). Close it with downtime_events_close when it runs again, and classify it with a reason. Needs 'Manage machine data'. - **`downtime_events_summary`**: Recorded stoppages per machine in a window (default: the last 30 days), most stopped time first, each broken down by reason code: the direct answer to "which machines were down most, and why?". Counts only the part of each stop inside the window; a stop still open counts up to the window end. Complements analytics.downtime, which measures time in a DOWN / MAINTENANCE status. - **`guide_get`**: EightyOne concepts and how to read the data: the glossary, downtime, maintenance, production, inventory, quality, commissioning, what deserves attention, and this company's own guidance (topic "company"). Without a topic, lists the topics. Read the glossary before working on an unfamiliar area. - **`handoffs_decline`** (changes data): Decline a task a partner sent you, with a reason they will see. - **`handoffs_send`** (changes data): Hand one of your tasks to another EightyOne company (by its short name, or a partner subcontractor). The partner gets its own copy; your people come off the task. Goods can travel with it (items), and a data-request task carries the scopes it asks for. - **`inventory_counts_create`** (changes data): Open a count of one storage unit. It snapshots what the system expects there, and when the company has an inventory task type it also creates the count task operators fill in from the app. Needs 'Manage inventory'. - **`inventory_counts_finalize`** (changes data): Close counting: each line's expected quantity becomes what's on hand now, discrepancies are computed, and anything on hand the count never saw becomes a line counted 0. Nothing is posted to stock until it's reconciled. Needs 'Manage inventory'. - **`inventory_counts_get`**: A cycle count with its lines: expected (on hand by the system), counted, the discrepancy once finalized, and whether stock moved while it was open (a reason to recount rather than adjust). - **`inventory_counts_list`**: Cycle counts, newest first: which storage unit, status (draft, in_progress, completed = finalized and waiting to be reconciled, reconciled) and totals. inventory_counts_get shows the lines and discrepancies. - **`inventory_counts_reconcile`** (changes data): Decide each line of a finalized count and post it. accepted: stock becomes the counted quantity; adjusted: becomes adjustedQuantity; ignored (and any line not listed): stock stays as the system has it. Every write-off needs an adjustment reasonCodeId (reason_codes_list kind adjustment). Needs 'Manage inventory' and 'Adjust inventory'. Preview it first: it lists each addition, write-off and bin move. - **`inventory_counts_record_lines`** (changes data): Add what someone counted to an open count, by SKU (and bin, lot or serial where it matters). Quantities add to what was already counted for that line, as in the dashboard. Then finalize it. Needs 'Manage inventory'. - **`inventory_reorder_suggestions`**: Products below their minimum or at their dynamic reorder point (lead time × measured consumption), with the suggested quantity and vendor. Quantities and days only. - **`inventory_stock_levels`**: Goods on hand per product and storage site (up to 2000 rows), by the same rule reordering uses: good stock only (not the bads bin), not counting stock held by a partner. Filter by product id, SKU or storage site. - **`line_items_find_ssu_by_barcode`**: Resolve a scanned line-item barcode to the SSU (machine / location) whose storage unit holds it. 404 when the unit, its storage unit or that storage unit's SSU is unknown. - **`machine_catalog_search_makes`**: Search the global catalog of machine manufacturers (e.g. 'Trumpf'). Use the existing spelling when it matches: everyone shares one catalog. - **`machine_catalog_search_models`**: Search the models of one make in the global catalog. - **`me_get`**: The caller: the user this credential acts as, the company it is bound to, how it is calling (REST or MCP; human, agent or service account) and the permissions it actually has here (the union of the user's roles' groups ∩ the credential's permissions). Call it first to know what you can do. - **`meters_list`**: Usage meters on machines (run hours, strokes, cycles…) and their current readings. Usage-based maintenance rules open a task each time a meter advances by their interval. Filter by machine. - **`meters_record_reading`** (changes data): Enter a meter's current total (not the change), as a technician reads it off the machine. It can open usage-based maintenance tasks: preview it with dryRun first to see which. Needs 'Manage maintenance'. - **`ncrs_disposition`** (changes data): Decide what happens to nonconforming material, once per NCR. use_as_is: recorded only. rework: creates a rework task (of reworkTaskTypeId, or the NCR's own type). scrap: moves `quantity` of the lot to the bads bin and blocks the lot. return: blocks the lot for return to the vendor. Needs 'Disposition NCRs'. - **`ncrs_open`** (changes data): Open a nonconformance report: a task of the company's NCR task type (Settings), linked to the task it was found on and carrying its machine and work order, with the affected lot or product recorded on it. A failed in-line check opens one by itself; use this for what people find. Disposition it with ncrs_disposition. - **`partner_deliveries_receive`** (changes data): Book goods a partner sent you into stock, line by line (damaged quantities go to the bads bin), optionally reporting the rest missing. - **`purchase_orders_list`**: Purchase orders (purchase requests): vendor, status, and each line's product, quantity ordered and received. Quantities only: EightyOne holds no prices. - **`purchase_orders_receive`** (changes data): Book what arrived: per PO line, the quantity and the storage unit it goes to (with the lot for lot-tracked products). An inspection plan for the product or vendor puts it in quarantine and opens an inspection task. Can't receive more than ordered. Needs 'Receive inventory'. Preview it first. - **`shipments_cancel`** (changes data): Cancel a shipment that hasn't shipped. Nothing moves: a draft holds no stock. Needs 'Manage inventory'. - **`shipments_create`** (changes data): Draft a shipment to a customer: per line the SKU, quantity and the storage unit it ships from (and the lot for lot-tracked products). Nothing leaves stock until shipments_ship. Factories only. Needs 'Manage inventory'. - **`shipments_get`**: A shipment with its lines. - **`shipments_list`**: Shipments to customers, newest first: draft (being prepared), shipped or cancelled. - **`shipments_ship`** (changes data): Confirm a draft shipment left: each line's goods come out of stock, written to the customer. For a customer that is an EightyOne company, the goods arrive in their transit. Refused if any line lacks the stock, names a lot that isn't available, or takes another customer's consigned stock. Needs 'Manage inventory'. Preview it first. - **`ssus_get`**: One SSU by its external id, with the company's guidance note on it for agents (if any): read it before drawing conclusions about this machine. - **`ssus_list`**: The company's SSUs (sub-site units: machines, assets, locations within a site), newest first. - **`stock_transactions_list`**: The stock movement ledger (consumption, receipts, transfers, shipments), newest first, for pull-based ERP sync. Quantities only. - **`stock_transfer`** (changes data): Move stock between storage units, or between bins in one unit, as the dashboard's transfers do: name the product by SKU, and the lot or serial when it's tracked. It can't move more than is there. Needs 'Manage inventory'. - **`storage_units_get`**: A storage unit and everything in it: per product, bin (goods / bads / quarantine), lot and serial. - **`storage_units_list`**: Where stock sits: each storage unit (shelf, rack, bay, dock…) with its storage site and whose it is: warehouse or site (the company's own), client, personnel (a technician's van), customer_consigned, partner. - **`storylines_ssu`**: What happened on one machine (SSU) in a window (default: the last 7 days), as a chronological narrative: orders run, stops and their reasons, maintenance done, scrap and NCRs, meter readings, plus headline numbers. The best way to explain a machine's downtime. - **`storylines_work_order`**: The life of one work order as a narrative: release, each operation started and finished, what was consumed and produced, scrap, quality checks and NCRs, shipment; with lead time and yield. - **`task_types_get`**: One task type by its id. - **`task_types_list`**: The company's task types: the templates every task is created from (work orders' operations, maintenance, NCRs, inspections, approvals…), alphabetically. Use a type's `id` as `typeId` when creating a task. - **`tasks_connect`** (changes data): Link two tasks. 'dependency': the second waits for the first. It can't progress until the first is done, and goes back to open if it was already ready or dispatched (it becomes ready if the first is already done). 'follow': only records that the second follows the first. Connections are removed in the dashboard. - **`tasks_create`** (changes data): Create a task of a task type, exactly as the dashboard does: the type's defaults apply (SLA due date, default assignee, transit, created-as-ready), and the task.created webhook fires. Needs 'Create tasks' unless the type is user-generated. - **`tasks_get`**: One task by its id. - **`tasks_get_form`**: The fields a task's form records, in order: type, whether it's required, the choices of a select, the limits of a measurement, whether a condition hides it, and the current answer. Answer them with tasks_submit_form. - **`tasks_list`**: Tasks, newest first. Everything in EightyOne is a task (work-order operations, maintenance, inspections, NCRs, approvals…). Open tasks only unless a status is given or includeCompleted=true. Without the 'See all tasks' permission you see only tasks assigned to you, plus your teams' tasks with 'See tasks for my team'. - **`tasks_record_counts`** (changes data): Add good and scrap quantities to a running work-order operation or producing task, as operators do from the app. Counts add up and feed OEE; the output posts to stock once, when the task completes. To count at completion instead, pass goodQuantity / scrapQuantity to tasks.update with the status. - **`tasks_submit_form`** (changes data): Answer a task's form fields by title (tasks_get_form lists them), and optionally complete the task (status done or submitted) or mark it incomplete, as the app does. Completing needs every required field; measurements outside their limits open an NCR; machine-status fields set the machine's status. Counts and failure codes go with the completion, as in tasks_update. Preview it first. - **`tasks_update`** (changes data): Change a task, with the dashboard's rules: status changes need 'Change task status from mobile' (assignees may still complete their own work), reassigning needs 'Reassign tasks', blocked tasks can't progress, producing tasks complete with goodQuantity / scrapQuantity (counts during the run: tasks_record_counts), maintenance types may need failure codes (problemCodeId, causeCodeId, remedyCodeId) to complete, 'incomplete' needs a reason and can create a follow-up (createFollowUp), 'declined' needs a reason. Tasks sent to a partner only accept details, dueDate and priorityId. - **`work_orders_get`**: One work order with its operations (the traveler's tasks) and their status. - **`work_orders_list`**: The company's work orders, newest first, optionally by status. Use for ERP pull-sync or to see what production is working on. - **`work_orders_release`** (changes data): Release a draft work order to the floor: one operation task per routing step, each on the only machine of its work center when there is just one, with the type's parameters pinned and later steps waiting for gates. Needs 'Release work orders'. Preview it with dryRun first: it lists the operations it creates. ## More capabilities Not listed as tools, to keep the list short. Find them with `capabilities_search` and run them with `capabilities_call` (pass the name below). - `data_packages.get`: Get a data package - `data_packages.list`: List data packages - `guidance.list`: List company guidance for agents - `handoffs.get`: Get a handoff - `handoffs.list`: List handoffs - `handoffs.timeline`: Handoff timeline - `machine_catalog.lookup_sku`: Find a machine by SKU - `partner_deliveries.get`: Get a partner delivery - `partner_deliveries.list`: List partner deliveries - `reason_codes.list`: List reason codes - `teams.list`: List teams - `webhooks.delivery_logs`: Webhook delivery log - `webhooks.event_types`: List webhook event types - `webhooks.get`: Get a webhook - `webhooks.list`: List webhooks ---