heso
Learn more

Approvals

Holding a risky action until a person says yes.

Some actions are too risky to run on the agent’s say-so. When one of your gate rules says an action needs a person, heso does not run it and does not refuse it either. It holds the action and waits for a reviewer. The gate returns suspended (or raises SuspendedError), and nothing happens until someone approves or denies it.

What you get back

A held action is one of the three things the gate can tell you, alongside allowed and blocked. When an action is held, you do not get a result yet. You get a signal that a person is in the loop, and your code decides what to show the user while it waits.

  • allowed: the action passed policy and ran. You have the result and a signed receipt.
  • blocked: the action failed policy. It never ran.
  • suspended: the action needs a person. It is held until a reviewer clears or denies it.

The durable way: @heso.gated

In Python the @heso.gated decorator makes a held action durable. The first time you call a gated function that needs approval, heso parks it and returns heso.SUSPENDED. The body does not run. Later, you call the very same function again with the same session, and that second call is the resume: if an approver has said yes, the body runs once and you get its result.

import heso

heso.init()  # reads heso.toml + your operator key

@heso.gated(sla="1d", on_timeout="deny")
def refund(order_id: str, amount: int) -> str:
    # runs only after a person approves
    return stripe_refund(order_id, amount)

# first call: the policy says this needs a person.
# heso parks it and returns the suspended sentinel.
result = refund("ord_123", 5000, _heso_session="refund-ord_123")
if result is heso.SUSPENDED:
    show_user("Sent for approval, hang tight.")

# ...time passes, an approver says yes...

# same function, same session = the resume.
# the body fires once, you get the real result.
result = refund("ord_123", 5000, _heso_session="refund-ord_123")
if result is not heso.SUSPENDED:
    print("refunded:", result)

Same call in, same call out

There is no separate “resume” function to learn. Re-invoking the gated function with the same session is the resume. heso tracks the lifecycle for you, so the body fires exactly once even if you retry the call many times.

Prefer an exception over a sentinel? Pass raise_on_suspend=True and a parked call raises heso.Paused instead of returning heso.SUSPENDED. Either way the body stays unfired until a person clears it.

How a hold gets cleared

When heso parks an action it notifies the reviewers on the account. The approver opens the held action, sees what the agent wanted to do, and signs off with their own device. That signature is what clears the hold. Because the approval is signed by a person, the receipt ends up carrying two signatures, so it shows who approved, not just that the agent acted.

  1. The gate parks the action
    Policy says this action needs a person. heso holds it and tells you suspended. The reviewers get notified.
  2. A reviewer decides
    The approver opens the held action and approves or denies it. They sign with their own device, so the decision is theirs, not the cloud’s. heso never signs an approval for anyone.
  3. The same call runs once
    On approval, the next time you make the same call the action runs a single time and you get its result. On a denial, the action stays unfired.

An approved action is L1

An ordinary allowed action is signed by your operator key alone, which is L0. When a person clears a held action, the resulting receipt is signed by your operator key plus the approver, which is L1. So a receipt’s trust level tells you at a glance whether a human was in the loop. See Gate for how the gate itself works.

If nobody answers

A hold does not wait forever. The sla sets how long heso waits, and on_timeout decides what happens if the window closes with no decision. The safe default is deny: an action nobody approves in time is treated as denied and never runs.

Most holds need a single approver. If an action needs more than one person to sign off, heso counts distinct approvers and clears the hold once enough have signed. You do not wire any of that yourself; it is set by policy.

Limits

An approval clears one specific action, identified by exactly what the agent was about to do. If the agent changes the action while it waits, the prior approval does not carry over: the new action is a new request that needs its own sign-off. This is on purpose, so a person can never be tricked into approving one thing and having a different thing run.

Next

Gate

The surface that decides allowed, blocked, or held, and the only thing that can stop an action.

Python quickstart

Install heso, write your first rule, and watch an action get held for approval.