Skip to content

Guides

Set Policies

Author policy rules — attribute matches, IP allowlists, method/endpoint restrictions, rate limits, time windows, operation and parameter rules, and approvals — from the dashboard or the CLI, plus non-stored per-request rules via an SDK constrained client.

Policy changes require alter CLI 0.10.0 or newer. An older CLI receives HTTP 428 with upgrade guidance. For an already-open dashboard tab, copy any unsaved draft before reloading the page, then review the change again. These checks prevent stale writes; they cannot be bypassed by retrying an old client.

Creating, editing, enabling, disabling, or deleting a runtime policy shows a review. Select Review, check its target and effect, then explicitly save. The review leads with What this change does: one plain sentence per change (for example, “The limit changes from 100 to 1000 requests per day”) and, where the rule’s meaning settles it, a rating of More restrictive, Less restrictive, or Mixed. Content rules, AI review, approver changes, and timezone changes are rated Impact uncertain rather than guessed. Any less-restrictive change carries a warning, not only disabling or deleting. The complete before/after definitions are under View full definitions. Editing a disabled rule does not enable it. Removing a restriction never overrides other policies or grants access by itself.

A concurrent edit makes your reviewed version stale; the save changes nothing. Refresh the current rule and review your intended change again. If a save’s response is lost, retrying the unchanged review uses the same retry key and does not create a second rule or success audit. Changing the draft requires a new review.

Every CLI example in this guide creates a rule, which prints its review and proceeds. Scripted updates and deletions require --yes and the explicitly reviewed --expected-version; see change safety and retries.

By the end of this guide you can restrict what an agent or connection is allowed to do: deny requests by attribute, confine traffic to an IP range or an endpoint allowlist, cap its request rate, confine it to business hours, allow a provider operation only when its parameters look right (or redact fields / force a fresh sign-in), and hold sensitive calls for human approval — every rule type the policy engine supports.

A policy is a narrowing-only layer on top of a default-deny base. A request already needs ownership of the connection, the right scope, and a valid credential before any rule runs. A rule can only further restrict that base — creating one never grants or widens access. When a rule matches, it denies the request, rate-limits it, holds it for approval, or redacts part of it.

One feature, several labels: the developer portal calls this surface Runtime policies (every app, provider, agent, and grant page has a Runtime policies section with an Add runtime policy button), the Wallet has a Limits page and a Limits tab on each connection, the CLI is alter policy, and this documentation says policy or policy rule. They all author the same stored rules, evaluated by the same engine.

SurfaceUse it for
Dashboard (developer portal)Create, inspect, edit, toggle, simulate, and delete stored organization, application, provider, agent, and connection rules.
WalletEnd users manage account-wide and connection-specific rules and inspect disclosure-safe inherited controls.
CLI (alter policy)Scriptable application, provider, agent, and connection rules for CI and infrastructure-as-code. Organization rules stay dashboard-only; account rules stay Wallet-authored.
SDK constrained clientA non-stored rule carried by every credential request made through the returned with_constraints / withConstraints sub-client.
Connect popup (end users)While authorizing an OAuth connection or delegating existing managed-secret access to an agent, the end user can set usage limits in the popup — the same self-scoped, narrowing-only rules available later in the Wallet. On by default; an application can hide the step with the Connect-session option (allowUserPolicyRules in the TypeScript SDK, allow_user_policy_rules in the Python SDK).

Stored rules attach at six levels — organization, application, provider, agent, connection (grant), and account (user). End users can add connection rules in the Wallet or during Connect authorization. An SDK constrained-client rule can narrow calls without storing the rule. A request is evaluated against every applicable rule across the delegation chain, and a denial at any level wins.

For Git-reviewed changes, use policy files: portable definitions, explicit environment bindings, before/after tests and a saved plan. Code-owned rules identify their manager in the viewer and cannot be edited, disabled or deleted interactively. Release ownership through a reviewed plan to return a rule to dashboard management without removing its restriction.

