What is an action
An action is a single thing your agent attempts: one model completion, one tool call, one outbound HTTP request, one payment. HESO captures the action before it runs. That way policy can decide on it — allow it, block it, redact a field, or route it to a human — and the decision can be signed into a receipt.
Every captured action is described by an ActionDetail: a small, structured record of what the agent is about to do, to which host, under which workflow and account, with which arguments. That record is the input policy reads and the payload that ends up inside the Action Receipt. One action in, one receipt out.
Two things label every action and drive the decision: the verb (what kind of action it is) and the fields map (its arguments). The rest of the loop — policy, approval, redaction, signing — operates on this record.
The seven verbs
The verb is the broad category an action falls into. There are exactly seven, written in lowercase snake_case. Policy rules match on the verb (or on "any"), and several of them cover risky actions — payments, deletes, account changes, large data exports — that carry a pinned floor the engine enforces at load time.
| Verb | What it is |
|---|---|
llm_call | A model or chat completion. |
tool_call | A tool or function invocation. |
http_request | A raw outbound HTTP call. |
payment | Moving money. |
data_export | Exporting or reading out data. |
account_change | Changing an account or config. |
delete | A destructive removal. |
The ActionDetail record
ActionDetail is the record HESO captures for every action. It sits under content.action in the receipt. The fields below are exactly what a captured action carries — nothing is invented at sign time.
- verbVerbrequired
- One of the seven verbs. The broad category policy matches on.
- tool_namestringrequired
- The specific operation, e.g.
stripe.transfers.create. Identifies what ran, beyond the verb. - target_hoststring
- The outbound host the action reaches, e.g.
api.stripe.com. Omitted forllm_calland other internal actions that have no remote host. Policy scope matches against this. - workflowstringrequired
- The named workflow the action belongs to, e.g.
vendor-payouts. A rulesubjectcan scope to a workflow. - accountstringrequired
- The account the action runs under. A rule
subjectcan scope to an account. - fieldsRecord<string,string>required
- The action’s arguments, post-redaction — any field marked for redaction is already a hash or removed before this map is built. See the fields map.
- result_hashstring
- A hash of the action’s result, when one is recorded. Pins the output bytes without storing them.
- errorstring
- An error string, when the action failed rather than returning a result.
A captured action for an allowed vendor payment looks like this:
{ "verb": "payment", "tool_name": "stripe.transfers.create", "target_host": "api.stripe.com", "workflow": "vendor-payouts", "account": "acct_19", "fields": { "amount_usd": "4200", "payee": "Globex LLC", "member_id": "blake3:7d1a…" }}
How actions are captured
In Python, the SDK captures the action before it runs. You wrap a function with a decorator, or wrap a client with the proxy, and HESO builds the ActionDetail from the call it intercepts. The gate decision happens on that record; only then does the underlying call proceed.
Capture does two things automatically:
- Infers the verb from the call. A
createon an LLM client becomesllm_call; arequeston an HTTP client becomeshttp_request. - Turns kwargs into fields. The call’s keyword arguments become the
fieldsmap, so policy can read them by name.
from heso import tool @tool # captured before the body runsdef create_invoice(account: str, amount_usd: int): # verb inferred from the name → tool_call # kwargs become fields: {"account": ..., "amount_usd": "..."} return billing.create(account=account, amount_usd=amount_usd)
You can also gate a whole client at once with heso.wrap, which captures its create and request calls as actions in place. The full decorator and proxy surface lives in the Python SDK reference.
The fields map
fields is a flat Record<string,string>of the action’s arguments — and it is the post-redaction map. By the time policy reads it, anything marked for redaction is already a commitment hash or removed, so secrets never sit in plaintext in the record that gets signed and shipped.
This map is what policy conditions evaluate. A condition names a field, picks an operator, and compares against a typed value — alongside the verb and target_host, these are the inputs a rule matches on.
[[rule]]id = "pay-cap"verb = "payment"# reads action.fields.amount_usd off the captured actionconditions = [ { field = "amount_usd", op = "lte", value = 5000, display = "at most $5,000" },]decision = "allow"
Because the map is built post-redaction, a condition reads the redacted value, not the original — so policy can match on the presence or shape of a field without the cloud ever seeing its secret. See conditions & operators for every operator the engine evaluates and how values are typed.
The ActionDetail captured here is exactly what lands inside the signed Action Receipt under content.action. The receipt’s action_hash is computed over the canonical bytes of that content, so the verb and fields you see captured are the same bytes anyone can verify later.
