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.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?
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
InMemoryJobStoreimplementation is a best-effort check-then-save: it looks up the idempotency key, then inserts if absent. SqliteJobStoreoverridessave_if_absent()to be truly atomic via aUNIQUEindex onidempotency_key. Two concurrent submits with the same key race on theINSERT; the loser catchesIntegrityErrorand returns the winning job, so both callers receive the samejob_id.- NULL idempotency keys are exempt from SQLite’s
UNIQUEconstraint, so keyless jobs are never de-duplicated against each other.
Restart Recovery
OnSqliteJobStore 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 returns503 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).Retry-After on the client and reuse the same Idempotency-Key so the retry de-duplicates cleanly:
Start Server
Submit Job
Submit a Recipe Job
Point the same endpoint at an installed recipe by addingrecipe_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 samejob_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).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
Use idempotency keys for retries
Use idempotency keys for retries
Pass
Idempotency-Key (HTTP) or idempotency_key= (recipe helper) so duplicate submits return the same job instead of duplicating work.Prefer webhooks for long jobs
Prefer webhooks for long jobs
For runs over a few minutes, set
webhook_url and let your service react to completion instead of holding an open poll loop.Start the jobs server before integration tests
Start the jobs server before integration tests
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.Set PRAISONAI_JOBS_DB_PATH in production
Set PRAISONAI_JOBS_DB_PATH in production
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.Handle 503 backpressure on the client
Handle 503 backpressure on the client
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.Related
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.

