Core concepts
Policies
Inspect tool calls and block, require approval, redact, or log — before side effects happen.
Agents decide what to do; policies decide whether it is allowed to happen. A policy inspects every tool call before it executes and applies one of four effects: block it, pause for human approval, redact its input from traces, or just log a note. Because policies run in the tool broker — between the agent's request and the tool's execute — they stop side effects before they occur, not after.
Policies are plain, declarative objects. You attach them to a workflow, and every tool call in that run is evaluated against them, with each decision recorded in RunState.policyDecisions (see runtime).
Defining a policy#
Create a policy with policy({...}). It needs a non-empty name and at least one rule; the call throws an AppError otherwise.
import { policy } from "@assemble-dev/sdk";
export const refundGuard = policy({
name: "refund-guard",
appliesToToolNames: ["issueRefund"],
block: ({ input }) => false,
logOnly: () => "refund tool was called",
});Rule signature#
Every rule receives the same context and returns a boolean or a string:
type ToolPolicyContext = {
toolName: string;
agentName: string;
input: JsonValue;
};
type ToolPolicyRule = (context: ToolPolicyContext) => boolean | string;- Return
false— the rule does not trigger. - Return
true— the rule triggers with a generated reason (for example"blocked by policy refund-guard"). - Return a
string— the rule triggers and the string becomes the recorded reason. Prefer this: the reason surfaces in traces, the approvals inbox, and error messages.
Warning: If a rule throws, the tool call is blocked with a reason of the form
blocked by policy <name>: <effect> rule threw: <message>. A buggy rule fails closed, not open.
Scoping with appliesToToolNames#
appliesToToolNames limits a policy to specific tools. Omit it and the policy is evaluated for every tool call in the workflow. Scope tightly — a policy named refund-approval that silently evaluates your search tool is a debugging surprise waiting to happen.
The four effects#
A policy can define any combination of the four rules. Within a single policy they are checked in this order — block, requireApproval, redact, logOnly — and the first rule that triggers decides that policy's action.
block#
Stops the tool call entirely. The call is recorded as blocked in RunState.toolCalls, and the workflow throws an AppError (code FORBIDDEN), failing the run. Use it for hard limits that no human should override at runtime.
export const capRefunds = policy({
name: "cap-refunds",
appliesToToolNames: ["issueRefund"],
block: ({ input }) => {
const refund = RefundInputSchema.parse(input);
return refund.amount > 10_000 ? `refund of $${refund.amount} exceeds the hard cap` : false;
},
});requireApproval#
Pauses the run before the tool executes. run returns status: "awaiting_approval" with an approvalId, the partial RunState is persisted, and the tool only executes after a human approves. See human approval for the resume flow. Use it for reversible-but-expensive actions where a human in the loop is cheaper than a mistake.
redact#
Lets the tool execute normally but masks the flagged input before it is traced or uploaded to the platform. Use it when the call is fine but the payload contains data — card numbers, PII — that must not land in trace files or your workspace. This composes with per-tool sensitiveFieldNames (see tools) and the platform-level redaction described in security.
logOnly#
Records a policy decision with your reason and changes nothing else. The tool executes as usual. Use it to observe how often a rule would fire before promoting it to block or requireApproval — a dry run for policy changes.
Attaching policies to a workflow#
Pass policies on the workflow definition. They apply to every tool call the run makes:
import { workflow } from "@assemble-dev/sdk";
import { z } from "zod";
export const supportWorkflow = workflow({
name: "supervised-support",
inputSchema: z.object({ ticket: z.string().min(1) }),
outputSchema: z.object({ reply: z.string() }),
agents: [classifier, refundSpecialist],
tools: [issueRefund],
policies: [refundApprovalPolicy],
});For a tool to actually execute during a run, wire it to an agent with agentToolBindings — see Tools. Policies evaluate whenever the bound tool is invoked.
When multiple policies apply to the same call, each is evaluated and the strongest action wins: block beats requireApproval, which beats redact, then logOnly, then allow. Every individual decision is still recorded in RunState.policyDecisions, so you can see which policies fired even when a stronger one won.
Worked example: refund approval#
Auto-approve small refunds, pause for a human on large ones:
import { policy } from "@assemble-dev/sdk";
import { z } from "zod";
const RefundInputSchema = z.object({ amount: z.number().positive() });
export const refundApprovalPolicy = policy({
name: "refund-approval",
appliesToToolNames: ["issueRefund"],
requireApproval: ({ input }) => {
const refund = RefundInputSchema.parse(input);
if (refund.amount <= 100) {
return false;
}
return `refund of $${refund.amount} exceeds the $100 auto-approval limit`;
},
});A run with { amount: 42 } completes without interruption. A run with { amount: 250 } returns status: "awaiting_approval", and the reason string above is exactly what the approver sees in the dashboard inbox.
Parse input with Zod inside the rule rather than trusting its shape — the rule receives the raw tool input as JsonValue, and a parse failure blocks the call instead of letting it through.
Effects reference#
| Effect | Tool executes? | Run outcome | Recorded as | Use when |
|---|---|---|---|---|
block | No | Run fails with AppError | toolCalls status blocked + policy decision | The action must never happen. |
requireApproval | Only after approval | status: "awaiting_approval", resumable | Pending entry in approvals + policy decision | A human should decide first. |
redact | Yes | Unchanged | Policy decision; input masked in traces | Payload contains sensitive data. |
logOnly | Yes | Unchanged | Policy decision only | You want visibility, not enforcement. |
Where to go next#
- Follow the pause-and-resume flow in human approval.
- See how decisions and redacted payloads appear in tracing.
- Review platform-level redaction defaults in security.