Skip to main content
Submit long-running agent tasks and recipes, then retrieve results asynchronously via a jobs server. The user submits a long job; the agent runs asynchronously and returns when the job completes.

How It Works

The user submits a job, the server runs it in the background, and the result comes back on completion.

Choose a Result Mode

Pick how the result is delivered once the job finishes.

Quick Start

1

Submit via recipe helper

2

Submit via HTTP API

Persistent Store

Point the Jobs API at a SQLite file so job state and idempotency keys survive restarts — no code change, just one env var.
Construct the store yourself when you want explicit control:
SqliteJobStore and create_app are both exported from praisonai.jobs. _build_default_store() in praisonai/jobs/server.py selects the backend from the environment:

Which store should I use?

In production the Jobs API refuses to boot on the in-memory store. _build_default_store() raises RuntimeError with this message:
Jobs API refused to start with the in-memory store in production. Set PRAISONAI_JOBS_DB_PATH (e.g. /var/lib/praisonai/jobs.db) to persist jobs and idempotency keys across restarts, or explicitly pass store=InMemoryJobStore() to create_app().
The check fires only when ENVIRONMENT=production and PRAISONAI_JOBS_DB_PATH is unset. Passing store=InMemoryJobStore() explicitly to create_app() bypasses the guard — the documented escape hatch for ephemeral production workloads that accept losing job state on restart.

Atomic Idempotency

JobStore exposes save_if_absent(job) so a duplicate submit resolves to a single job and side effects run exactly once.
  • The default InMemoryJobStore implementation is a best-effort check-then-save: it looks up the idempotency key, then inserts if absent.
  • SqliteJobStore overrides save_if_absent() to be truly atomic via a UNIQUE index on idempotency_key. Two concurrent submits with the same key race on the INSERT; the loser catches IntegrityError and returns the winning job, so both callers receive the same job_id.
  • NULL idempotency keys are exempt from SQLite’s UNIQUE constraint, so keyless jobs are never de-duplicated against each other.

Restart Recovery

On SqliteJobStore startup, _reconcile_interrupted_jobs() marks any row still in QUEUED or RUNNING as FAILED, with error="Interrupted by service restart" and completed_at=now. A crashed executor leaves no worker to resume its in-flight jobs, so those rows would otherwise poll forever and pin an idempotency key to a job that can never complete. Terminal reconciliation gives callers — and idempotent retries — a definitive outcome.

Backpressure & Admission Control

A flood of submits now returns 503 instead of accepting an unbounded backlog. The executor gates admission in two tiers: max_concurrent caps how many run bodies execute at once, while max_queued caps how many jobs are admitted (queued + running) before new submits are rejected. When admission is saturated, JobExecutor.submit() raises JobQueueFull(retry_after=1.0). The router deletes the stranded QUEUED row via store.delete(job.id) — freeing the idempotency key — and returns HTTPException(status_code=503, detail="Job queue is full; retry later.", headers={"Retry-After": "1"}). Set the ceiling on the constructor:
New in PR #4336: the Jobs API returns 503 Service Unavailable with a Retry-After header when the executor’s admission ceiling (max_queued, default max_concurrent × 10) is hit. The stranded QUEUED row is deleted, so retrying the same Idempotency-Key is safe. max_queued is a JobExecutor constructor argument — pass a custom executor to create_app(executor=...) to change it (get_executor() reads PRAISONAI_MAX_CONCURRENT_JOBS and PRAISONAI_JOB_TIMEOUT from the environment, but not max_queued).
Honour Retry-After on the client and reuse the same Idempotency-Key so the retry de-duplicates cleanly:

Start Server

Advanced / factory mode (multiple workers, direct uvicorn control):

Submit Job

Submit a Recipe Job

Point the same endpoint at an installed recipe by adding recipe_name (and, optionally, recipe_config). The prompt field becomes the recipe’s input data.

Idempotency

Since PR #1673, the in-process store is safe to read concurrently with writes. You can safely share a single InMemoryJobStore instance between the FastAPI app and background tasks that periodically read stats.

Polling

SSE Streaming

Multiple viewers of the same job_id now each receive every progress update — one disconnecting no longer freezes the others.
New in PR #4336: /api/v1/runs/{id}/stream now supports multiple concurrent viewers of the same job. Every subscriber receives every progress update. If you call JobExecutor.register_progress_callback / unregister_progress_callback directly, note that unregister_progress_callback now takes an optional second argument: pass the specific callback to remove only that subscriber; call without a callback to clear all subscribers (the shutdown path).
When driving JobExecutor directly, remove only your own subscriber so co-registered viewers keep streaming:

Webhook Callback

Session Grouping

Cancel Job

List Jobs

Complete Example

CLI Usage


Best Practices

Pass Idempotency-Key (HTTP) or idempotency_key= (recipe helper) so duplicate submits return the same job instead of duplicating work.
For runs over a few minutes, set webhook_url and let your service react to completion instead of holding an open poll loop.
praisonai serve jobs --port 8005 (or, for factory mode, python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory) — the in-process store is safe for concurrent reads after PR #1673.
Export PRAISONAI_JOBS_DB_PATH=/var/lib/praisonai/jobs.db so SqliteJobStore persists jobs and idempotency keys across restarts. With ENVIRONMENT=production and no path set, create_app() raises RuntimeError rather than silently losing state.
Size max_queued to your worker capacity and retry on 503 using the Retry-After header with the same Idempotency-Key. The rejected QUEUED row is deleted, so the retry starts clean instead of colliding with a stranded record.

Background Tasks

Run agent work in-process without a separate jobs server.

Async Jobs CLI

Submit, stream, and cancel jobs from the terminal.

Durable Tool Runs

Persist and resume tool executions across restarts.

Rate Limiter

Shape request bursts before they reach the admission ceiling.