Retry and Dead-Letter Planner
workflow.retry_dead_letter_plan
Overview
You send a batch of failures and a retry policy, and we tell you what to do with each one: retry it after a computed delay, or stop and dead-letter it. The decision order is fixed — a terminal error code wins, then a failure you marked non-retryable, then an exhausted attempt budget, and everything else is retried. Delays come from your backoff settings, optionally jittered from a seed you supply, so the same batch always produces the same plan. We compute the plan only: nothing is enqueued, retried, slept on, or written anywhere, and we never look at your queue or the clock.
Retry logic is written once per service and drifts: one worker gives up too early, another hammers a dead upstream, and nobody can explain why a job landed in the dead-letter queue. Putting the decision and the arithmetic in one place makes the policy explicit and the outcome reproducible.
Reliability
Fully deterministic: the same failures, policy, and seed always give the same plan, because there is no clock and no randomness. Jitter, when you ask for it, comes from SHA-256 over your seed, the failure id, and the attempt number. Failures, none of which are worth retrying unchanged: RETRY_POLICY_INVALID means the policy contradicts itself — maxDelayMs below baseDelayMs, jitterPercent without DETERMINISTIC jitter, or a multiplier other than 1 on a non-EXPONENTIAL backoff — so fix the policy and resend. RETRY_FAILURE_LIMIT_EXCEEDED means you sent more than 1000 failures, so split the batch. RETRY_SEED_REQUIRED means you asked for DETERMINISTIC jitter without a seed, so supply a stable seed such as the queue name or the batch id. RETRY_DELAY_OVERFLOW means the un-capped delay for one attempt is larger than a 64-bit millisecond value can hold, which needs a huge multiplier and base delay together, so lower one of them.
Context footprint
Returns one small object per failure plus batch counts, so a 1000-failure batch stays a few hundred kilobytes. A successful call costs 1 execution credit.
When to use it
When you have failures and a policy, and you want one auditable answer per failure: retry after this many milliseconds, or stop here and why.
When you need the retry to actually happen, when retryability depends on inspecting live upstream state, or when a single hard-coded sleep is enough.
Scope
Non-goals
- Does not enqueue, retry, sleep, or dead-letter anything
- Does not read or write your queue, database, or job state
- Does not invent a timestamp — delays are offsets, not wall-clock times
- Does not decide whether an error is retryable for you
- Does not generate random jitter; jitter is derived from your seed
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content and may return submitted, transformed, extracted, or reformatted content.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response may contain submitted or transformed content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| failures | array | Yes | Failed jobs to classify, 1 through 1000 closed objects with unique ids, attempt counts and last error codes. |
| policy | object | Yes | Closed retry rules: maxAttempts, backoff, baseDelayMs, maxDelayMs plus optional multiplier, jitter, jitterPercent, retryableCodes and terminalCodes. |
| seed | string | No | Required unless jitter is not DETERMINISTIC; up to 256 characters seeding deterministic jitter. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| actions | array | Yes | One closed decision per failure in the order you sent them, 1 to 1000 entries. |
| summary | object | Yes | Closed batch counts: total, retry, deadLetter, terminalCode, notRetryable and attemptsExhausted. |
| policy | object | Yes | Policy actually applied, with omitted multiplier, jitter and jitterPercent filled in so a stored plan can be recomputed. |
| _meta | object | Yes | Always present on success, with delaysCapped and delaysFloored. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"workflow.retry_dead_letter_plan","input":{"failures":[{"id":"job-1","attempt":2,"errorCode":"UPSTREAM_TIMEOUT","retryable":true}],"policy":{"maxAttempts":5,"backoff":"EXPONENTIAL","baseDelayMs":1000,"maxDelayMs":60000}}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("workflow.retry_dead_letter_plan", {"policy":{"backoff":"EXPONENTIAL","terminalCodes":["SCHEMA_INVALID"],"baseDelayMs":1000,"maxDelayMs":60000,"maxAttempts":5,"multiplier":2},"failures":[{"attempt":2,"errorCode":"UPSTREAM_TIMEOUT","retryable":true,"id":"job-1"},{"attempt":1,"errorCode":"SCHEMA_INVALID","retryable":true,"id":"job-2"},{"attempt":5,"errorCode":"UPSTREAM_TIMEOUT","retryable":true,"id":"job-3"}]});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| RETRY_POLICY_INVALID | 400 | No | The request payload failed this skill's input validation. |
| RETRY_FAILURE_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| RETRY_SEED_REQUIRED | 400 | No | The request payload failed this skill's input validation. |
| RETRY_DELAY_OVERFLOW | 429 | No | A size, quota, rate, or time limit was exceeded. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not enqueue, retry, sleep, or dead-letter anything
- Does not read or write your queue, database, or job state
- Does not invent a timestamp — delays are offsets, not wall-clock times
- Does not decide whether an error is retryable for you
- Does not generate random jitter; jitter is derived from your seed
- Executing the retries themselves
- Deciding retryability from the error text
- Scheduling a single job with no policy — a fixed sleep is simpler
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "workflow.retry_dead_letter_plan",
"input": {
"policy": {
"backoff": "EXPONENTIAL",
"terminalCodes": [
"SCHEMA_INVALID"
],
"baseDelayMs": 1000,
"maxDelayMs": 60000,
"maxAttempts": 5,
"multiplier": 2
},
"failures": [
{
"attempt": 2,
"errorCode": "UPSTREAM_TIMEOUT",
"retryable": true,
"id": "job-1"
},
{
"attempt": 1,
"errorCode": "SCHEMA_INVALID",
"retryable": true,
"id": "job-2"
},
{
"attempt": 5,
"errorCode": "UPSTREAM_TIMEOUT",
"retryable": true,
"id": "job-3"
}
]
}
}Examples
Exponential backoff across a mixed batch: one retry, one terminal code, one exhausted budget
{
"policy": {
"backoff": "EXPONENTIAL",
"terminalCodes": [
"SCHEMA_INVALID"
],
"baseDelayMs": 1000,
"maxDelayMs": 60000,
"maxAttempts": 5,
"multiplier": 2
},
"failures": [
{
"attempt": 2,
"errorCode": "UPSTREAM_TIMEOUT",
"retryable": true,
"id": "job-1"
},
{
"attempt": 1,
"errorCode": "SCHEMA_INVALID",
"retryable": true,
"id": "job-2"
},
{
"attempt": 5,
"errorCode": "UPSTREAM_TIMEOUT",
"retryable": true,
"id": "job-3"
}
]
}{
"_meta": {
"delaysFloored": 0,
"delaysCapped": 0
},
"actions": [
{
"id": "job-1",
"reasonCode": "RETRY_SCHEDULED",
"action": "RETRY",
"nextAttempt": 3,
"delayMs": 2000,
"retryAtOffsetMs": 3000
},
{
"id": "job-2",
"reasonCode": "TERMINAL_CODE",
"action": "DEAD_LETTER",
"nextAttempt": 0,
"delayMs": 0,
"retryAtOffsetMs": 0
},
{
"id": "job-3",
"reasonCode": "ATTEMPTS_EXHAUSTED",
"action": "DEAD_LETTER",
"nextAttempt": 0,
"delayMs": 0,
"retryAtOffsetMs": 0
}
],
"summary": {
"total": 3,
"retry": 1,
"deadLetter": 2,
"terminalCode": 1,
"notRetryable": 0,
"attemptsExhausted": 1
},
"policy": {
"maxAttempts": 5,
"backoff": "EXPONENTIAL",
"baseDelayMs": 1000,
"maxDelayMs": 60000,
"multiplier": 2,
"jitter": "NONE",
"jitterPercent": 0
}
}Fixed backoff honouring a server Retry-After floor
{
"policy": {
"maxDelayMs": 60000,
"backoff": "FIXED",
"baseDelayMs": 5000,
"maxAttempts": 4
},
"failures": [
{
"id": "webhook-42",
"errorCode": "RATE_LIMITED",
"retryable": true,
"attempt": 1,
"retryAfterMs": 30000
}
]
}{
"_meta": {
"delaysFloored": 1,
"delaysCapped": 0
},
"actions": [
{
"id": "webhook-42",
"reasonCode": "RETRY_SCHEDULED",
"action": "RETRY",
"nextAttempt": 2,
"delayMs": 30000,
"retryAtOffsetMs": 30000
}
],
"summary": {
"total": 1,
"retry": 1,
"deadLetter": 0,
"terminalCode": 0,
"notRetryable": 0,
"attemptsExhausted": 0
},
"policy": {
"maxAttempts": 4,
"backoff": "FIXED",
"baseDelayMs": 5000,
"maxDelayMs": 60000,
"multiplier": 1,
"jitter": "NONE",
"jitterPercent": 0
}
}Recommended use cases
- Turn a batch of failed jobs into a retry schedule your worker can act on
- Apply one shared, auditable retry policy across several services
- Spread retries with reproducible jitter so a whole batch does not wake up together
Avoid for
- Executing the retries themselves
- Deciding retryability from the error text
- Scheduling a single job with no policy — a fixed sleep is simpler
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.