# Plan and commit

> Why no Tabba tool writes on its own, what a plan and its token are, and what the server holds a commit to.

Source: https://www.tabba.io/developers/mcp/propose-commit

Tabba works on one rule: the agent prepares, a person approves. On the MCP
server that rule is two tool calls. An agent that hears "five hours" and
takes seven off a line has changed what a customer is invoiced, so the server
has no tool that both decides and writes.

## Two steps, always [#two-steps-always]

1. **Plan.** A `plan_*` tool resolves what was asked, holds it to the rules
   Tabba holds any write to, and answers a summary in plain language and an
   opaque `proposal_token`. Nothing is written.
2. **Commit.** The separate `commit` tool takes that token and nothing else,
   and applies the plan the server kept.

What is written is the plan you read. The client never sends the change a
second time, so it cannot commit something other than what it showed you.

Which plan tools you are offered follows your role. A standard user is
offered `plan_register_work` and `commit`. An auditor is offered no plan tool
and no `commit`. Every other plan tool is an admin's, and the organisation
admin, an admin and a bookkeeper are admins here: `plan_upsert_customer`, `plan_upsert_product`,
`plan_create_invoice`, `plan_delete_invoice`, `plan_create_series`,
`plan_cancel_series`, `plan_setup_from_contract`, and with `mcp:issue` also
`plan_issue_invoice`, `plan_issue_occurrence` and `plan_create_credit_note`.

## What the server enforces, and what the client is asked to do [#what-the-server-enforces-and-what-the-client-is-asked-to-do]

These are different things, and only the first is a guarantee.

**The server enforces:**

* No tool but `commit` writes. Every plan tool is marked read-only in its
  annotations, and `commit` is marked destructive.
* `commit` applies a plan that exists on the server, belongs to you and to
  the company of your token, has not run out, and still describes the records
  as they are.
* A plan is applied once.
* The token's scope and your role are asked again at the commit.

**The client is asked:**

* To show you the plan and wait for your confirmation before it calls
  `commit`. The server's instructions and the description of every plan tool
  say so, and for a plan that books they say to name the very document.
* To put a plan tool's questions to you and never answer them itself.

The server cannot see whether a client showed you anything. If your client
can require approval per tool, require it for `commit`: that is the one call
where a change becomes real, and reading and planning stay free.

## What a plan looks like [#what-a-plan-looks-like]

A plan is answered as text the agent can show you, with the same content in
structured form beside it:

```
**Register work** for Jane Doe on "Development (Bredgade ApS)".

| figure | now | becomes | change |
|---|---|---|---|
| hours | 12 h | 17 h | +5 h |

An hourly line: the hours move its revenue.

Nothing is written yet. Show the user this plan, and once they confirm call
commit with proposal_token="mcpp_6f1c2b9e-…" (runs out
2026-10-13T09:27:33+00:00; takes mcp:write).
```

The last sentence is one line on the wire. A plan that reduces a figure says
so under `Warnings`. If the summary is right, the agent calls `commit` with
the token. If it is not, you say so and nothing was written.

## The token [#the-token]

* **Fifteen minutes.** After that the commit is answered `PROPOSAL_EXPIRED`
  and the plan tool is called again. A plan describes the records as they
  stood when it was made.
* **Applied once.** Committing the same token again writes nothing and is
  answered the first commit's result. A client that retries a commit that
  timed out cannot apply it twice. The result is kept for a day after the
  plan's expiry. After that the token is `PROPOSAL_NOT_FOUND`.
* **Its planner's.** A token is good for the person and the company it was
  planned for. Another member of the company, an admin included, finds no
  plan under it and is answered `PROPOSAL_NOT_FOUND`.
* **Opaque.** It is `mcpp_` and an id. It carries nothing of the plan.

> [!NOTE]
> **PROPOSAL_STALE means a record changed**
>
> A plan keeps the state of every record it reads or writes, and the commit
> compares them again before its first write. If a colleague changed the
> invoice, or registered hours on the line, after the plan was made, the
> commit is refused: what you confirmed is no longer what would be written.
> Call the same plan tool again, read the new plan, and commit that token.
> Nothing is absorbed into the old one.

