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.
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
- 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 opaqueproposal_token. Nothing is written. - Commit. The separate
committool 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
These are different things, and only the first is a guarantee.
The server enforces:
- No tool but
commitwrites. Every plan tool is marked read-only in its annotations, andcommitis marked destructive. commitapplies 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
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
- Fifteen minutes. After that the commit is answered
PROPOSAL_EXPIREDand 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.
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
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
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
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
- 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.6is 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_configstates the company's base currency. - An occurrence is named by
period, the first day of its billing period, asYYYY-MM-DD.
What takes mcp:issue
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
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
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, 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
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.
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.