Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

Impact limits

Shared caps on money, resource mutations or any unit you declare. Every agent, session and key draws from the same limit, reserved before dispatch.

Budgets cap what Anlyon does for you, per key. Impact limits cap what your agents do to the world. A limit counts one of three things:

dimension What it counts unit
money Integer minor units in one currency The currency, for example usd
resource_mutations Changes to resources resource_mutations
A unit you declare, such as emails, messages or rows Whatever the action’s impact.amount evaluates to The dimension name

Limits apply to governed actions. That means an action with an adapter, or an action that carries a declaration. See Actions for the fields.

A limit is keyed on things the server controls: the environment, the dimension and unit, an optional action name, an optional resource prefix, and a period (day, month or total). A day is the UTC day. Nothing in that key belongs to the caller. A new session, a new API key or a child agent does not get a new allowance.

await operator.impactLimits.create({
  name: 'Refunds per day', dimension: 'money', currency: 'usd', period: 'day', amount: 50_000, // $500.00
});
await operator.impactLimits.create({
  name: 'Fixture edits', dimension: 'resource_mutations', period: 'day', amount: 20,
  resourcePrefix: 'github:acme/fixtures@',
});
await operator.impactLimits.create({
  name: 'Daily emails', dimension: 'emails', period: 'day', amount: 500,
});

const { data } = await agent.impactLimits.list();
// [{ limit, unit, consumed, reserved, unknownExposure, available, ... }]
operator.impact_limits.create(name="Refunds per day", dimension="money", currency="usd", period="day", amount=50_000)
operator.impact_limits.create(
    name="Fixture edits", dimension="resource_mutations", period="day", amount=20,
    resource_prefix="github:acme/fixtures@",
)
operator.impact_limits.create(name="Daily emails", dimension="emails", period="day", amount=500)

limits = agent.impact_limits.list().data

A limit in a declared unit reads back with that unit and no currency:

{ "dimension": "emails", "unit": "emails", "currency": null, "limit": 5, "available": 5 }

Creating and changing limits needs impact-limits:write. No key receives it by default, so the agent a limit constrains cannot raise it. Reading limits needs impact-limits:read, which is in the default set.

Where the quantity comes from

Action Quantity
stripe.refund The refund amount in the action’s one currency, plus one resource mutation.
github.file_update One resource mutation per file update.
resend.email_send One emails unit per recipient, counted across to, cc and bcc.
A declared action The action’s impact.amount, evaluated over the validated input before dispatch.

A caller cannot pass a quantity apart from the request. For a declared action the quantity is whatever the declared expression gives for that input.

An amount that reads an optional input the caller left out gives no quantity. That effect is unbounded for the dimension. A limit that applies refuses it with impact_unbounded. With no applicable limit it runs.

How capacity moves

  1. Reserve immediately before dispatch, after any approval. Every applicable limit is reserved in one transaction. If one has no room, none is reserved and nothing is sent.
  2. Settle the confirmed impact exactly once, into the period the reservation was made in, even if the outcome arrives next month.
  3. Release what is known not to have happened, and nothing else.
  4. Hold everything else. Pending, unknown and possibly partial failures keep their capacity, across restarts and period boundaries, until evidence or an operator settles them.

reserved includes that held exposure. unknownExposure is the part of it whose outcome is unknown or possibly partial. It is not an additional amount. available is limit - consumed - reserved.

Repeated attempts of one effect (a replay, a duplicate submission of the same operation) never consume twice. Repeated legitimate changes to the same resource are separate operations and count separately.

Lowering a limit never releases existing exposure. It blocks new work until availability recovers. Archiving a limit stops it applying to new effects while its reservations still settle.

When the limit has no room

An operation is admitted whole or refused whole. A send to four recipients with two left is refused, none of the four is sent, and the two stay available.

A refused effect records why, and nothing reaches the provider:

{ "stage": "rejected", "outcome": "not_dispatched", "rejectionReason": "impact_limit_exceeded", "grade": "refused" }

The invocation’s status is failed, its grade is refused, and its error names impact_limit_exceeded. See When the limit is hit.

How it behaves

  • A limit counts governed actions. A governed action has an adapter or a declaration. Give an action a declaration to put it under a limit.
  • A limit counts the calls routed through Anlyon. Keep the provider key in the vault so the action is the path to the provider.
  • A limit belongs to one environment and scope. Each environment has its own.
  • The amount of a declared action is your expression. For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. An upper_bound set too low is an error in the definition.
  • An effect with no quantity runs when no limit applies to it. It is refused under a limit that applies, and runs otherwise.
  • A limit can be changed. An operator holding impact-limits:write can raise, lower or archive it.
  • A limit and a budget count different things. An impact limit caps what an action changes, such as the money it moves. A per-key budget caps the Anlyon operations a key may perform.

Was this page helpful?