AI assistants
The copilot is a chat assistant for your team. A user asks for work in plain language, such as drafting a quote or importing a supplier file. The copilot answers, and when it wants to change data it does not write anything. It posts a proposal card that describes the change. A person approves the card, and only then can the change be applied.
That split is the whole model. Approving a card is one step and applying it is another, with its own permission. This guide covers chats (sessions), proposals, approvals and the apply and undo steps. The copilot also keeps saved prompts, a playbook of standing instructions, supplier import rules, scheduled tasks and memory. Those are listed in the API reference and are not covered here.
The examples use <...> for values you supply. For the exact field list of every type, see the API reference.
1. Before you start
The copilot needs the catalog copilot feature on your plan. Without it, every operation here is refused. Each operation also needs one of these permissions:
| Permission | What it allows |
|---|---|
catalog:copilot:chat | Open and manage your own chats, send messages, edit, approve and reject cards, and read the copilot's settings |
catalog:copilot:parse | Parse an uploaded supplier file, and build an image-sourcing proposal |
catalog:copilot:apply | Apply an approved card, and retry the failed items of a batch |
catalog:copilot:rollback | Undo an applied batch |
settings:organization:update | Change the organization's autonomy cap, playbook and saved prompts |
Every chat belongs to the user who opened it. A colleague's chat, a chat of another organization and an unknown ID all read as not found. Writes are rate limited per user. Approving, rejecting and editing cards allow 30 requests a minute, applying, retrying and undoing allow 5, and starting a parse or image search allows 10.
The copilot never has more power than the person using it. When a card is applied, the write is checked against that person's own permissions as well as the apply permission, so a user who cannot create a quote by hand cannot get the copilot to create one.
2. Chats
A chat is a CopilotSession. It holds the messages, the proposal cards the copilot made, and whether a reply is being written right now.
| Operation | What it does |
|---|---|
createCopilotSession | Starts a new chat. |
copilotSessions(first, after) | Lists your chats, most recent first. |
copilotSessionHistory(first, after, search, status) | Lists your chats for a history view: pinned first, then most recent. search matches titles and message text. status is ACTIVE (the default) or ARCHIVED. |
copilotSession(id) | Loads one chat with its newest messages and all its cards. |
copilotSessionMessages(sessionId, before, first) | Reads older messages, oldest first, when copilotSession did not return them all. |
renameCopilotSession, pinCopilotSession | Change the title (at most 120 characters), or pin a chat to the top. |
archiveCopilotSession, unarchiveCopilotSession | Move a chat out of the main list, or back into it. |
deleteCopilotSession | Removes the chat from every list. Its cards and the record of what was applied stay as an audit trail. |
rateCopilotMessage(id, messageId, rating, note) | Rates an assistant message as up or down, with an optional note. |
query Chat($id: ID!) {
copilotSession(id: $id) {
id
title
runActive
activeRunId
messages { id role text cardIds }
proposals { id kind status payload error batchId }
}
}
3. Send a message
Sending a message is a streaming call, so it is a REST endpoint and not a GraphQL operation. The rest of the copilot is GraphQL.
| Call | Purpose |
|---|---|
POST /catalog-copilot/sessions/<session-id>/messages | Send a message and read the reply as a server-sent event stream. |
GET /catalog-copilot/runs/<run-id>/events?after=<seq> | Replay the events a client missed, for example after a dropped connection, and read the run's status. |
POST /catalog-copilot/runs/<run-id>/cancel | Stop a reply. |
POST /catalog-copilot/sessions/<session-id>/files | Upload a file for the chat. A supplier file needs catalog:copilot:parse. An image only needs catalog:copilot:chat. |
GET /catalog-copilot/capabilities | Read what this organization's copilot accepts. |
Authenticate these calls with the same bearer token as GraphQL. The message body takes message, an optional requestId (a UUID you generate), optional attachments (each with the fileKey the upload returned), and optional modelOverride. The stream carries these events: token, card, question, activity, thinking, usage, done and error. Ignore event names you do not know.
Sending a new message on a chat stops the reply that was still running there. While a reply is running, copilotSession reports runActive: true and the run's ID in activeRunId. A client that reloads can use that ID to reattach.
4. Review a proposal
Each card in proposals has a kind (what it does, for example a quote draft or a stock transfer), a payload (the change itself), and a status:
| Status | Meaning |
|---|---|
PROPOSED | Waiting for a decision. |
REPRICE_REQUIRED | Prices changed since the card was made. It behaves like PROPOSED, and approving it accepts the fresh numbers. |
APPROVED | A person approved it. It is ready to apply. |
APPLYING | The change is being made. |
APPLIED | The change was made. batchId names the batch. |
FAILED | Applying failed. error says why. |
REJECTED | A person turned it down. |
SUPERSEDED | The copilot replaced it with a newer card. supersededById names the new one. |
To change a card before you decide, call updateCopilotProposal(sessionId, input) with proposalId and a new payload. The card is checked again and goes back to PROPOSED, so an edit always needs a fresh approval. A card can be edited while it is PROPOSED, APPROVED or FAILED (and REPRICE_REQUIRED for a quote draft). Editing a quote draft prices it again on the server, as the caller, and replaces the payload with the result: you send the changed lines, not the totals.
mutation Edit($sessionId: ID!, $input: UpdateCopilotProposalInput!) {
updateCopilotProposal(sessionId: $sessionId, input: $input) {
id
status
payload
}
}
5. Approve or reject
approveCopilotProposal(sessionId, proposalId) approves a card as the signed-in person. It writes nothing. It only moves the card to APPROVED.
It is refused when:
- The card was superseded. The message names the newer card.
- The card carries blockers, which are reasons the change would be refused if it were applied. Edit the card until the blockers clear.
- The card is in a status that cannot be approved. Only
PROPOSED,REPRICE_REQUIREDandFAILEDcards can be approved. - The card is
FAILEDbut its earlier attempt already created records. Undo that batch first, or applying again would create them twice. - A critical operation needs a confirmation phrase typed in the chat, and it was not typed. The error code is
TYPED_CONFIRMATION_REQUIRED. - The card changes a role. That needs the
confirmationargument set to the role's exact name, and the approver must hold every permission the change uses. - The card is being applied right now. The error is a conflict that says when to retry.
rejectCopilotProposal(sessionId, proposalId, reason, note) turns a card down. reason is one of: wrong customer, wrong items, wrong quantity or unit, wrong price, wrong details, not needed, or other. An unknown reason is refused. note is optional and is shortened above 280 characters. The reason and note help the copilot learn what to do differently. A rejected card that has no reason can take one later, and the first reason stays.
Standing approval for low-risk cards
Some cards are low risk. A user can stop being asked about them:
setCopilotSessionApprovalRule(sessionId, enabled)approves low-risk cards automatically for one chat.setCopilotAutoApproveLow(enabled)does the same for all of the caller's chats.copilotApprovalRules(sessionId)reads both settings.
These never apply to a card of medium risk or higher. Those always need a person.
6. Apply a proposal
applyProposal(input) is the only call that writes. It needs catalog:copilot:apply and takes sessionId, proposalId and idempotencyKey.
mutation Apply($input: ApplyProposalInput!) {
applyProposal(input: $input) {
jobId
batchId
replayed
report
}
}
{ "input": { "sessionId": "<session id>", "proposalId": "<proposal id>", "idempotencyKey": "<unique key you generate>" } }
It is refused unless the card is APPROVED, with Only an APPROVED card can be applied. Beyond that it checks that:
- The organization's autonomy cap allows it (section 8).
- The caller holds the write permission for what the card does.
- The document the card refers to has not moved on since the card was made.
Applying one approval writes once. If the same approved card is applied twice, for example from two tabs, the second call does not create a second set of records. A new approval is what allows the next attempt.
Read report for what changed. batchId names the batch of records written. If a batch only partly succeeded, retryFailedProposalItems(sessionId, proposalId, batchId) applies just the failed items again.
A parse or an image search runs in the background. startCopilotParse(input) takes sessionId and the fileKey of an uploaded supplier file and returns a CopilotJob. Poll copilotJob(jobId) until status is COMPLETED or FAILED. startImageSourcing(input) builds a card for variants that lack images.
7. Undo a batch
rollbackCatalogBatch(batchId) restores the state from before an applied batch. It needs catalog:copilot:rollback, and it also checks the delete permission for what the batch created. The result counts the records restored, deleted, skipped and failed, and details explains each one that needed a note.
catalogBatches(input) lists the batches the copilot applied, newest first, and catalogBatch(id) reads one.
8. Limit what the copilot may do
An organization can cap the copilot for everyone with setCopilotAutonomyLevel(level). It needs settings:organization:update. copilotAutonomyLevel reads the current cap.
| Level | The copilot may |
|---|---|
full | Do anything the user's own permissions allow. This is the default. |
no_money | Draft and do catalog work, but nothing that moves money or stock. |
read_only | Answer questions only. It proposes nothing. |
Tightening the cap does not delete existing cards. They stay in the chat, and applying one that the cap now forbids is refused with a message that names the workspace policy. Pass null to remove the cap.
9. Common errors
| What you see | Why | What to do |
|---|---|---|
| The feature is not in your plan | The catalog copilot is not on the plan | Enable it on the plan |
Missing permission or a permission refusal | The user lacks the permission for the call | Grant it, or hide the action |
Minori session not found | The chat is not the caller's, or does not exist | Reload the list of chats |
| This card was replaced by a newer version | The card is SUPERSEDED | Open the card named in the message |
| Approval is refused with blockers | The change would be refused if applied | Edit the card until the blockers clear |
TYPED_CONFIRMATION_REQUIRED | A critical change or a role change needs a typed confirmation | Ask the user to type it, or pass confirmation for a role change |
Only an APPROVED card can be applied | The card was not approved, or was edited after approval | Approve it again |
| A conflict that says the card is being applied | Another apply is still running | Wait for the time in the message, then retry |
| Applying is refused for the workspace policy | The autonomy cap forbids this kind of change | Ask an administrator to change the cap, or do the work by hand |
| Too many requests | The per-user limit was reached | Wait a minute and retry |