Skip to main content

Squillify development is paused indefinitely. Read why, and discover what this experiment achieved. Read the story

MicroSkills Reference

Retry and Dead-Letter Planner

workflow.retry_dead_letter_plan

ProductionBounded custom (Class B)Deterministicstrictfastv1.0.0
No network

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

Note
Classify a batch of failures as retry or dead-letter and compute each retry delay from a declarative backoff policy.

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

IdempotentRetry safeStreaming supportedFile input supportedRequires external service

Capabilities

data-transformation

Cost & billing

Flat rate per invocation

MetercallsBillable onsuccessCost per call1 credit

File handling

Accepts file_refProduces file_refMax input1 MB

How we handle your data

Sensitive output possible
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.

Read the Microskill Data Handling Policy

Input schema

FieldTypeRequiredDescription
failuresarrayYesFailed jobs to classify, 1 through 1000 closed objects with unique ids, attempt counts and last error codes.
policyobjectYesClosed retry rules: maxAttempts, backoff, baseDelayMs, maxDelayMs plus optional multiplier, jitter, jitterPercent, retryableCodes and terminalCodes.
seedstringNoRequired unless jitter is not DETERMINISTIC; up to 256 characters seeding deterministic jitter.

Output schema

FieldTypeRequiredDescription
actionsarrayYesOne closed decision per failure in the order you sent them, 1 to 1000 entries.
summaryobjectYesClosed batch counts: total, retry, deadLetter, terminalCode, notRetryable and attemptsExhausted.
policyobjectYesPolicy actually applied, with omitted multiplier, jitter and jitterPercent filled in so a stored plan can be recomputed.
_metaobjectYesAlways present on success, with delaysCapped and delaysFloored.

API contract

POST /v1/execute
curl
curl -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}}}'
Integrations
TypeScript SDK
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

CodeHTTPRetryableDescription
VALIDATION_ERROR400NoThe request payload failed schema validation.
UNAUTHORIZED401NoMissing or invalid API key.
SKILL_NOT_ACCESSIBLE403NoThe API key is not granted access to this skill.
SKILL_NOT_AVAILABLE_FOR_PLAN403NoThis skill is not included in your current plan.
PLAN_ELIGIBILITY_UNAVAILABLE503YesPlan eligibility could not be decided. Retry after backoff.
SKILL_NOT_FOUND404NoThe requested skill or version does not exist.
TENANT_RATE_LIMITED429YesRequest rate limit exceeded. Retry after backoff.
QUOTA_EXCEEDED429YesPlan quota exhausted. Retryable once the quota period resets.
EXECUTION_TIMEOUT504YesExecution exceeded the allowed time budget.
RETRY_POLICY_INVALID400NoThe request payload failed this skill's input validation.
RETRY_FAILURE_LIMIT_EXCEEDED429NoA size, quota, rate, or time limit was exceeded.
RETRY_SEED_REQUIRED400NoThe request payload failed this skill's input validation.
RETRY_DELAY_OVERFLOW429NoA size, quota, rate, or time limit was exceeded.

Other limits

LimitValue
Recommended inline bytes32 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)

Request bodyjson
{
  "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

Inputjson
{
  "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"
    }
  ]
}
Expected outputjson
{
  "_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

Inputjson
{
  "policy": {
    "maxDelayMs": 60000,
    "backoff": "FIXED",
    "baseDelayMs": 5000,
    "maxAttempts": 4
  },
  "failures": [
    {
      "id": "webhook-42",
      "errorCode": "RATE_LIMITED",
      "retryable": true,
      "attempt": 1,
      "retryAfterMs": 30000
    }
  ]
}
Expected outputjson
{
  "_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
  }
}
  • 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

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.

Tags

workflowretryresiliencevalidation