Select a policy anywhere it appears to open the shared detail viewer. Owned rules expand the complete definition: effect, target and source, operations, every parameter/operator/value condition, approval configuration, status, and timestamps. The developer portal’s organization-level Runtime policies page indexes all six stored levels — organization, application, provider, agent, connection, and account. Account (user) rules are authored by the end user in the Wallet and shown read-only (disclosure-limited) in the portal; the Wallet’s Limits page separates the editable account-wide and connection-specific rules the end user owns from disclosure-safe inherited developer controls.

For a composed view, open a connection’s Limits tab in the Wallet: it groups the effective rules into Your account-wide limits, Company and app restrictions, Your limits for this connection, provider and agent restrictions, and restrictions from the connection this was shared from. In the developer portal, the grant drawer’s Effective runtime policy chain lists the same rules with organization and application as separate groups. Explain a request tests a request shape, opens each visible rule’s definition, and links to its actual owner, including an inherited connection.

  • An app. App-wide policies can be authored before any agents or connections exist. A narrower target must exist before you attach a rule to it.
  • For the CLI, an authenticated alter session (see the CLI reference).

The portal’s Create a restriction action opens an authoring panel over the page you are on, so the list, simulator or drawer behind it stays as you left it. Choose a target, then fill in the panel and select Review; on a wide screen, a summary of what the policy does stays beside its definition. Every Add runtime policy and Edit action opens the same panel. Wallet personal limits use the same panel; Connect contains the builder in a compact island. Existing restrictions remain available to inspect, enable, disable or delete through Manage existing restrictions.

Search the catalog or choose a goal: Require approval, Restrict access, Limit usage, or Control content. AI risk review · JEV is a policy option alongside deterministic restrictions and human approval, not an AI authoring assistant. Only types supported by your surface appear. The panel starts with Applies to, the target the policy is created for. When creating a policy for an application in the portal (the entire application, a provider, an agent or a connection), Change returns to choosing a target, starting from the current one, and asks first if the draft has changed. Organization policies, Wallet, Connect and any existing policy have a fixed target; to apply one elsewhere, create a new policy there. Name and purpose follow and are optional. After choosing a type, its definition appears as numbered sections, required ones first. Sections marked Optional stay collapsed until you select Add. Collections are paginated; edit one condition, time window or review question at a time. Guided and JSON edit the same draft. When a new policy can be tried against the application before saving, Try an example appears at the end of the panel. If a value is invalid when you select Review, the builder opens the section that holds it. Closing the panel with unsaved changes asks before discarding them.

Guided and JSON are two views of one draft. JSON contains only the body, not the target, rule type, name, or enabled state. Switching views neither saves nor requires a valid definition. If JSON is incomplete, its exact text stays visible until you repair it; stale guided fields never replace it. Guided collections preserve individual literal values: a comma inside an item is not a separator. Unsupported shapes remain available in JSON rather than being overwritten. Full validation runs before examples, review and saving. Leaving a changed draft asks for confirmation.

An app with no connections can still save a complete policy. Testing an example request is optional and requires a real applicable connection; an empty connection picker does not mean the policy allows access. Editing a disabled rule keeps it disabled.

In Connect, Review does not save or stage anything. Add to connection stages the reviewed personal limit; it takes effect only when the user approves the connection. A failed staging request leaves the draft open for retry or cancel, and connection approval remains blocked while an unsaved draft is open.

Goal: allow at most 1,000 requests per hour, counted per caller principal (the agent when the request runs as one, otherwise the connection).

Dashboard

Open the agent (or app), go to its Runtime policies section, and click Add runtime policy. Choose Limit request usage. In Usage limit, enter Maximum requests 1000 and Per calendar period hour. Optionally enter a Name at the top of the panel, then select Review and Save restriction. The new rule card shows the 1000 / hour badge.

CLI

Terminal window
echo '{"limit": 1000, "period": "hour"}' | \
alter policy rules create --type quota --agent <agent-id> \
--body - --name "1000/hour per agent"

Over the limit, the call is denied with a retry-after until the window resets (top of the minute/hour, UTC midnight, or the first of the month). This is the only rule type that responds with a rate-limit signal rather than a plain denial — the SDKs surface it as QuotaExceededError (see errors) and never auto-retry it.

