Core concepts /Actions & verbs

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.

VerbWhat it is
llm_callA model or chat completion.
tool_callA tool or function invocation.
http_requestA raw outbound HTTP call.
paymentMoving money.
data_exportExporting or reading out data.
account_changeChanging an account or config.
deleteA 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 for llm_call and 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:

action.jsonjson
{
  "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 create on an LLM client becomes llm_call; a request on an HTTP client becomes http_request.
  • Turns kwargs into fields. The call’s keyword arguments become the fields map, 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.

heso.tomltoml
[[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 captured action is what gets signed

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.

Next steps