Actions & verbs
An action is any call HESO captures and gates. Every action carries exactly one of seven verbs, recorded in an ActionDetail.
What is an action
An action is one thing your agent attempts: a model completion, a tool call, an outbound HTTP request, a payment. HESO captures it before it runs, so policy can decide on it and the decision can be signed into a receipt.
Capturing first is what makes a decision possible. Once the action exists as a record, policy can allow it, block it, redact a field, or route it to a human. Only an allowed action proceeds, and the verdict lands in the Action Receipt. One action in, one receipt out.
Two things on every action drive the decision: the verb (what kind of action it is) and the fields map (its arguments). Both live in the ActionDetail, the structured record HESO builds at capture time.
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 a verb (or on "any"), and the verb is the authoritative lane every security decision keys on.
| 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. |
Dangerous lanes
Four verbs are dangerous lanes: payment, delete, account_change, and data_export. Each carries a built-in floor. Your policy can tighten a floored lane but can never bypass it: a rule that tries to blanket-allow a delete past its floor fails when the policy loads.
The other three verbs — llm_call, tool_call, http_request — have no floor. They are governed entirely by the rules you write. See policy & decisions for how a verb maps to an allow, block, redact, or require_approval.
The ActionDetail record
ActionDetail is what 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. - workflowstringrequired
- The named workflow the action belongs to, e.g.
vendor-payouts. The grouping key an auditor reconstructs a session from. - accountstringrequired
- The account, tenant, or principal the action ran on behalf of.
- fieldsRecord<string,string>required
- The action’s arguments, post-redaction — any field marked for redaction is already a commitment hash or removed before this map is built. See the fields map.
- target_hoststring
- The outbound host the action reaches, e.g.
api.stripe.com. Omitted forllm_calland other internal verbs with no remote host. Policy scope matches against this. - result_hashstring
- A BLAKE3 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 runs
def 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 map 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 value — alongside the verb and target_host, these are the inputs a rule matches on.
[[rule]]
id = "big-payout"
verb = "payment"
# reads action.fields.amount_usd off the captured action
conditions = [
{ field = "amount_usd", op = "gt", value = 5000, display = "amount over $5,000" },
]
decision = "require_approval"
approvers = ["finance"]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.
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.