Action cards & money
An action card (ActionCard) is a recommended action waiting for a person to
approve or reject it. Cards come from two places:
- Deterministic derivers over the tenant’s records. These produce the twin cards and the evidence documents.
- The twins agent’s TOON output.
Every card is served through TwinsService.
Anatomy of a card
| Field | Meaning |
|---|---|
type | The business domain and routing key. The derivers and handlers emit Compliance, Finance, Operations, Procurement and S&OP. Model-produced cards may carry another string, which clients treat as uncategorised. |
priority, impact, status | status is Approved or Rejected once decided. Empty means pending. |
transaction_value | The card’s contribution to the dollar headline (see below). |
| Artifact fields | One typed payload per artifact family: route_card, email_draft, calendar_invite, erp_update, claims_card, cold_chain_card, customs_clearance_card, and others. |
evidence_document | One field for the whole evidence-document family. See Evidence documents. |
origin (ActionOrigin) | Which stage produced the card: reconcile, fetch or simulate, with a run_id and a label. If it’s absent, the stage wasn’t recorded. It’s never inferred. |
judgement | The judging ladder’s verdict, reached before the card was accepted. See Agents & oversight. |
decision_trail | Who decided the card, when, which way and why (see below). |
GetActionQueue returns the pending queue by default. Pass
status="Approved" or status="Rejected" to read back decided cards, and use
workflow_type to filter on type.
Money: one headline, disjoint buckets
The Twins dashboard headline is a sum of ActionCard.transaction_value across cards.
A sum can’t tell what the money is for, so FACE enforces a rule instead: two cards
must never charge the same dollars.
A card contributes to the headline only if all three of these hold:
- It has not been rejected. A rejection is a decision not to take the saving. Approved and pending cards both count.
- It carries a dollar value. An emissions card sets its value in kilograms of CO2e, so it contributes nothing to a money total.
- It is in a named charging bucket. A test (
cmd/evidence_dollar_buckets_test.go) lists the closed set of buckets allowed to charge and says why each one owns its money. A new producer that charges without being added to that list fails the build. Adding it is where someone has to write down whose dollars they are.
The charging buckets
Four evidence-document kinds charge:
| Kind | The money it owns |
|---|---|
OSD_REPORT | The cargo itself: units short plus units damaged, at the cited unit price. This is a loss already incurred. |
FNOL | The reserve on a newly reported loss, only for claim numbers no claims card already charges. |
RATE_CON_ACCESSORIAL_AUDIT | Accessorials invoiced above the signed rate confirmation. |
MSA_SLA_COMPLIANCE_AUDIT | Service credits owed back under an SLA clause. |
Five twin-card families charge:
| Card family | The money it owns |
|---|---|
cold_chain_card | Value at risk of the load on a reefer whose own readings broke a stated band. |
claims_card | The reserve on an open claim. For billing alerts, the invoice amount due on an account. |
customs_clearance_card | Demurrage or per-diem accrued by a hold. Never the duty and never the cargo. |
importer_of_record_card | Duty and tax with no accountable party. |
cross_border_trucking_card | Detention accrued at a crossing. |
Why other documents keep their money out of totals
Most evidence documents are about money another bucket already owns. A cargo loss survey, a notice of intent, a statement of position and a subrogation demand all concern the same cargo the OS&D report charges. A temperature excursion and a warranty audit concern the same load the cold-chain card charges. If they charged too, one loss would be counted several times, and the error would look like better news.
So those documents state their money in claimed_amount (what the document asks
for) or exposure_amount (value at risk, not yet lost), and no total reads those
fields. The reader still sees the figure; the headline just doesn’t add it up twice.
Savings summary
GetSavingsSummary returns two separate figures:
total_savings_usd: the pipeline. It counts savings on cards nobody has rejected.approved_savings_usd: the subset the operator has committed to. It’s always less than or equal to the total.
Neither figure is “realised.” Nothing persists whether an approved action was carried out in the world, so the cockpit must not label either figure that way.
Deciding a card
ExecuteAction is the one place a person decides a card:
- The decider is identified from the verified session. An approval is refused unless the session token’s subject can be verified. A rejection is recorded whatever the attribution says.
- A reason is required on rejection and optional on approval. Whitespace-only counts as empty. The limit is 2000 characters, and a longer reason is refused rather than truncated.
- On approval, the evidence document is sealed (see Sealing), and any executable artifacts run.
- The response reports what happened:
decision_recorded: the decision was durably written.success: false only when an approval executed nothing.transaction_id: minted only on success.not_executed: tokens such asemail:no_google_connector,calendar:not_implemented,erp:not_implementedorbilling:charge_failed, one for each artifact that didn’t run.
How a client should read the response. decision_recorded=true with
success=false means the card is decided but nothing was executable. The client
should stop, and must not retry. Genuine failures arrive as gRPC status errors
instead.
The decision trail
ActionCard.decision_trail is a list of ActionDecision entries, and the last entry
is the current decision. Each entry records:
decision:ApprovedorRejected.decided_at: the server’s clock when the decision was recorded.decided_by: the verifiedsubclaim of the deciding session, never a value the caller supplied.attribution(DeciderAttribution): how well the decider could be named.decided_by_role: the verified role.reason.
A retried decision with the same content doesn’t add a new entry. The original entry is kept, including its original time. An operator who changes their mind gets a second entry.
What this does not establish
- The headline is a sum of claims about money, not money received. It’s only as good as the cited records behind each card.
decided_bydoesn’t prove a human pressed a button. A person’s token driven by a script looks the same.decided_by_roleseparates a service account from a person’s session.- The trail is a verified attribution, not a signature. It is written by the instance into a store the instance owns.
- A required reason proves someone typed something, not that it’s true or complete.
- An approval is not an execution receipt. Read
transaction_idandnot_executed.