Confine access to business hours (time window)

Section titled “Confine access to business hours (time window)”

Goal: allow requests only Mon–Fri, 09:00–17:00, US Eastern.

Dashboard

In Runtime policies → Add runtime policy, choose Set allowed hours. In Timezone, enter America/New_York. In Time windows, add an item, select mon–fri, and set From 09:00 and Until 17:00. Select Review, then Save restriction.

CLI

Terminal window
echo '{
"windows": [{"days": ["mon","tue","wed","thu","fri"], "start": "09:00", "end": "17:00"}],
"timezone": "America/New_York"
}' | alter policy rules create --type time_window --agent <agent-id> \
--body - --name "Business hours"

The request is denied unless its local time (in the required timezone) falls inside a window. A window is [start, end) — start inclusive, end exclusive; use "24:00" for end-of-day. A start later than end is a valid overnight window (e.g. fri 22:00 → sat 06:00). Up to 20 windows per rule; the timezone is required.

Goal: make an application read-only — deny every POST, PUT, PATCH, and DELETE, app-wide.

Dashboard

On the app’s Policies page, click Create a restriction, keep Entire application as the target, and click Continue with this target. Choose Block matching requests. In Conditions, add a request condition for method, choose Match any of several values, and add POST, PUT, PATCH, and DELETE as four separate items. Under Try an example, select Show and review the app-wide OAuth-provider, managed-secret, active-grant, and agent counts. If a representative connection exists, optionally select it and click Test unsaved runtime policy. The trace marks the candidate and is cleared if you edit the draft. Then select Review and Save restriction; a connection is not required to save.

CLI

Terminal window
echo '{"when": {"method": ["POST", "PUT", "PATCH", "DELETE"]}, "effect": "deny"}' | \
alter policy rules create --body - --name "Read-only app"

A match rule denies when ALL of its conditions match; a condition value is an exact string or a list (membership). Conditions can name request metadata (method, provider_id, agent_id, client_ip, …) or the classified operation vocabulary — operation (a catalog operation id) and family (send, read, write, delete, admin, payment) — so {"when": {"family": ["payment"]}, "effect": "deny"} blocks everything that moves money. Two fail-closed behaviors to know: a method condition also gates raw-token retrievals (a raw token confers every method), and an operation/family condition also fires on requests that cannot be classified. client_ip is an exact-IP match — for ranges, use an IP allowlist rule instead.

One operation can belong to several families, and a family condition matches if ANY of them is listed. Many provider APIs multiplex on the request body: the same endpoint that edits a resource also deletes it, selected by a field rather than by the HTTP method. Notion trashes a page by PATCHing it with {"in_trash": true}; a Git-host commit endpoint deletes files through its actions[]; a search index’s alias endpoint removes aliases through its body. Those operations therefore carry both write and delete, so {"when": {"family": ["delete"]}, "effect": "deny"} blocks them — including a routine edit through the same endpoint. That is deliberate: the alternative is a deny an agent can dodge by expressing the delete through the update call. When an agent legitimately needs to edit but never delete on such an endpoint, allow it by operation (the specific catalog operation id) rather than widening the family rule.

Confine traffic to an IP range (IP allowlist)

Section titled “Confine traffic to an IP range (IP allowlist)”

Goal: only accept requests originating from the office egress range.

Dashboard

In Runtime policies → Add runtime policy, choose Restrict request origins. In Networks, add one item per address or CIDR range — for example 203.0.113.5 and 10.0.0.0/8. Select Review, then Save restriction.

CLI

Terminal window
echo '{"allow": ["203.0.113.5", "10.0.0.0/8"]}' | \
alter policy rules create --type ip_allowlist --body - --name "Office egress only"

Requests from any source IP not on the list are denied. A /0 catch-all — or a set of entries that together cover the whole address space — is rejected at authoring: an allowlist that matches every IP is not a restriction. Multiple IP allowlist rules at different levels compose by intersection (a request must satisfy all of them), and a request whose source IP cannot be determined is denied, never waved through. This is an operator rule type (dashboard/CLI); end users cannot author it from the Wallet.

