Skip to content
Français

Pause when a quota is reached

Save a workflow after a terminal quota error and resume when access is available.

Set onQuota on the workflow’s start() method when you want to preserve progress after a terminal quota error. With a checkpoint configured, the affected task pauses so it can resume later.

import { defineTask, OutpostError } from "@elie-laloum/outpost";

export let calls = 0;
export const review = defineTask({
  key: "review",
  perform: () => {
    if (++calls === 1)
      throw new OutpostError("quota", "You've hit your session limit", {
        resetAt: new Date(Date.now() + 1_000).toISOString(),
      });
    return "reviewed";
  },
});
export function reviewCount() {
  return calls;
}
import {
  createWorkflowCheckpointStore,
  createLocalTransport,
} from "@elie-laloum/outpost";

export const checkpoint = {
  store: createWorkflowCheckpointStore({
    transporter: createLocalTransport({ directory: ".outpost/storage" }),
  }),
  runId: "nightly-2026-09-28",
  version: "1",
};
import { reportValue } from "./reporter.ts";
import { defineWorkflow } from "@elie-laloum/outpost";
import { review } from "./quota-review.ts";
import { checkpoint } from "./quota-checkpoint.ts";

export const result = await defineWorkflow("nightly", [review]).start({
  checkpoint,
  onQuota: { action: "pause", maxWaitMs: 6 * 60 * 60_000 },
});
result.unwrap();
reportValue(result.value(review));
// Example output: reviewed

It prints reviewed: the simulated limit resets after one second, within maxWaitMs, so the workflow waits and runs the task again.

onQuota needs a checkpoint to hold the pause. With the default maxWaitMs of 0, nothing waits in the process: every pause is durable.

Agents and model providers reject with an OutpostError of code quota:

SourceSignalReset time
Claude CodeRejected rate_limit_event, rate_limit or billing_error, limit textFrom resetsAt
CodexusageLimitExceeded or rateLimitExceeded, usage-limit textUnknown
Copilot CLIsession.error of type quota or rate_limit, limit textUnknown
Kimi CodeQuota, balance or rate-limit textUnknown
AntigravityRESOURCE_EXHAUSTED or quota textUnknown
Model providersHTTP 429, rate-limit or insufficient_quota stream errorFrom Retry-After

A CLI signal counts only when the agent process fails. Retry notices are not quotas. quotaFault(error) reads the message and resetAt of a caught quota error, even when wrapped.

A fallback agent switches agents instead of waiting: the task pauses only when every candidate hits a limit.

Drag to move · Ctrl + scroll to zoom
100 %
  • PauseThe attempt that hit the limit ends.
    1. Keep the retriesThe error does not consume retry attempts.
    2. Save the pauseThe task becomes paused with a quota record, and the checkpoint is saved.
    (Steps)
    • → Wait : then
  • WaitOnly when the reset time is known.
    1. Wait in the processA reset within maxWaitMs emits a quota event with status: "waiting", then runs the task again.
    2. Pause durablyOtherwise the task stays paused. Independent tasks continue, dependent tasks wait, and start() returns paused.
    (Steps)
    • → Resume : then
  • ResumeA later start() with the same checkpoint.
    1. Run againAn unknown or past reset runs the task at once.
    2. Wait firstA reset within maxWaitMs is awaited, then the task runs.
    3. Stay pausedA later reset leaves the task paused without calling the agent.
    (Steps)

The paused record in result.tasks holds quota.resetAt: schedule the next start() from it. onQuota authorizes the rerun, without resume: "retry-incomplete". A loop task resumes the phase of the round that hit the limit.

The first attempt after a pause receives context.quota: the captured conversation and the retained work branch.

Task or callNext attempt
defineAgentTask()Continues the conversation in your sandbox and workspace.
defineIsolatedTask()Continues it in a new sandbox, on the same branch; an integrated workspace starts from the interrupted branch.
defineInteractiveAgentTask()Continues the conversation of the interrupted turn.
defineQueuedTask()Publishes a new job, <key>:quota:<attempt>, with the original idempotencyKey.
speculate()In a durable race, reruns the candidates a limit stopped, in new conversations.

A continued turn sends a short resume instruction instead of the brief. Set quotaResume: "restart" on an agent or isolated task to send the original request again.

Claude Code, Codex, Copilot CLI and Kimi Code can continue. A fallback agent restarts from its first candidate with the original brief.

A worker stores a handler’s quota error in QueueResult.quota, and defineQueuedTask() rejects with code quota. Pass the conversation through the task input:

import { defineQueuedTask } from "@elie-laloum/outpost";
import type { TaskQueue } from "@elie-laloum/outpost";

function implement(queue: TaskQueue) {
  return defineQueuedTask({
    key: "implement",
    queue,
    handler: "implement",
    input: (context) => ({ continueFrom: context.quota?.conversation ?? null }),
    decode: String,
  });
}

The handler then dispatches with continuation: { id: input.continueFrom }. Job queues covers workers and idempotency keys.

When limits stop candidates and none wins, speculate() returns status quota with the earliest known reset. Throw it from a task to pause the workflow:

import { OutpostError, speculate, defineTask } from "@elie-laloum/outpost";
import type { SpeculationOptions } from "@elie-laloum/outpost";

function race(options: SpeculationOptions) {
  return defineTask({
    key: "race",
    async perform() {
      const result = await speculate(options);
      if (result.status === "quota" && result.quota)
        throw new OutpostError("quota", result.quota.message, {
          ...(result.quota.resetAt ? { resetAt: result.quota.resetAt } : {}),
        });
      return result.winner?.branch ?? null;
    },
  });
}

A durable race then reruns only those candidates, with cumulative budgets. Without durability, every candidate runs again.

  • start() rejects onQuota without a checkpoint.
  • Each rerun counts against budget.attempts. The workflow timeoutMs also ends waits, and a cancelled wait leaves the task paused.
  • Reset times written in the agent’s text are not parsed; they stay in the message.
  • Other agents, disabled capture, a request with its own continuation or several passes restart from the brief.
  • Uncommitted changes of an interrupted integrated attempt stay in its retained worktree.
  • Workers forward only conversations captured by the handler’s dispatch.

API: WorkflowQuotaPolicy · WorkflowQuotaPause · QuotaResumePolicy · quotaFault · TaskContext · QueueResult · WorkflowOptions