Skip to content
Decision log

An append-only decision log for AI coding agents.

One entry per material decision — the title, the rationale, the logical date, whether it changes a public contract, and the PR and commit it shipped in — written by the agent over MCP or REST, attributed to the credential that wrote it, and linked to the build session that was open. A database trigger rejects every delete and every update except the two source links; getting one wrong means a new entry that points at the old one.

decision log

append_decision_log → 201 · linked to the open session
Decision-log entry A ledger row with a timestamp, a decision pill and a text stub. 14:02:11 decision Second decision-log entry A second ledger row with a later timestamp. 15:47:03 decision A correction entry A third ledger row whose pill reads corrects — a new entry pointing at the first. 16:10:58 corrects

rows are appended, never rewritten · the third corrects the first · illustrative

How it works

Append. Correct. Amend the source. Never rewrite.

Four verbs, in the order an agent meets them. Each tool links to its row in the catalogue.

  1. Append

    A title and a rationale in markdown, the decision’s logical date, whether it changes a public contract, how many files it touched, and — when known — the PR and commit. If the same agent has a build session open, the entry links to it and takes the project from it; otherwise pass project_id.

    append_decision_log
  2. Correct

    A wrong decision is not edited. A correction is a new entry that carries corrects_entry_id, lives in the corrected entry’s project, and links to the open session like any other; the original stays exactly as written.

    correct_decision_log
  3. Amend the source

    Once the PR lands, stamp source_pr_url and source_commit_sha. These two columns are the only in-place change the trigger admits; a no-op amend returns the entry unchanged and writes no audit row.

    amend_decision_log_entry_source
  4. Read it back

    Newest first with a keyset cursor; a substring search across title and rationale; counts by day, week or month; a per-author roll-up. The same nine operations over REST, and two dashboard pages for people.

    list_decision_log
Decision-log entry A ledger row with a timestamp, a decision pill and a text stub. 14:02:11 decision Second decision-log entry A second ledger row with a later timestamp. 15:47:03 decision A correction entry A third ledger row whose pill reads corrects — a new entry pointing at the first. 16:10:58 corrects
Three ledger rows: two decisions and, later, a correction pointing at the first. Nothing is overwritten; the reader sees both the decision and its correction, in date order.

In the session protocol this is step four: the agent starts or resumes a build session, asks which rules apply, does the work, and then writes append_decision_log once per material decision before it files its deferrals and ends the session. Because the append happens while the session is open, the entry links to it without being told to. In Claude Code the skills and hooks that make those calls are on the integration page. The commit that ships the work carries the session id as a trailer, and the entry — once its source is amended — carries the commit; a reader can go from a decision to its session and to the commit that landed it. This repository keeps its own decisions this way; the number is below. Why decisions belong in a log and not in an end-of-session summary — decided and discussed look alike in a paragraph — is the argument of the context-loss guide; where in the session the append happens is the start-session and end-session guide.

What an entry holds

A row, not a document

Each card names the column it is made of. Every column is written once; only the two source links can be stamped later.

Title and rationale

A one-line title (1–300 characters) and the rationale as markdown, up to 100,000 characters. The rationale is what the next session reads when it asks why something is the way it is.

column:body_markdown

A logical date — and a backfill stamp

The date the decision was made, defaulting to today. A past date is accepted and stamps backfilled_at, so an imported or late-written decision sits in its historical position and is marked as written after the fact. Listing and the period counts both order by this date.

column:entry_date

Public-contract flag, files touched

A flag for a decision that changes a public contract, and a count of the files it touched — the two things a reviewer scans a ledger for first. The list shows the flag as a badge.

column:has_public_contract_impact

Who wrote it

agent:<ai_account_id>, derived from the credential that made the call — never taken from the arguments — plus the transport it came in on (api or mcp, stamped by the server) and the client name and version the caller declared. The per-author roll-up groups on this key.

column:authored_by_actor_key

The session it was made in

Auto-linked to the caller's open build session — the most recent active one for the same actor, within the named project if one was given — which also supplies the project. On an append, an explicit session id can be passed instead; without either, project_id is required.

column:related_build_session_id

The PR and commit it shipped in

source_pr_url and source_commit_sha, given at append time or stamped afterwards with the amend-source tool — the only two columns the trigger lets change.

column:source_pr_url
API surface

Nine tools, nine routes, two pages

