Skip to main content

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 decision is not a log line. It is authoritative until an explicit later decision supersedes it, which is what lets an agent picking work up weeks later continue what you agreed rather than re-argue it — and what makes a change of course visible, because reversing a decision records a new one naming the decision it replaces.

What a decision carries

  • The commitmentwhat in one line of business English, and why: 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 as decider_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.
  • Applicabilitystanding for a course that outlives the work it authorized, one_shot for an authorization covering exactly the actions it named. A page that published on Tuesday is not a course anybody continues; “target finance leaders on this campaign” is.
  • 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_id of 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 alternativesalternatives_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.
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.
You do not configure this and it does not gate anything: the approval card is unchanged, and the declaration is a sentence attached to the record rather than a condition on the change going ahead. What it changes is what the record can tell you afterwards. 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.
Nothing in this readout changes your autonomy settings. Your configuration stays the ceiling; the scorecard is evidence you read, never something that grants Erdo more authority.

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

Identity and access ride the request context, so a caller only ever reaches their own organization’s record — and inside it, only the projects they can view. 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 org with X-Organization-ID.
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. GET /v1/decisions pages with limit (default 50, max 200) and offset, newest first. 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.