Skip to content

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

FieldMeaning
typeThe 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, statusstatus is Approved or Rejected once decided. Empty means pending.
transaction_valueThe card’s contribution to the dollar headline (see below).
Artifact fieldsOne typed payload per artifact family: route_card, email_draft, calendar_invite, erp_update, claims_card, cold_chain_card, customs_clearance_card, and others.
evidence_documentOne 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.
judgementThe judging ladder’s verdict, reached before the card was accepted. See Agents & oversight.
decision_trailWho 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:

KindThe money it owns
OSD_REPORTThe cargo itself: units short plus units damaged, at the cited unit price. This is a loss already incurred.
FNOLThe reserve on a newly reported loss, only for claim numbers no claims card already charges.
RATE_CON_ACCESSORIAL_AUDITAccessorials invoiced above the signed rate confirmation.
MSA_SLA_COMPLIANCE_AUDITService credits owed back under an SLA clause.

Five twin-card families charge:

Card familyThe money it owns
cold_chain_cardValue at risk of the load on a reefer whose own readings broke a stated band.
claims_cardThe reserve on an open claim. For billing alerts, the invoice amount due on an account.
customs_clearance_cardDemurrage or per-diem accrued by a hold. Never the duty and never the cargo.
importer_of_record_cardDuty and tax with no accountable party.
cross_border_trucking_cardDetention 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:

  1. 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.
  2. 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.
  3. On approval, the evidence document is sealed (see Sealing), and any executable artifacts run.
  4. 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 as email:no_google_connector, calendar:not_implemented, erp:not_implemented or billing: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: Approved or Rejected.
  • decided_at: the server’s clock when the decision was recorded.
  • decided_by: the verified sub claim 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_by doesn’t prove a human pressed a button. A person’s token driven by a script looks the same. decided_by_role separates 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_id and not_executed.