The MCP tool, the versioned REST route under /v1 and the dashboard page — all three run the same service layer, and REST and MCP serialize the decision-log aggregate through one shared serializer, tested byte-identical.

append_decision_log
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "append_decision_log",
    "arguments": {
      "title": "Ack and sweep predicates live in one UPDATE",
      "body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
      "entry_date": "2026-08-18",
      "has_public_contract_impact": false,
      "files_touched_count": 2
    }
  }
}
REST
POST /v1/decision-log
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "title": "Ack and sweep predicates live in one UPDATE",
  "body_markdown": "Both predicates in the same statement; the loser matches zero rows.",
  "entry_date": "2026-08-18",
  "has_public_contract_impact": false,
  "files_touched_count": 2
}
Dashboard
app.postmq.com/decision-log            newest first; filter by project; search title and rationale
app.postmq.com/decision-log/{id}       the entry, its rationale, what it corrects, its session
correct_decision_log
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "correct_decision_log",
    "arguments": {
      "corrects_entry_id": "01K…",
      "title": "Ack and sweep predicates live in one UPDATE — and the state guard is load-bearing",
      "body_markdown": "The state = 'in_flight' guard on both UPDATEs is what keeps the loser at zero rows.",
      "entry_date": "2026-08-18"
    }
  }
}
REST
POST /v1/decision-log/01K…/correct
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "title": "Ack and sweep predicates live in one UPDATE — and the state guard is load-bearing",
  "body_markdown": "The state = 'in_flight' guard on both UPDATEs is what keeps the loser at zero rows.",
  "entry_date": "2026-08-18"
}
Dashboard
app.postmq.com/decision-log            the correction carries a Correction badge
app.postmq.com/decision-log/{id}       Corrects → links to the entry it supersedes
amend_decision_log_entry_source
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "amend_decision_log_entry_source",
    "arguments": {
      "decision_log_entry_id": "01K…",
      "source_pr_url": "https://github.com/…/pull/519",
      "source_commit_sha": "a4f114c"
    }
  }
}
REST
POST /v1/decision-log/01K…/amend-source
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "source_pr_url": "https://github.com/…/pull/519",
  "source_commit_sha": "a4f114c"
}
Dashboard
app.postmq.com/decision-log/{id}       Amend shipping source — the one form on the page
query_decisions
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "query_decisions",
    "arguments": {
      "q": "lease sweep",
      "limit": 20
    }
  }
}
REST
GET /v1/decision-log/search?q=lease+sweep&limit=20
Authorization: Bearer pmq_…redacted…
Dashboard
app.postmq.com/decision-log            Search title / rationale — the same substring match

The 9 MCP tools · from the server's own manifest

Append-only: an entry is never deleted and, apart from its two source links, never edited; a correction is a new entry that points at the one it supersedes. PostMQ speaks standard MCP, so any MCP-capable client can call them; each name links to its row in the catalogue.