Goal: confine a connection to read-only repository access.

Dashboard

In Runtime policies → Add runtime policy, choose Allow only selected requests. Add GET as an item in Methods and /repos/** as an item in Endpoints. Select Review, then Save restriction.

CLI

Terminal window
echo '{"allowed_methods": ["GET"], "allowed_endpoints": ["/repos/**"]}' | \
alter policy rules create --type restriction --grant <grant-id> \
--body - --name "Read-only repos"

A request is denied unless its method is in allowed_methods (when set) AND its provider API path matches allowed_endpoints (when set) — at least one of the two must be present. An endpoint is an exact path (/user) or a trailing /** prefix wildcard (/repos/** matches /repos, /repos/, and everything under it); no other wildcard form exists. Exact entries are byte-exact including the trailing slash — /user does not admit /user/; list both forms, or use a /** wildcard, when both should pass. Non-canonical alias spellings (duplicate slashes, . segments, backslashes, percent-encoded separators) are denied outright under an endpoint allowlist — their provider-side resolution is ambiguous — and are rejected when authoring patterns. Restrictions are enforced on proxied calls, so a connection carrying one refuses raw-token retrieval — a raw token could not be held to the allowlist after handoff. Like the IP allowlist, this is an operator rule type.

Restrict a specific operation (content rule)

Section titled “Restrict a specific operation (content rule)”

Content rules bind reviewed provider operations — or whole semantic families (send, read, write, delete, admin, payment) — from the operation catalog, optionally narrowed by conditions over the request’s parameters. They have three effects: deny, redact (strip named fields before the call), or step_up (require a recent end-user sign-in).

Content rules apply only to providers with an operation catalog. A custom managed secret has no catalog, so its traffic can never classify — a content rule that reaches it denies every request rather than ignoring it. Use a method/endpoint restriction to scope what a custom API’s traffic may do.

Deny by parameter — outbound email to internal recipients only

Section titled “Deny by parameter — outbound email to internal recipients only”

Goal: allow Gmail send only when every recipient is @example.com.

Dashboard

Open Create a restriction, choose the target, click Continue with this target, then choose Control operations and content. In Operations, choose Browse operation catalog, select Google, and add gmail.users.messages.send from the catalog. Conditions is optional, so select Add on it first; then add an item with Parameter name recipients and Operator not all within; use a list of values and add *@example.com as one item. In Response, keep Deny the request. Select Review, then Save restriction.

CLI

Terminal window
echo '{
"match": {"operations": ["gmail.users.messages.send"]},
"params": [{"name": "recipients", "op": "not_subset_of", "value": ["*@example.com"]}],
"effect": "deny"
}' | alter policy rules create --type content_match --provider google \
--body - --name "Internal recipients only"

Condition operators are equals, any_in (fires if any value matches), not_subset_of (fires when values are NOT fully covered by the allowlist — the recipient-allowlist shape), and the numeric gt/gte/lt/lte. List values support per-entry globs (*@example.com). A few providers canonicalize a path parameter so that equivalent spellings of the same key match one rule (SAP S/4HANA, where '0000500001' and '500001' are the same sales order). A glob cannot be compared against a canonicalized value, so a pattern on one of those parameters is rejected when the rule is saved — use equals, or any_in with the exact key values.

A condition’s name can reference a parameter from the operation’s catalog entry (like recipients above) or any query-string parameter the request carries — for example visibility on GitHub’s list-repositories operation or limit on Slack’s list-users operation. Query parameters are evaluated identically whether the caller embeds them in the request URL or passes them through the SDK’s separate query-parameters argument, and the policy simulator projects a simulated URL’s query string the same way. The numeric operators (gt/gte/lt/lte) require a cataloged numeric parameter; an ad-hoc query value compares as a string, so a numeric condition on it denies. Ambiguous spellings fail closed: when an uncataloged query key is repeated with different values, or a query key shadows a differently-sourced cataloged parameter, any condition naming that parameter denies rather than guessing which value the provider would honor — conditions on other parameters are unaffected. A cataloged scalar query parameter repeated with different values cannot be projected at all, so every parameter condition on that operation denies.

String comparisons are normalized: equals and the any_in/not_subset_of pattern matching compare case-insensitively with surrounding whitespace trimmed (internal whitespace is significant). This is deliberate anti-bypass behavior — a rule denying blocked also denies BLOCKED and blocked (a padded variant), so a condition cannot be dodged by changing case or padding. Number-typed parameters compare numerically (a string-typed parameter compares as normalized text). On a set-shaped parameter (for example recipients), equals matches only an exact single value — a multi-member set never matches; use any_in to match a member.

Swap the effect to strip fields before the call is forwarded (the call is refused if the redaction cannot be provably applied):

Terminal window
echo '{
"match": {"operations": ["gmail.users.messages.send"]},
"effect": "redact",
"redact": {"fields": ["subject"]}
}' | alter policy rules create --type content_match --provider google \
--body - --name "Strip subject on sends"
Terminal window
echo '{
"match": {"operations": ["gmail.users.messages.send"]},
"effect": "step_up",
"step_up": {"max_session_age_seconds": 300}
}' | alter policy rules create --type content_match --provider google \
--body - --name "Recent sign-in to send"

Hold sensitive calls for a human (require approval)

Section titled “Hold sensitive calls for a human (require approval)”

Goal: pause every write for an approver to review.

Terminal window
echo '{
"effect": "require_approval",
"when": {"method": ["POST", "DELETE"]},
"approval": {"approvers": ["lead@example.com"], "channels": ["email"]}
}' | alter policy rules create --type require_approval --agent <agent-id> \
--body - --name "Approve writes"

In the dashboard choose Require human approval on the same Add runtime policy form. The stored rule type remains require_approval; the detail viewer also calls this human in the loop (HITL).

The same gate also has a second, simpler authoring surface: the grant editor’s Human-in-the-loop approval block (CLI: --grant-policy on alter managed-secrets grants create / update). That block is the grant’s baseline — one unconditional requirement on every call through that grant, replaced as a whole each time the grant’s settings are saved — while a require_approval rule is named, optionally conditional, and can sit above one grant (agent-, provider-, or app-wide). Use the block for “every call on this credential needs a human”; use a rule for “only these calls need a human”. Both appear in the policy viewer, and authoring both for the same condition produces two gates. The end-to-end approval flow — how the request pauses, how the application receives the outcome, and how the two surfaces interact — is covered in Add Human-in-the-Loop Approvals.

One rule produces one approval gate and currently designates the first address in its approver list. For multi-party N-of-N approval, author distinct applicable rules with distinct approver sets; Alter combines them into separate gates and requires every gate to approve.

The when condition also accepts the content-rule vocabulary — operations/families plus parameter conditions — so approval can hinge on what the request carries, not just its shape:

Terminal window
# Only payments over 1,000 need sign-off
echo '{
"effect": "require_approval",
"when": {
"match": {"families": ["payment"]},
"params": [{"name": "amount", "op": "gt", "value": 1000}]
},
"approval": {"approvers": ["cfo@example.com"]}
}' | alter policy rules create --type require_approval --body - --name "Approve large payments"

Omit when entirely to require approval on every request at the target. An operation-scoped approval condition on a request that cannot be classified is denied — it is never silently approved around.

An ai_review rule can add a human approval step to an otherwise permitted, classified proxy call. It never runs on reads and never weakens deterministic rules. A timeout or invalid vendor answer leaves the deterministic decision in place, with a skip reason in the request audit.

Before authoring a rule, an organization admin must turn on AI request review consent in Settings → Runtime policies. The plan must include AI review. The platform switch and mode must also be enabled by the operator after the published sub-processor notice period. Consent can be withdrawn from the same page; new requests then stop going to TypeSafe.

Terminal window
echo '{
"effect": "require_approval",
"review": {"threshold_level": 3, "families": ["payment", "delete"],
"triggers": [{"id": "large_refund", "label": "Large refund",
"question": "Is this refund unusually large for the account?"}]},
"approval": {"approvers": ["lead@example.com"]}
}' | alter policy rules create --type ai_review --body - --name "Review risky calls"

Threshold levels are 1–4; tail_mass and confidence_floor optionally tune the probability cutoff (both 0–1). At most eight triggers may be added. Each trigger has a unique lowercase ID, a question of 10–500 characters, an optional label shown to approvers, and an optional probability threshold in (0, 1]. Choose AI risk review · JEV in the dashboard to edit the same fields. The stored type remains ai_review; this runtime reviewer is separate from assisted policy drafting.

Every applicable rule — at every level, across the whole delegation chain — is evaluated on every request, and they compose as AND: a request must satisfy all of them, and a denial at any level wins. There is no rule ordering to reason about and no way for one rule to override another, because no rule can widen access — each one only narrows. So a stack like:

  • app-wide: time_window (business hours)
  • agent: quota (1,000/hour)
  • provider: content_match (internal recipients only)
  • connection: require_approval (writes need sign-off)

means a request must be inside the window AND under the quota AND pass the content conditions, and a matching write still waits for its approval. Multiple windows inside ONE time-window rule are a union (any window admits); multiple time-window rules across levels intersect (all must admit).

Dry-run a request against the live rules without making a real call:

Terminal window
alter policy simulate --grant <grant-id> \
--operation gmail.users.messages.send \
--params '{"recipients": ["someone@example.com"]}'

The output shows the verdict, the per-rule trace, and which operation the request classified to. The dashboard’s Explain a request shows the same evidence: a block, required approval, fields to redact, recent sign-in, unsimulated AI review, or incomplete request context. These are policy checks, not authorization to execute. Missing API-key identity, client IP, or endpoint is identified instead of being presented as a reassuring no-match. Supply API-key identity with the CLI’s --api-key-id or the dashboard’s API-key selector, using the key ID, never its secret. No request is executed, approval created, quota consumed, or model called.

To test an unsaved app-wide rule, put the complete create object in a file and add --candidate. The rule is validated and overlaid in memory for this one request; it is marked candidate in the trace and is never created. A candidate with enabled set to false is not overlaid and gets no trace row, matching the fact that a disabled rule is never enforced once saved:

Terminal window
cat > candidate-rule.json <<'JSON'
{"rule_type":"json_match","rule_body":{"when":{"method":"POST"},"effect":"deny"},"name":"Block POST"}
JSON
alter policy simulate --grant <grant-id> --method POST \
--candidate @candidate-rule.json

One simulation is one scenario, not proof over every possible request shape. Test the methods, endpoints, agents, IPs, times, operations, and parameters that matter for the rule before you save it.

Attach a non-stored rule with a constrained SDK client

Section titled “Attach a non-stored rule with a constrained SDK client”

Stored rules are the durable controls. with_constraints / withConstraints returns a sibling client whose credential requests all carry the additional rule. Use that sibling for constrained calls and retain the original client for calls that should not carry the rule:

from alter_sdk import content_match_rule
# Require a recent end-user sign-in for calls through this sibling:
fresh = app.with_constraints(
rule=content_match_rule(
operations=["gmail.users.messages.send"],
effect="step_up",
max_session_age_seconds=300,
),
)
await fresh.proxy_request(
"POST",
provider_url,
grant_id=grant_id,
reason="Send a reviewed message",
)
import { contentMatchRule } from "@alter-ai/alter-sdk";
const fresh = app.withConstraints({
rule: contentMatchRule({
operations: ["gmail.users.messages.send"],
effect: "step_up",
maxSessionAgeSeconds: 300,
}),
});
await fresh.proxyRequest({
method: "POST",
url: providerUrl,
grantId,
reason: "Send a reviewed message",
});

The builders validate the rule locally (effect-specific requirements included) before it is sent.

Terminal window
alter policy show-app # app-level rules at a glance
alter policy rules list --agent <agent-id> # list rules at a level
alter policy rules update --rule <rule-id> --disable # toggle without deleting
alter policy rules delete --rule <rule-id> # remove

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.