Decorator: Narrate With Three Lines
The @observe decorator already gives you a run per call, the inputs, and the return value recorded as the agent's response. This walkthrough adds the three things a reader needs on top of that: what phases it went through, what it decided, and what it concluded.
Start: the plain decorated agent
import waxell_observe as waxell
from waxell_observe import observe
waxell.init()
@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict]) -> str:
past_due = [a for a in accounts if a["days_past_due"] > 30]
return f"{len(past_due)} accounts past due; drafted recovery for {len(past_due)}"
Open the run. You'll see a run named dunning-officer, its inputs, and the returned string as agent_response. Good — but the middle is a black box.
Step 1 — name the phases
waxell.step() works inside a decorated function because the decorator has activated a context. Each call becomes a step span the UI groups under the run and lists on the Timeline tab.
@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict]) -> str:
waxell.step("read billing", output={"accounts": len(accounts)})
past_due = [a for a in accounts if a["days_past_due"] > 30]
waxell.step("classify", output={"past_due": len(past_due)})
return f"{len(past_due)} accounts past due; drafted recovery for {len(past_due)}"
Step 2 — record the decision
Decisions are where a reader most wants a why. Ask for the context (waxell_ctx is injected when your function declares it) and record the options, the choice, and the reasoning:
@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict], waxell_ctx=None) -> str:
waxell.step("read billing", output={"accounts": len(accounts)})
past_due = [a for a in accounts if a["days_past_due"] > 30]
waxell.step("classify", output={"past_due": len(past_due)})
action = "escalate" if len(past_due) > 5 else "soft-reminder"
waxell_ctx.record_decision(
name="recovery_action",
options=["soft-reminder", "escalate"],
chosen=action,
reasoning=f"{len(past_due)} past-due accounts; threshold is 5",
)
return f"{len(past_due)} accounts past due; action={action}"
Step 3 — set an explicit result
The decorator already records your return value as the response. set_result is for the structured conclusion — the thing dashboards, delivery, and the narrator should read rather than parse out of a sentence:
waxell_ctx.set_result({
"summary": f"{len(past_due)} past due; action={action}",
"past_due": len(past_due),
"action": action,
})
return f"{len(past_due)} accounts past due; action={action}"
What the run looks like now
dunning-officer ✓ 0.4s
step read billing accounts=12
step classify past_due=3
decision recovery_action → soft-reminder (3 past-due; threshold 5)
result {summary: "3 past due; action=soft-reminder", ...}
response "3 accounts past due; action=soft-reminder"
A reader gets the whole arc from the run header without opening a single span — and the AI narrator now has the agent's own account to summarize instead of guessing from span names.
Don't forget the failure path
If the read fails, don't just return early — that leaves a run with no explanation. Record the failure so it shows as a red span:
if accounts is None:
waxell_ctx.record_tool_call(name="billing.read", status="error",
error="no admin token", tool_type="api")
waxell_ctx.set_result({"summary": "could not read billing (no admin token)"})
return "could not read billing"
Full pattern in Tell the Story of a Run → Recording what degraded.