The decision-log MCP tools, from docs/mcp/tool-surface.json
toolwhat it doesneeds
append_decision_log Append a material engineering decision to the append-only decision log. If you have an open build session it is scope-auto-linked and supplies the project; otherwise pass project_id. entry_date defaults to today. Requires the write_session_state scope. required: title, body_markdown write_session_state
correct_decision_log Correct a prior decision by appending a NEW entry that supersedes it (the original is never edited). The correction inherits the corrected entry's project. Requires the write_session_state scope. required: corrects_entry_id, title, body_markdown write_session_state
amend_decision_log_entry_source Stamp the shipping source_pr_url / source_commit_sha on an existing decision-log entry — the only in-place update the append-only ledger permits. At least one field is required. Requires the write_session_state scope. required: decision_log_entry_id write_session_state
get_decision_log_entry Fetch a single decision-log entry in your workspace by its decision_log_entry_id. Open to any authenticated caller. required: decision_log_entry_id authenticated
get_decision_log_entry_summary Fetch the lightweight summary (no rationale body) of a decision-log entry by id. Open to any authenticated caller. required: decision_log_entry_id authenticated
list_decision_log List decision-log entries newest-first (keyset-paged). Pass the previous page's next_cursor to continue. Optionally scope to one project. Open to any authenticated caller. authenticated
query_decisions Search your workspace's decision log by a substring across title and body. Ranked by recency, capped at 50. Returns a lightweight summary shape. Open to any authenticated caller. required: q authenticated
count_decisions_by_period Count decision-log entries bucketed by period (day, week, or month) for an activity view. Optionally scope to a project and an inclusive from/to date range. Open to any authenticated caller. authenticated
summarize_decisions_by_author Roll up decision-log entry counts per authoring actor (with each author's latest decision date). Optionally scope to a project and an inclusive from/to date range. Open to any authenticated caller. authenticated

The 9 REST routes

Under the versioned /v1 API. Writes carry write_session_state; reads carry the default authenticated policy.

The decision-log REST routes
routewhat it doesneeds
POST /v1/decision-log append an entry (auto-links the open build session) write_session_state
POST /v1/decision-log/{id}/correct append a correction that supersedes {id} write_session_state
POST /v1/decision-log/{id}/amend-source stamp source_pr_url / source_commit_sha on {id} write_session_state
GET /v1/decision-log list newest first, keyset cursor, optional project filter authenticated
GET /v1/decision-log/search substring search over title and rationale, ranked by recency authenticated
GET /v1/decision-log/count-by-period entry counts bucketed by day, week or month of entry_date authenticated
GET /v1/decision-log/authors entries per author, with each author’s latest decision date authenticated
GET /v1/decision-log/{id} one entry, rationale included authenticated
GET /v1/decision-log/{id}/summary one entry without its rationale authenticated
What people see

Two dashboard pages, read-only but for the source

app.postmq.com/decision-log lists the workspace's entries newest first — date, title, a Public contract badge, a Correction badge, the author key — with a project filter and a search over title and rationale that is the same substring match the tool runs; Load more walks the same keyset cursor. app.postmq.com/decision-log/{id} shows the fields, the rationale, a link to the entry it corrects and to the build session it was made in, and the one form on the page: amend the shipping source. The overview page lists recent decisions. Two honest limits: the reverse link — from an entry to the corrections that supersede it — is not surfaced, and there is no author filter, only the author column and the per-author roll-up.

Every append, correction and source amendment is also a row on the workspace's audit chain — three of the 38 session-state lifecycle event types written into the same tamper-evident SHA-256 chain as messaging, 38 of 123 event types in all (high-frequency touches such as pings and usage reports are deliberately not audited). A workspace can browse that chain in the dashboard and read it over REST and MCP, with the operator-significant families withheld; the mechanism is on the security page.

Guarantees and limits

What holds, how, and what is deliberately not built

A mechanism per row; the badge says whether it ships on, ships off, or is roadmap. Every row here ships on — nothing in the decision log is optional or default-off.

Decision-log guarantees and limits: what holds, the mechanism, and its scope
what holdshowscope
Entries cannot be edited or deletedC07 tg_decision_log_entries_append_only rejects every DELETE and every UPDATE that assigns any column other than source_pr_url / source_commit_sha — enforced in the database, beneath the service layer shipped
A correction is a new entryC08 corrects_entry_id points at the superseded entry (a database CHECK stops an entry correcting itself); the correction inherits its project; the original is never touched — the list marks it with a badge and the detail page links it to the original shipped
Only the two source links change in placeC60 amend_decision_log_entry_source, read under an update lock so concurrent amends serialize; a no-op amend returns unchanged and writes no audit row shipped
An append links to the open sessionC58 the caller’s most recent active build session for the same actor (within the named project, if one was given) is auto-linked and supplies the project; without one, project_id is required shipped
Every entry is attributedC59 authored_by_actor_key = agent:<ai_account_id>, derived from the credential and never from the arguments; the transport (api or mcp) stamped by the server, client name and version as declared; the per-author roll-up groups on this key shipped
Three events on the audit chainC66 decision_log.appended, decision_log.corrected, decision_log.source_amended — each written into the workspace’s tamper-evident SHA-256 chain in the same transaction as the row shipped
Writes need one scope; reads need only authenticationC65 append, correct and amend-source require write_session_state (operational tier) over REST and MCP alike; the six reads require an authenticated caller in the workspace shipped
Listing is keyset-paged; search is a substring matchC62 newest first over (entry_date, id) with an opaque cursor; query_decisions is a SQL LIKE over title and body, ranked by recency, capped at 50 — not full-text search shipped
Counts bucket by the entry’s dateC63 count_decisions_by_period groups on entry_date (day, week or month, inclusive from/to); a ledger total comes from paging list_decision_log to a short final page, which is how the number below is produced shipped
REST and MCP return the same bytesC05 one serializer for the decision-log aggregate, pinned by a byte-parity test class covering append, get, summary, list, search, count and authors shipped
The dashboard reads; it amends nothing but the sourceC64the reverse link — from an entry to the corrections that supersede it — is not surfaced; there is no author filter, only an author column and the per-author roll-up /decision-log lists newest first with a project filter and a title-and-rationale search; /decision-log/{id} shows the fields, the rationale, the entry it corrects and its session, plus the amend-source form shipped
A markdown decision log importsC20 import_session_state_from_markdown parses the SpecStep markdown convention: preview first, commit once; already-imported entries are skipped by a fingerprint of their source block shipped

A decision log you already keep in markdown comes with you: import_session_state_from_markdown previews the corpus as a dry run and commits it on the second call, and entries already imported are skipped by a fingerprint of their source block, so re-running is safe. The parser is the SpecStep markdown convention; the details are on the session-state page.

Why the trigger rather than a convention: an append-only log is the one artefact that answers “who decided this, and could the answer have been changed afterwards?” without asking anyone to be trusted. That argument, and the honest boundary around it, is on the AI coding governance page.

This is also the ledger people most often ask about after reading their coding client’s memory documentation, so the difference is set out row by row, with a public source on both sides of each row, on PostMQ and Claude Code auto memory — which recommends running both, and says where the client’s own memory is the whole answer.

Our own log

The number is the project's own ledger

687 decision-log entries in this project
2026-06-25 first entry
914of 944 commits on main carrying a session trailer

As of 2026-09-02 — this project's own decision log, counted at build by paging list_decision_log (4 pages, terminated by a short final page); never taken from the period counter.

We run PostMQ on PostMQ. Every number here is generated from the project's own ledger at build — never typed — and the dogfood check fails the build when the file goes stale. The count is produced by walking list_decision_log to a short final page rather than by summing count_decisions_by_period: the counter buckets by each entry's logical date and is an activity view, not a ledger total. The session trailer on a commit is what ties it to the session that produced it.

SpecStep, Valuly and MeetCrew were built with the same session-state system PostMQ hosts.

Questions

Six things people ask

It is the ledger an ADR would be filed in, not an ADR template. An entry is a title, a rationale in markdown, a logical date, a public-contract flag, a files-touched count, the PR and commit it shipped in, the session it was made in and — for a correction — the entry it supersedes. There is no proposed / accepted / deprecated status field: supersession is a correction entry, and the history is the two entries side by side.

No. The database trigger rejects every DELETE and every UPDATE that touches anything but the two source-link columns, from any application path — the service has no delete method, the tools have no delete tool, and the dashboard’s only form amends the source. A decision that turns out to be wrong gets a correction entry; the original stays as written, and the only mark of the correction is the new entry’s pointer to it.

It is a shared record, not a private one. A local notes file lives on one machine and is rewritten in place by the agent that keeps it. A decision-log entry is written to the server over MCP or REST, attributed to the credential that wrote it, linked to the build session that was open, readable by every other agent and person in the workspace, and cannot be edited afterwards. Keep whatever your client keeps locally; log the decisions here.

Yes. The nine operations are also nine REST routes under /v1/decision-log, with the same scope rule and one shared serializer — the read and append responses are pinned byte-identical to the tools’; the dashboard reads the same ledger and can stamp a shipping source. MCP is the transport an agent uses; REST is for scripts and integrations.

Over REST and MCP, any credential in the workspace that carries the write_session_state scope — an operational-tier scope, so an agent’s ordinary credential can hold it; a signed-in owner can also stamp the shipping source from the dashboard. Reads need only an authenticated caller in the workspace. Every write records who: an agent’s actor key is derived from the credential, never taken from the request.

It is a substring match, not full-text search: query_decisions runs a SQL LIKE over the title and the rationale, ranked by the entry’s date, capped at 50 hits. Good for “what did we decide about the lease sweep”; not a ranked index.

Last verified 2026-08-18 against main at a4f114c: the trigger, the service, the nine tools, the nine routes and the two dashboard pages this page names were read on that day.

Related: session state — the protocol this log is step four of · the decision-log tools in the catalogue · the audit chain every write lands on · connect an agent · pricing — the decision log is unlimited on every tier.

Log the first decision. It will still be there.

Create a workspace, connect your agent, call append_decision_log.