A plan that books depends on more than the records it names. A plan to issue
an invoice is stale once work is registered on its lines. A plan to issue an
occurrence is stale once what the occurrence bills has changed. A plan to end
a recurring series is stale once the end would credit, drop or issue
something else than the plan listed.

## When a commit fails part-way [#when-a-commit-fails-part-way]

A plan can hold several steps: the contract plan creates a customer, its
products and a recurring series in one commit. Each step is kept as soon as
it is applied.

* If a step fails, commit the same token again. The applied steps are taken
  as they came out and the rest are applied. No record is created twice.
* A resume compares the records again, for everything the first attempt did
  not itself write. A change in between is `PROPOSAL_STALE`, and the answer
  says that part of the plan was applied: read what is there, then plan what
  is left.
* A resume is bounded by the plan's fifteen minutes.
* Two commits of one token never both apply it. The second is answered
  `COMMIT_IN_PROGRESS`: wait a few seconds and commit the same token again.
  Do not plan again.

## When a plan tool asks back [#when-a-plan-tool-asks-back]

A plan tool can answer `needs_clarification` with questions in place of a
plan. The agent puts them to you and calls the same tool again with your
answers. Nothing is kept on the server for a question.

```
More is needed before this can be planned:
1. [hours_mode] Jane Doe already has 12 h on "Development (Bredgade ApS)". You gave 5 h. Does that REPLACE the total, or is it ADDED to it?
   - replace: the total becomes 5 h (a change of -7)  [answer: replace]
   - add: the total becomes 17 h (a change of +5)  [answer: add]

Ask the user, then call this tool again with the same arguments and hours_mode filled in (top-level, or under "answers"). Send an [answer: …] value exactly as written.
```

Each option carries the exact value to send back, so an agent that reads only
text can still answer.

### Hours are a total [#hours-are-a-total]

`plan_register_work` registers a person's total on a line, not an increment.
It shows what is registered already, and it takes either form:

| Argument      | Meaning                                                   |
| ------------- | --------------------------------------------------------- |
| `hours`       | The new total of hours on the line.                       |
| `hours_delta` | A signed change: `5` adds five hours, `-2` takes two off. |
| `hours_mode`  | The answer to the question above: `replace` or `add`.     |

Passing `hours: 5` on a line where 12 are registered is a request to take
seven off, so the tool stops and asks. Where nothing is registered yet the
two readings are the same number, and it goes ahead. Pass `hours_delta` when
you mean to add.

Completion works the same way: `completion_rate` (0 to 1) or
`completion_percent` (0 to 100) for the new value, `completion_rate_delta` or
`completion_percent_delta` for a change, and `completion_mode` for the same
question. Hours and completion are both kept on every line. The line's
recognition method, `hourly` or `value`, decides which of them moves revenue.

The line is named by a `line_id` from `get_my_work`. A recurring series shows
the lines of its current occurrence there before the occurrence is an
invoice. Registering work on such a line makes the occurrence an invoice at
the commit, and the plan says so.

A standard user registers their own work. Naming a colleague in `user` is
refused with `REGISTER_FOR_OTHERS_FORBIDDEN`. An admin may name a colleague
who holds a place on the line.

### Other scales [#other-scales]

* **Shares.** The plan tools take `distribution_percentage`, a person's share
  of a value line, as a percentage from 0 to 100, and the shares of a line
  add up to 100. The read tools state the same share as a fraction: `0.6` is
  60 %. An hourly line takes no share: each person on it bills at a rate.
* **Money** is in major units, and an amount a plan tool takes is before
  VAT. `get_config` states the company's base currency.
* **An occurrence** is named by `period`, the first day of its billing
  period, as `YYYY-MM-DD`.

## What takes mcp:issue [#what-takes-mcpissue]

A token with `mcp:write` drafts and registers: nothing it commits is booked
in the accounting system, and nothing is sent to a customer. That is decided
by what a plan leads to, not by which tool made it. These plans take
`mcp:issue`, and all of them are an admin's:

| Plan                                                                                    | Why                                                                        |
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `plan_issue_invoice`, `plan_issue_occurrence`, `plan_create_credit_note`                | They book in the accounting system and send to the customer.               |
| `plan_create_invoice` with `auto_send: true`                                            | The daily run issues the invoice on its issue date.                        |
| `plan_create_series` or `plan_setup_from_contract` with `activate: true`                | The series runs from the commit, and the daily run issues its occurrences. |
| `plan_cancel_series`, where the end credits what was sent past it or issues what is due | The end books a credit note or sends an invoice.                           |

