First identify which layer answered
A request through Lizzy can fail in three places. The response shape tells you which recovery policy owns the next step. Do this classification before reading a message or retrying the request.
- 1Lizzy API
Authentication, validation, permissions, balance, resource state, and Lizzy service failures use the stable error envelope.
- 2Teacher model
BYOK keeps the provider status and body. Managed sources keep the model status and return teacher_unavailable.
- 3Edge gateway
If the backend cannot be reached, the Worker returns a minimal backend_unavailable 503 response.
Stable machine code
Look for error.type, error.code, and error.request_id.
Source-kind contract
BYOK may return a provider body. Managed failures use the teacher_unavailable envelope.
Keep X-Lizzy-Call-IdMinimal 503
The Worker’s fallback body has a code and message but currently no request ID.
Treat write outcome as unknown{
"error": {
"type": "api_error",
"code": "backend_unavailable",
"message": "The service is temporarily unavailable. Please retry."
}
}
Branch on error.code, not message text
Lizzy-owned failures use one JSON envelope. type is a broad category;code is the stable programmatic decision; message is for a person; and param points to the invalid field when one exists.
{
"error": {
"type": "conflict_error",
"code": "idempotency_key_in_flight",
"message": "A request with this Idempotency-Key is still being processed.",
"param": null,
"request_id": "req_7ff3257080d347da96e2"
}
}
# This read-only request intentionally returns a Lizzy 404 envelope.
curl --silent --show-error --max-time 10 \
"https://lizzy.albinilabs.com/v1/not-a-real-route" \
| jq '.error | {type, code, message, param, request_id}'
Keep the ID owned by the layer you are debugging
error.request_id.headers_file="$(mktemp)"
trap 'rm -f "$headers_file"' EXIT
response=$(curl --silent --show-error --max-time 15 \
--dump-header "$headers_file" \
-H "Authorization: Bearer $LIZZY_API_TOKEN" \
"https://lizzy.albinilabs.com/v1/events?limit=1")
# Print identifiers and codes, not the token or an entire sensitive payload.
awk 'BEGIN { IGNORECASE=1 } /^X-Request-Id:/ { gsub("\r", "", $2); print "request_id=" $2 }' \
"$headers_file"
printf '%s' "$response" | jq '{
error_code: .error.code,
error_request_id: .error.request_id,
first_resource_id: .data[0].id
}'
Retry only after the outcome is classified
- 1
Did you receive an HTTP response?
If not, the outcome of a write is unknown. Retry a GET. Retry a creating POST only with its original idempotency key and byte-identical body; otherwise inspect the relevant list or existing resource before doing anything new.
- 2
Was it a Lizzy envelope?
Use
error.code. Fix 4xx preconditions instead of looping. A managedteacher_unavailablekeeps the model status; apply normal backoff to a 429 or switch the source model. For other 429 errors, wait forretry_after_seconds. For a 5xx, retry reads and idempotent writes with exponential backoff, jitter, and a deadline. - 3
Was it a BYOK provider response?
Follow that provider’s status, body, and retry guidance. Preserve
X-Lizzy-Call-Idand yourX-Lizzy-External-Id; do not try to parse a provider body as a Lizzy envelope. - 4
Did an async watch simply end?
A client timeout or exhausted polling budget is not a terminal resource state. Keep the returned ID, pause, and resume GET polling later. Never create replacement work solely because a watch window ended.
import json
import os
import random
import time
import uuid
import requests
url = "https://lizzy.albinilabs.com/v1/distill/loops"
body = b'{"name":"retry-example","description":"One retained byte string"}'
key = str(uuid.uuid4()) # Persist this beside body before the first attempt.
headers = {
"Authorization": f"Bearer {os.environ['LIZZY_API_TOKEN']}",
"Content-Type": "application/json",
"Idempotency-Key": key,
}
for attempt in range(4):
try:
response = requests.post(url, headers=headers, data=body, timeout=20)
except (requests.Timeout, requests.ConnectionError):
if attempt == 3:
raise
time.sleep(min(2 ** attempt, 8) + random.random())
continue
parsed = response.json()
code = parsed.get("error", {}).get("code")
if response.status_code == 429:
delay = parsed["error"].get("retry_after_seconds", min(2 ** attempt, 8))
time.sleep(float(delay) + random.random())
continue
if response.status_code >= 500 or code == "idempotency_key_in_flight":
if attempt == 3:
response.raise_for_status()
time.sleep(min(2 ** attempt, 8) + random.random())
continue
response.raise_for_status()
print(json.dumps(parsed, indent=2))
break
else:
raise RuntimeError("Retry budget ended; retain the key and inspect before resuming")
Status narrows the problem; code chooses the recovery
retry_after_seconds, add jitter, and keep the same ID or idempotency key.Resume from the resource ID
Dataset versions, uploads, connector pulls, runs, and deployments can outlive the HTTP request that created or discovered them. Persist every returned resource ID before entering a watch loop. Poll its GET endpoint until a documented terminal state, then stop.
Reports are different: a successful run creates its report already inready state. Read the report ID from the completed run and fetch it directly; there is no separate report operation to poll.
const token = process.env.LIZZY_API_TOKEN;
if (!token) throw new Error("LIZZY_API_TOKEN is required");
const terminal = new Set(["succeeded", "failed", "canceled"]);
const runId = process.env.LIZZY_RUN_ID;
if (!runId) throw new Error("LIZZY_RUN_ID is required");
for (let attempt = 0; attempt < 20; attempt += 1) {
const response = await fetch("https://lizzy.albinilabs.com/v1/distill/runs/" + encodeURIComponent(runId), {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw await response.json();
const run = await response.json();
if (terminal.has(run.status)) {
console.log(run);
process.exit(run.status === "succeeded" ? 0 : 1);
}
await new Promise((resolve) => setTimeout(resolve, Math.min(1_000 * 2 ** attempt, 10_000)));
}
throw new Error("Watch window ended. Keep LIZZY_RUN_ID and resume this GET later.");