Decisions
A Workstream remembers a great deal of context: what was discussed, what was tried, what somebody suggested. A decision is the narrower thing your organization actually committed to — a course, the reason it was chosen, who authorized it, and what it changed in the outside world. Approving a card records one. So does an agent committing to a strategy that changes nothing outside Erdo, such as which audience a campaign targets. The record exists to answer questions that used to have no data behind them. What did we decide about this campaign, and why? Did the change actually go through? Did leads or cost move afterwards, and how strong is that evidence? And the one every other question is really asking: can this be trusted to make these calls without me? A standing decision carries a review deadline. It guides work only until that deadline or an explicit supersession; passing the deadline lapses it without deleting its history. A new workstream commitment must be reviewed within 14 days forpaid_media.* classes or 30 days for other classes. An optional exit condition can end it sooner. This lets later work continue a current course while making old assumptions visible for review.
The loop can reconfirm a course only by recording a new decision with a fresh
review_by that supersedes the old slug. When the exit condition is met or the
premise no longer holds, it can retire its own workstream’s hold using
record_standing_decision with retire=true, supersedes_decision_slug and a
reason in why. This changes no provider state and needs no approval; changes
to campaigns or other external resources still use their normal approval gates.
A second current commitment with the same class and subject in the same
workstream is refused unless it explicitly supersedes the existing one.
Agent reconfirmation and retirement stay within the workstream that recorded the hold.
Operators can retire it through the decision surfaces below. Scheduled monitors, such as
the paid-media heartbeats, read standing decisions to set their baselines and ignore lapsed ones;
when a lapsed hold still looks relevant, they mention it in their summary or raise an attention
item instead of changing it.
Retire a superseded commitment
After changing a course directly, end its old standing commitment so later reconciliation passes stop treating it as authority. Organization admins and owners, including authorized managers and delegated client credentials with that role, can retire aworkstream_commitment:
POST /v1/decisions/{decisionSlug}/retire with
{"reason":"The course was replaced"}. Add organization_slug to target an authorized
client organization. MCP exposes erdo_retire_decision, taking decision_slug, reason,
and optional organization_slug.
The response contains retirement, the one-shot effective decision recording who ended the
course and why; decision, the original with superseded_by_decision_slug; and created.
Repeats return the existing retirement and created=false. If a later course already replaced
it, the current state is returned without creating another retirement. Lapsed commitments are
already out of force and return a 400 explaining that they lapsed. Approval-sourced and
one-shot decisions cannot be retired here. Scoped API keys, external-user tokens and guests
cannot retire decisions. This operation changes no campaigns or other provider state.
An expired commitment may still say status=authorized: that is its recorded lifecycle,
not a claim that it remains in force. lapsed=true means its review deadline passed;
superseded_by_decision_slug means another decision ended it. Neither rewrites the original
authorization. Current standing lists and reconciliation exclude lapsed and superseded holds.
What a decision carries
- The commitment —
whatin one line of business English, andwhy: the argument the proposal was made on. When the person deciding gave their own reason for accepting, refusing or correcting it, that is kept separately asdecider_rationale. The two are never merged: a proposal’s argument read months later as the approver’s reason for saying yes is a motive nobody stated. - The class — a stable, provider-agnostic name for the kind of decision this is
(
paid_media.ad_group.pause,page.publish,experiment.variant.stop). It is what the scorecard groups on, so it survives a tool being renamed or split. - Applicability and review —
standingfor a course that outlives the work it authorized,one_shotfor an authorization covering exactly the actions it named. Every new workstream standing decision hasreview_by(maximum 14 days forpaid_media.*, 30 days otherwise) and may include anexit_condition. Lapsed decisions remain in history; current and lapsed workstream decisions are shown separately. - The authority — who exercised the judgement (
human,llm_agent,deterministic_policy,safe_default,system) and, where relevant, which person and which agent run. A decision a person made carries that person’s name (decided_by_name) alongside the kind, so the record reads “X approved this”, not just “a human did”. - The subject — the campaign, page, or lead form the decision is about, so the record is searchable by the thing that changed.
- The ask it answers — for a decision that came from an approval, the
approval_idof the card somebody answered. It is on the row and it is also a filter, so you can go from an approval straight to what answering it produced, whether the answer was yes or no. One approval can name several decisions: a batch is split into one decision per coherent intent once it is answered. - Actions — one row per exact external call the decision authorized, each with its own subject and its own execution result. A batch that pauses four ad groups keeps four results, so a partial success stays visible instead of collapsing into one flag.
- Expected effects — what the decision said, in advance, it would move: the metric, the predicate that counts as success, the windows to judge it over, and any guardrails. Most decisions declare none, and that is the honest answer rather than a gap — a permission fix has no business effect anything could measure.
- The alternatives —
alternatives_considered: what else the proposing agent weighed and rejected, asked for at the moment the change is proposed. It is optional, because most changes have no real alternative — a fix, a correction, a bookkeeping write — and Erdo leaves the field blank in those cases. Where it is filled in, it shows you the options that were on the table.
Paid media declares what it expects, without being asked
Most expectations are declared by whichever agent proposes the change. Changes to Google Ads bidding and budgets are different: they declare one automatically, because what a spend change is supposed to achieve is a property of the change itself rather than something anybody should have to remember to write down. Four changes carry a declaration: a campaign’s budget, an ad group’s CPC bid, enabling a campaign, and any campaign update whose fields include the bidding strategy or the budget. Each one declares the same thing — cost per conversion should not end up materially worse than it already is — with the numbers filled in from that campaign’s own last fourteen days, read at the moment the ask is raised:- The metric is Google Ads cost per conversion for that campaign alone.
- The target is the campaign’s current cost per conversion plus 25%, over the next fourteen days, against the fourteen before the change. A change that lands within a quarter of where it started is inside the noise of a fortnight of ad auctions; outside it is something you would want to know about.
- The baseline is stated in the decision in plain terms — what the campaign was costing per conversion, over how many conversions and how many clicks — so you can see what the prediction rested on, not just what it predicted.
erdo decisions list --outcome not_met becomes a real question about your ad spend, and a change that made leads more expensive
stops being something you have to notice yourself.
Two cases deliberately declare nothing. A campaign with fewer than five conversions in the
previous fortnight has no stable cost per conversion — one more conversion would move it by a
quarter — so no target is invented from it. And pausing or removing a campaign takes its
traffic to zero, where cost per conversion is not a number that can be measured at all. In both
cases the decision is still recorded in full; it simply carries no expectation, which is a
different and more honest thing than carrying one nothing can ever settle.
When a paid-media effect settles
About two weeks after the change goes live. The clock starts when the change is fully in force — not when you approved it — and runs for the declared horizon, which is fourteen days for the bidding and budget changes above. Erdo then compares that fortnight against the fortnight before the change, and records the answer on the decision. The comparison reads your own synced Google Ads data, not the Google Ads API. Erdo never calls a provider to score a decision: the numbers come from the account data already syncing into your workspace, so the same figures are there for you to check, and the exact query behind a settled outcome is recorded alongside it. If your Google Ads account is not synced into Erdo, the effect says so and waits rather than reporting a result — and connecting it later is enough: the next completed sync picks the waiting effects back up and measures them, including ones recorded before the account was ever connected. Two things stop an outcome being claimed either way. A window with fewer than five conversions cannot support a cost-per-conversion claim — one more lead would move it by a quarter — and a window with fewer than thirty clicks cannot support a click-based one. And if the synced data stops well before the window closes, the reading is not evidence about the period it is supposed to describe. Both settle inconclusive with the reason recorded, rather than a met or not-met nobody should act on. The day the change went live belongs to neither window: it is part before and part after, and counting it either way would quietly put post-change traffic in the “before”. Because this is a before-and-after comparison with no control group, a met outcome reads moved as expected rather than worked — the metric did what the decision said it would, over a period in which other things also changed. The distinction is kept everywhere the outcome is shown; see How an outcome is described.Open a decision in full — the report
Wherever a decision’s slug appears — on a Workstream, on an experiment — it is a link to that decision’s report: one page that shows what was decided, what it was decided on, and what has happened since. It reads in the same order as an attention item’s report, and leaves out any section it has nothing to say about:
The report is read-only. Answering, approving and steering stay on the
approval card and the attention item, and nothing on this page changes
your autonomy settings.
Add
?org=<organization-slug> to the URL and Erdo switches to that organization before opening the
decision, which is how someone running several organizations from a
manager account links a colleague to one customer’s decision.
The lifecycle
effective means the change is live in the outside world. measuring means its declared
expectation is being watched. settled means the evidence came in. censored means something
intervened — usually a later decision replacing this one mid-window — so there is no honest
before-and-after left to compute; the decision still stands, what was lost is the ability to
score it.
An action Erdo could not confirm ends unknown rather than succeeded or failed. That state is
kept deliberately: the change may be live and nothing verified it, and it is resolved by reading
the provider back, never by trying again.
How an outcome is described
Every settled outcome carries the kind of evidence that produced it, and the words Erdo uses depend on it. This is not a stylistic preference — a before-and-after movement presented as proof the change caused it is the most damaging thing this record could produce, so the vocabulary is fixed to what the evidence can support:
An uncertain or stale result is
inconclusive under every kind, with a reason recorded — a
result nobody can explain is a result nobody can act on.
Erdo sends the phrase itself as outcome_label on every read, so each surface says the same
thing about the same row rather than deriving its own wording.
The scorecard
decisions scorecard reports raw counts with their denominators. It deliberately publishes
no eligibility verdict and no single pooled “worked rate”:
- Totals — every decision in the window, split by status and by producer family, with the backfilled legacy history named separately so the numbers reconcile.
- Execution — how many decisions authorized an external call at all, how many actions ran, and how each ended, with the unconfirmed ones counted rather than absorbed into a success rate.
- Measurement coverage — how many decisions declared an expectation, how many effects exist, and where each one sits: settled, censored, or waiting on a measurement Erdo cannot yet read. A met rate over six settled effects out of two hundred decisions is a different claim from the same rate over a hundred and eighty, so the denominator travels with it.
- Outcomes, stratified — the settled split cut by decision class and evidence kind, and again by producer family and evidence kind. Never pooled: averaging a deterministic confirmation against an observational movement produces a number that is wrong in whichever vocabulary you read it in.
- Deciders — who exercised the judgement, how often the safe default applied because nobody answered, and how often a person refused something the system proposed.
- Time to authorize — the median and 90th percentile wait between a decision being proposed and being authorized.
CLI
erdo decisions list prints one row per decision: the slug, its status, its execution result,
what it was, and — only once something settled — how it turned out, in the words its evidence
supports.
erdo decisions show prints the drill-in in the order the record means something: the commitment
and its authority, then what was actually attempted, then what was expected and what the evidence
said, then what this decision replaced or was replaced by.
Decisions are referenced by slug, never UUID. Commands act on your active organization; pass
--org <idOrSlug> to pin a different one.
MCP tools
Decision tools are in thework toolset. A connection with the curated list on does not list them. Add X-Erdo-MCP-Toolsets: work, or ?toolsets=work on the MCP URL. See Toolsets.
Lists default to your active organization. To read a client from a manager workspace, pass
organization_slug to erdo_list_decisions or GET /v1/decisions, or use
erdo decisions list --organization <client-slug>. The same user must be a member of the target
organization or have access through an active manager relationship. Scoped API keys and scoped
tokens cannot cross organizations. Unknown and inaccessible organizations both return 404.
Project visibility is checked in the selected organization under that same identity. A decision filed under a
workstream inherits that workstream’s project access, so a teammate who was never given a
restricted project sees none of its decisions in the search, cannot open one by slug, and does not
have its rows counted into the scorecard’s totals. Decisions that belong to no workstream — a page
publish, an email send — are organization-level and stay visible to everyone.
REST
Base URL
https://api.erdo.ai. Authenticate with Authorization: Bearer <token> and select the
active org with X-Organization-ID. Lists also accept organization_slug for an authorized
client read. Detail and scorecard reads use the active org; switch to the client organization
before opening a listed client decision in full or requesting its scorecard.
GET /v1/decisions/:slug returns the envelope, its actions, its effects and a lineage. The
lineage carries supersession in both directions plus workstream_slug, attention_item_slug, and
the approval card — its resolved display, the argument beneath it, and one entry per action with
the compact parameters somebody judged. The full tool input is left out, because a read API carrying
provider payloads would become a second store of them. Each action carries its annotations: the
before-state the change replaced, such as the ad copy an edit is replacing.
A call that failed at the provider and was corrected under the same approval appears as two
actions: the attempt that failed, marked superseded_by_retry, and the one that landed, marked
is_carried_retry. The detail keeps both because the first one is what happened and the provider’s
error is worth reading — but only the later attempt counts, so the decision’s own
execution_status, and the scorecard’s execution totals, weigh that approved call once.
GET /v1/decisions pages with limit (default 50, max 200) and offset, newest first. With applicability=standing it returns two lists: decisions holds the current commitments and lapsed_decisions the ones past their review deadline. Each list is paged on its own offset (offset and lapsed_offset, with the same limit), so a short page ends only that list and lapsed commitments never take a current commitment’s place on a page. Its optional since filter is an inclusive RFC3339 lower bound on the latest known movement: proposal, authorization, or trusted rejection. Unknown rejection dates remain unknown, so a proposal still matches by its proposal time. An
unrecognised value in a closed vocabulary — a misspelled status, a source that does not
exist — is refused rather than ignored, because silently dropping the filter would return the
whole organization to a caller who asked for one slice of it. approval_id is refused the same
way when it is not a UUID, and it narrows within your access rather than around it: an approval
answered inside a project you cannot view matches nothing.
GET /v1/decisions/:slug returns the decision, its actions, its effects, and its lineage. A slug
belonging to another organization comes back 404 rather than 403, so a slug can never be
probed for existence.
GET /v1/decisions-scorecard takes since and until as RFC3339 timestamps (since inclusive,
until exclusive) and echoes the window back on the response, so a readout can never be quoted
without the period it covers.
When an agent or Daily Monitor calls list_decisions, it must name the client’s
organization_slug on each client read. The agent tool retains its since, class_prefix, and
limit filters, plus scan_complete and truncated honesty flags. A failed or incomplete read
cannot establish that no paid-media decisions occurred; an empty manager-org read says nothing
about a client’s history.
