Troubleshooting¶
Most of the errors you'll hit early aren't bugs in your code. They're setup steps that haven't happened yet: a database that doesn't exist, a schema that wasn't installed, a worker that was never told about your task.
This page collects the ones you're most likely to see, with the exact message and the fix. If you're staring at a traceback, search this page for the line in bold.
Setup, once¶
Before any of the examples work, two one-time things have to be true about your database:
-
The Absurd schema is installed. Absurd stores everything (tasks, runs, checkpoints) in an
absurdschema made of SQL functions and tables.absurd-sdkdoes not create this for you, and it does not ship a CLI to do it. It's a plain SQL install you run once, the way you'd run any database migration. The SQL lives in the Absurd repository; this project also vendors a copy attests/fixtures/absurd.sql, so during development you can load it with: -
The queue exists. Once the schema is in place,
await absurd.create_queue()creates the queue your tasks live on. The example scripts call it insidemain().
If you skip either, you'll see one of the errors below.
Errors you'll actually see¶
connection failed: ... role "..." does not exist¶
You're connecting to a different Postgres than you think. This usually means something else (a local Homebrew Postgres, say) is already listening on port 5432, and your DSN's username doesn't exist on it.
Check what's on the port:
If a local Postgres is squatting there, either point your DSN at it (with a role that exists, often your own username) or stop it so the database you intended is the one answering. The DSN is the source of truth: postgresql://USER@HOST:PORT/DBNAME.
database "..." does not exist¶
The role connected fine, but the database in your DSN was never created. Create it:
Then make sure your DSN's database name matches (.../absurd).
schema "absurd" does not exist¶
You connected to the right database, but the Absurd schema isn't installed in it. This is step 1 above: load the Absurd SQL into that database once.
A symptom of the same problem is an error mentioning absurd.spawn_task or absurd.create_queue not existing: those are the schema's functions, and they're missing because the schema is.
The run worked, but nothing was checkpointed¶
You called await agent.run(...) directly, not inside a task. Outside a task the capability is transparent: the run completes like a normal agent run, with no durability. That's handy for tests and local experiments, but the checkpoints only happen inside a task, where Absurd can record each step.
Wrap it:
@absurd.register_task(name="analyse")
async def analyse(params, ctx):
result = await agent.run(params["prompt"]) # inside a task: durable
return {"output": result.output}
Unknown task (the worker fails the task)¶
A worker claimed a task whose name it doesn't recognize. The fix is almost always: register the task in the process that runs the worker. @register_task has to execute in the worker process before work_batch or start_worker. The process that only spawns doesn't register anything, it just writes a name and params to the database, but the worker must know that name.
An agent needs to have a unique name in order to be used with Absurd¶
The agent has no name, and Absurd uses the name as the prefix for every checkpoint. Give the agent a name (Agent("openai:gpt-5.2", name="analyst")) or pass one to the capability: AbsurdDurability(name="analyst").
... cannot be passed to run(toolsets=...) at runtime with Absurd¶
You passed a FunctionToolset, MCPToolset, or DynamicToolset to agent.run(toolsets=...) - or through agent.override(toolsets=...) / agent.override(tools=...) - inside a task. Executing toolsets are wrapped for checkpointing when the capability binds to the agent, so they have to be there at construction time. Move the toolset to the Agent(...) constructor; non-executing toolsets like ExternalToolset can stay per-run.
A toolset a capability contributes through AbstractCapability.get_toolset() is not a runtime toolset: it is part of the agent when AbsurdDurability binds, so it is wrapped and checkpointed like any constructor toolset.
"It runs, but the script never exits"¶
Not an error, just a surprise. await absurd.start_worker() is meant to run forever: it polls Postgres and keeps claiming tasks, which is exactly what you want for a long-lived worker process.
For a script you want to finish (a scratch file, a test), use work_batch instead. It drains the tasks that are waiting and returns:
See the tutorial for where each one fits.
Still stuck?¶
If it's the durability behavior that's confusing rather than the setup, How durability works walks through exactly what's checkpointed and what happens on a crash. For anything that looks like an Absurd-level problem (the schema, queues, task states), the Absurd project is the source of truth.