A windswept bonsai on a floating island

Get to know Cadenya

We’re developers who love to build. We set out to create a yes-code platform that makes building agents feel like the best parts of building software.

Durable AI agents in Django with Celery (2026)

Django

By Cadenya

To build a durable AI agent in Django with Celery, you split the agent into tasks and keep its state in your own Django models, because a Celery task runs once and has no built-in way to pause for a human-in-the-loop approval. DBOS and the Temporal Python SDK add durable execution on top: DBOS checkpoints workflows to Postgres from inside your Django process, and Temporal replays workflows on a worker you host, with signals for the approval.

So keep Celery when the agent is one or two steps and your team already runs the broker. Pick the DBOS Django integration when you want durable workflows on the Postgres you already have, and pick the Temporal Python SDK when you want a workflow server with signals and a managed cloud. And if you’d rather write and run none of it, Cadenya is a hosted agent loop with approvals and conversation state built in, and your Django app calls its API. A coding agent can set it up with no account, in a temporary workspace you claim later. What it offers is at the end.

Every price and limit below comes from each vendor’s own pages, checked on October 13, 2026.

Celery, DBOS, Temporal Python and Django tasks side by side

CeleryDjango 6.0 tasks frameworkProcrastinateDBOS Transact (Python)Temporal Python SDKCadenya
What it isTask queueTask API in Django coreTask queue on PostgreSQLDurable workflow library on PostgresDurable workflow SDK for a Temporal serverHosted agent loop with approvals and conversation state
What you run next to DjangoA broker (RabbitMQ or Redis) and worker processesA third-party backend with “a worker process and a durable queue”PostgreSQL and a workerPostgres, as the system databaseA Temporal server (or Temporal Cloud) and workers you hostNothing. Your app calls the Python SDK
What survives a crashThe task message, redelivered to another workerDepends on the backendThe job row in PostgresEach completed step, from a Postgres checkpointThe workflow, replayed from its historyThe objective’s durable event log, kept on Cadenya
Sending a decision to a running agentYour own model row plus a new taskYour own model row plus a new taskYour own model row plus a new jobDBOS.send and DBOS.recvSignals and Updatesclient.objectives.approve_tool_call(...)
LicenseBSD-3-ClauseBSD-3-ClauseMITMITMITApache-2.0 (SDKs)
Latest version on PyPI5.6.3Django 6.1.23.10.03.2.01.34.01.9.0 (Python SDK)
Python3.9 and later3.12 and later3.10 and later3.10 and later3.10 and later3.9 and later

Celery human-in-the-loop: approval between two tasks

Celery moves messages, not workflows. “To initiate a task the client adds a message to the queue, the broker then delivers that message to a worker.” The worker runs the function, and that’s the whole contract.

That shapes how an agent works on Celery. By default, the worker acknowledges the message “just before it’s executed, so that a task invocation that already started is never executed again.” Set acks_late and the worker acknowledges “after the task returns instead,” so a task cut off halfway can run again from the top. That’s why Celery asks for idempotent tasks. An agent turn that made three model calls and one refund isn’t idempotent unless you make it so.

So what about a person approving a tool call? Holding the task open while you wait is the one thing Celery’s docs warn against: “Having a task wait for the result of another task is really inefficient, and may even cause a deadlock if the worker pool is exhausted.” The advice is to “make your design asynchronous instead.”

For an agent, that means two tasks and a model row between them. The first task runs the loop until the model asks for a tool that needs approval, saves the conversation and the pending call, and returns. Your approval view records the decision and enqueues the second task:

# tasks.py
from celery import shared_task
from .models import AgentRun

@shared_task
def resume_agent(run_id: int) -> None:
    run = AgentRun.objects.get(pk=run_id)  # your model: messages, pending tool call, decision
    ...  # run the approved tool, call the model again, save the next state

# views.py
from django.http import HttpResponse
from .models import AgentRun
from .tasks import resume_agent

def approve(request, run_id):
    AgentRun.objects.filter(pk=run_id).update(decision="approved")
    resume_agent.delay_on_commit(run_id)
    return HttpResponse(status=204)

delay_on_commit matters here. Celery’s Django guide calls it “a common pitfall” to enqueue a task before the transaction commits, because “the Celery task may run before all changes are persisted to the database.” Celery 5.4 added delay_on_commit() so the task goes to the broker only after the commit.

That’s a working pattern. But the conversation format, the pending-call row, the retry rules, and the replay logic are all your code. Retries and idempotency for agent tool calls covers that part in more depth.

Django 6.0 tasks framework and Procrastinate: Postgres task queues

Django 6.0 “includes a built-in Tasks framework” for code that runs outside the HTTP request and response cycle. You define a task with from django.tasks import task and @task, then enqueue it.

The release notes are clear about the split: “Django handles task creation and queuing, but does not provide a worker mechanism to run tasks.” The built-in backends “are for development and testing only,” and “Production systems should rely on backends that supply a worker process and a durable queue implementation.”

So the tasks framework is an API, not a runtime. It gives third-party backends one interface to plug into, and you still pick and run one. The human-in-the-loop design is the same two-task pattern as above.

Procrastinate is the Postgres alternative to Celery. It keeps jobs in PostgreSQL and uses “PostgreSQL’s LISTEN” to wake a worker “whenever a task is available.” Its docs give the reason plainly: running a big Postgres is work already, and they didn’t want to also run “a RabbitMQ / Redis /…” next to it. It ships a Django integration with its own migrations, and it supports retry strategies on tasks. It’s still a task queue, though: one job, one run.

DBOS Django integration: durable workflows on Postgres