By default a created invoice is a draft that waits until it is issued, and a
created series is a draft that invoices nothing until an admin makes it
active in Tabba.

A plan that takes `mcp:issue` is refused at plan time for a token that holds
only `mcp:write`, with `SCOPE_INSUFFICIENT`, before you are asked to confirm
anything. Its commit is refused for such a token too.

## The contract plan and its form [#the-contract-plan-and-its-form]

`plan_setup_from_contract`, an admin's tool, plans the whole billing setup of
a contract at once: a new customer where they are not in Tabba yet, the new
products it needs, and either a one-off draft invoice (`billing: "one_off"`)
or a recurring series. You confirm once, and one commit writes all of it.

What the contract leaves open is asked: a line's product, how a new product
is filed and counted, and, as one question per line, the line's amount and
who works on it at what split or rate. The agent is told not to divide a
price or suggest amounts itself.

In a client that renders MCP Apps, those questions are shown as a form under
the tool's result. The form posts your answers as your own message in the
conversation. It plans nothing and commits nothing: the agent calls the plan
tool again with the answers, and you still confirm the plan before `commit`.
Where a client shows no form, the same questions are asked in the
conversation. Whether the form shows is the client's to decide.

## Every error says what to do next [#every-error-says-what-to-do-next]

A refusal is a tool result the agent reads, not a transport fault. It carries
a code, a message and a `next_step`:

```
Error [PROPOSAL_STALE]: A record the plan depends on changed after it was made. Nothing was written.
Next: Call the plan_* tool again: it plans from what is there now. Show the user the new plan before committing it.
```

The codes are the ones the REST API uses, listed under
[Errors](/developers/api/errors), plus a few that only a surface with a plan
and an acting person can answer. Where the accounting system refused, its
reason and its own words are in the message.

| Code                            | What happened                                                                                                | What to do                                                               |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `PROPOSAL_NOT_FOUND`            | The token names no plan of yours on this company: never issued, another person's, or removed after its time. | Plan again and commit the new token.                                     |
| `PROPOSAL_EXPIRED`              | The plan ran out before it was committed. Nothing was written.                                               | Plan again, read the new plan, commit its token.                         |
| `PROPOSAL_STALE`                | A record the plan depends on changed after it was made.                                                      | Plan again: the tool plans from what is there now.                       |
| `COMMIT_IN_PROGRESS`            | Another commit of the same token is applying the plan.                                                       | Wait a few seconds and commit the same token again.                      |
| `REGISTER_FOR_OTHERS_FORBIDDEN` | A standard user named a colleague in a work registration.                                                    | Register your own work, or ask an admin.                                 |
| `AMBIGUOUS_REFERENCE`           | A name matched several records.                                                                              | Pick from the candidates the answer lists.                               |
| `SCOPE_INSUFFICIENT`            | The plan books or sends, and the connection holds no `mcp:issue`.                                            | Connect again with a client that asks for issuing, or issue it in Tabba. |
| `ADMIN_REQUIRED`                | The plan is an admin's to commit, and you are no longer one.                                                 | Ask an admin.                                                            |
| `READ_ONLY_ROLE`                | You are an auditor of the company now, and an auditor commits nothing.                                       | Ask an admin or a bookkeeper to make the change.                         |

## What cannot be undone [#what-cannot-be-undone]

Some commits cannot be taken back from Tabba:

* **Issuing** books the invoice in the accounting system and sends it to the
  customer. An issued invoice is not changed or deleted. It is corrected
  with a credit note.
* **A credit note** is itself booked and sent, and is not taken back.
* **An invoice set to `auto_send`, an activated series, and an end of a
  series that credits** lead to the same bookings without a further
  confirmation.

All of these take `mcp:issue`, so a connection without it can draft
everything and book nothing. See [Auth and scopes](/developers/mcp/auth).

## A record of every commit [#a-record-of-every-commit]

A commit that applies a plan is written to the activity log once, with the
person it was made as, the operation and the client. A second commit of the
same token writes no entry.
