When the limit is hit
What a caller sees when a shared impact limit refuses a governed action: the status, the body, the effect, and what an operator can do next.
A shared limit is reserved immediately before dispatch. When it has no room, the call is refused before anything is sent. This page shows what comes back.
The responses below come from Anlyon’s CI test runs.
What the caller sees
The HTTP status is 200. The refusal is in the body, not in the status code. A caller that checks the status code alone will miss it. Read status and grade.
The scenario: a limit of 5 emails per day. One key sends to 3 recipients. A second key then asks for 3 more, with 2 left.
const { data } = await agent.actions.invoke('send-email', {
emailId: 'welcome-2',
to: ['a@example.com', 'b@example.com', 'c@example.com'],
subject: 'Welcome',
});
if (data!.grade === 'refused') {
console.log(data!.status, data!.error);
}result = agent.actions.invoke(
"send-email",
{"emailId": "welcome-2", "to": ["a@example.com", "b@example.com", "c@example.com"], "subject": "Welcome"},
)
if result.data["grade"] == "refused":
print(result.data["status"], result.data["error"])curl -s -X POST https://api.anlyon.com/api/v2/actions/send-email/invoke \
-H "Authorization: Bearer $ANLYON_AGENT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-2" \
-d '{ "input": { "emailId": "welcome-2", "to": ["a@example.com", "b@example.com", "c@example.com"], "subject": "Welcome" } }'{
"status": "failed",
"grade": "refused",
"error": "Dispatch refused (impact_limit_exceeded): Not enough allowance: daily emails needs 3, has 2."
}
The error names impact_limit_exceeded and lists every limit that fell short, with what the call needed and what was available. A call under several limits reports each shortfall in the same sentence.
The same sequence was repeated with a second API key and then with a caller that has no key. Neither brought a new allowance.
The effect
The refused call still leaves an effect. Read it with the effectId from the response.
const { data: effect } = await agent.actions.effect(data!.effectId!);
console.log(effect!.stage, effect!.outcome, effect!.rejectionReason, effect!.attempts.length);
const { data: refused } = await agent.actions.effects({ grade: 'refused', action: 'send-email' });effect = agent.actions.effect(result.data["effectId"]).data
print(effect["stage"], effect["outcome"], effect["rejectionReason"], len(effect["attempts"]))
refused = agent.actions.effects(grade="refused", action="send-email").datacurl -s https://api.anlyon.com/api/v2/actions/effects/eff_01JABCDEF \
-H "Authorization: Bearer $ANLYON_AGENT_KEY"
curl -s "https://api.anlyon.com/api/v2/actions/effects?grade=refused&action=send-email" \
-H "Authorization: Bearer $ANLYON_AGENT_KEY"{
"stage": "rejected",
"outcome": "not_dispatched",
"rejectionReason": "impact_limit_exceeded",
"grade": "refused",
"verification": "not_dispatched",
"attempts": []
}
Zero attempts means nothing was sent to the provider. The effect’s evidence has an entry that begins “Not dispatched”. It carries the shortfalls in its details.
From the CLI: anlyon effects list --action send-email --grade refused and anlyon effects get <id>. An invoke that is refused exits 14.
The allowance is untouched
Reservation is all or nothing. If one applicable limit has no room, none is reserved. The refused call above asked for 3 with 2 left. It did not take the 2.
const { data: limit } = await agent.impactLimits.get('lim_01JABCDEF');
console.log(limit!.consumed, limit!.reserved, limit!.available);limit = agent.impact_limits.get("lim_01JABCDEF").data
print(limit["consumed"], limit["reserved"], limit["available"])curl -s https://api.anlyon.com/api/v2/impact-limits/lim_01JABCDEF \
-H "Authorization: Bearer $ANLYON_AGENT_KEY"A third call, to 2 recipients, then succeeded. The limit read:
{ "consumed": 5, "reserved": 0, "available": 0 }
Three calls were made and the destination saw two writes.
Reading limits needs impact-limits:read, which is in the default set. An agent can check available before it asks.
impact_unbounded
A second refusal has the same shape and a different reason. It happens when a limit applies to the action and the call gives no evaluable amount for that limit’s dimension.
For a declared action that means the amount expression reads an input the caller left out. count(input.to) with no to has nothing to count. No quantity is unbounded, never zero.
{
"stage": "rejected",
"outcome": "not_dispatched",
"rejectionReason": "impact_unbounded",
"grade": "refused",
"attempts": []
}
The invocation comes back status: "failed" with an error that names impact_unbounded. The error text has this form:
Limit <limit id> (<limit name>) caps <dimension>, and this operation gives no exact quantity or upper bound for it.
The limit is untouched:
{ "consumed": 0, "reserved": 0 }
Two details from the same test run:
- With no applicable limit the call runs. Before the
emailslimit existed, the same input succeeded and the effect recorded an emptyimpact. A limit in another unit did not concern it. - The same action with a countable input runs under the same limit. A call with one recipient succeeded and consumed 1.
The fix is in the definition. Mark the input required in the action’s schema when the amount depends on it.
Held allowance
available is limit - consumed - reserved. reserved includes every effect whose outcome is still pending or unknown. That capacity stays held until evidence or an operator settles it. A period boundary does not release it.
In a test run a refund of 8000 against a daily limit of 10000 lost its response. The reservation was then dated to the previous day. On the new day the limit read:
{ "consumed": 0, "reserved": 8000, "unknownExposure": 8000, "available": 2000 }
A new call for 3000 was refused with impact_limit_exceeded. After reconciliation found the refund, the 8000 settled into the day it was reserved in and the current day read:
{ "consumed": 0, "reserved": 0, "available": 10000 }
So a limit can refuse a call while consumed is zero. Look at reserved and unknownExposure. unknownExposure is the part of reserved whose outcome is unknown or possibly partial. It is not an additional amount.
A limit that fills after review
A limit is checked at dispatch, not at review. An approval does not hold capacity. If the limit fills while an invocation waits for its approval, approving it dispatches nothing.
{ "rejectionReason": "impact_limit_exceeded" }
In a test run the preview showed available: 10000 and sufficient: true at review. Another refund of 5000 ran before the approval. The approval call succeeded and its invocation.status was failed. The allowances in a preview are as of review.
What an operator can do
Raise the limit. This needs impact-limits:write. No key receives that scope by default, so the agent a limit constrains cannot raise it.
await operator.impactLimits.update('lim_01JABCDEF', { amount: 10 });operator.impact_limits.update("lim_01JABCDEF", amount=10)curl -s -X PATCH https://api.anlyon.com/api/v2/impact-limits/lim_01JABCDEF \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 10 }'A limit’s name and amount can change. Its dimension, scope and period are fixed. Lowering a limit never releases existing exposure. It blocks new work until availability recovers.
Wait for the period. A day limit is the UTC day and a month limit is the UTC month. consumed starts again at zero in the next period. A total limit has one period and does not reset. Held exposure carries over, as shown above.
Resolve held exposure. If reserved is what fills the limit, settle the effects behind it. List them with effects({ unresolved: true }), then reconcile or resolve each one. See When the outcome is unknown.
Then invoke again. A refused effect is final. It does not run later when room appears. Send a new invocation with a new Idempotency-Key. The same operationKey with the same request returns the refused effect, so use a new one of those as well.
How it behaves
- Governed means an adapter or a declaration. A governed action draws from the limit. Keep the provider credential 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 money examples on this page are
stripe.refund. - The amount of a declared action is the expression you wrote. For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. Set an
upper_boundat the most one call can change. - A limit bounds quantity. Five emails fit a limit of five, whoever receives them.
- An operator holding
impact-limits:writecan raise, lower or archive a limit. Grant that scope with care. - The refusal record is a database record.