DBOS is a library that checkpoints workflows to a Postgres system database, with “no separate orchestration server.” The Django integration starts DBOS from your app’s AppConfig:

# polls/apps.py
import os
from django.apps import AppConfig
from dbos import DBOS, DBOSConfig

class PollsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'polls'

    def ready(self):
        dbos_config: DBOSConfig = {
            "name": "django-app",
            "application_version": "0.1.0",
            "system_database_url": os.environ.get("DBOS_SYSTEM_DATABASE_URL"),
        }
        DBOS(config=dbos_config)
        DBOS.launch()
        return super().ready()

(The docs suggest runserver --noreload in development “to avoid repeatedly relaunching DBOS.”)

Workflows are decorated functions “you can call from your services or views.” For an approval, the workflow calls DBOS.recv(), and your approval view calls DBOS.send() with the workflow ID. “All messages are persisted to the database, so if send completes successfully, the destination workflow is guaranteed to be able to recv it.”

That’s durable execution with no broker, next to the Django app you already run. You still own the system database, the application_version string on each deploy, and the agent loop inside the workflow. DBOS vs Temporal for AI agents compares the two engines head to head.

Temporal Python SDK with Django: signals for human-in-the-loop

Temporal splits your agent in two. Workflow code orchestrates and “must be deterministic,” and activities do the I/O, “such as querying a database, calling a third-party API.” For Django, that means every ORM query and every model call goes in an activity. The worker is its own process, outside your Django app, and your Django app is a client that starts workflows and sends signals.

The approval itself is a signal plus a condition. This is trimmed from Temporal’s Python message-passing docs:

from typing import Optional
from temporalio import workflow

# ApproveInput is a dataclass with a name field.

@workflow.defn
class GreetingWorkflow:
    def __init__(self) -> None:
        self.approved_for_release = False
        self.approver_name: Optional[str] = None

    @workflow.signal
    def approve(self, input: ApproveInput) -> None:
        self.approved_for_release = True
        self.approver_name = input.name

    @workflow.run
    async def run(self) -> str:
        await workflow.wait_condition(lambda: self.approved_for_release)
        ...

A Django view sends approve through a Temporal client, and the workflow moves on. DBOS vs Temporal for AI agents covers the engine itself, and the Temporal cost breakdown covers Temporal Cloud and self-hosting.

Django SSE streaming under ASGI: long-lived connections

Durable execution is half the job. The other half is getting the model’s reply to the browser as it arrives, and Django has a rule about that.

Under WSGI, Django’s docs say, the framework “is designed for short-lived requests,” and a streaming response ties up a worker process for as long as it streams. Under ASGI, a StreamingHttpResponse “need not stop other requests from being served whilst waiting for I/O,” which opens up “long-polling, and server-sent events.”

So if your Django app streams tokens itself, it moves to an ASGI server, and every open chat holds a connection on your web tier. That’s true whichever of the four options above runs the agent, because none of them streams to the browser for you.

A hosted loop changes where the stream lives. Cadenya streams objective events over server-sent events from its own API, and it can also send a signed POST to your Django app for each message, tool call, and approval. A webhook is a short request, so a plain WSGI view handles it.

What does Cadenya offer that Celery, DBOS and Temporal don’t?

Celery, DBOS, and Temporal run your Python durably, and your Python still has to be the agent. Cadenya is the agent loop itself, with approvals, conversation state, and resume built in, and your Django app only calls it. Here’s what that covers:

  1. Nothing else to run. No broker, no worker pool, no Temporal server, and no system database to provision. Your Django app creates an objective with the Python SDK (client.objectives.create(...)) and goes back to serving requests.
  2. State you don’t have to model. Every objective carries a durable event log of its agent loop, so there’s no AgentRun table, no pending-call row, and no replay code. You list the stored events after a run or stream new ones while it works.
  3. No old code to keep alive. An objective keeps its variation’s configuration snapshot even when you edit the variation later. There’s no determinism rule to follow and no workflow version to pin on deploy.
  4. Approvals that hold no worker. Mark a tool in a tool set as needing approval and Cadenya parks the loop until it gets a decision, with several tools able to wait at once. Your Django view calls client.objectives.approve_tool_call(objective_id, tool_call_id), or denies it with a memo that steers the agent.
  5. SDKs at 1.9.0 for the Python you run. The Python SDK supports Python 3.9 and later, under Apache-2.0, next to TypeScript, Ruby, and Go SDKs on the same 1.9.0 release. The TypeScript SDK alone has shipped 113 versions since April 2026.
  6. A price in one line. Free for 1,000 loops a month, $49 a month for 50,000, then $0.0015 a loop. A loop is one LLM request made during an objective.
  7. The loop, already written. Cadenya calls the model, runs the tools, streams events over SSE, and compacts a long conversation. On Celery, DBOS, or Temporal, that’s the code inside your task or workflow.
  8. Tools from your Django API’s OpenAPI spec. Point a tool set at an OpenAPI 3 document by URL and Cadenya checks it for changes each hour. Filters and progressive discovery keep a big API from filling the context window.
  9. Any model, on your own keys. You pick the model on a variation, from the AI providers in your workspace, so switching models needs no deploy. Weighted selection uses feedback to favor the variations that score well.

How objectives work walks through an agent turn, and the free plan needs no card.

No account yet? A coding agent can create a temporary workspace with cadenya workspaces create --temporary, run its first objectives there, and hand you a link to claim it, as auth.md explains. To create an account, sign up at app.cadenya.com/signup, then copy your API key from the API keys page at app.cadenya.com/account/apiKeys.

Grow wherever AI goes next.

Start shipping agents that are equipped to evolve.

A pine bonsai overlooking a mountain lake