litestar-org/litestar-skills/litestar-saq

Claude Code and agent skill by litestar-org/litestar-skills

Trust tier 3
  • community
  • ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when working with litestar_saq, SAQPlugin, SAQConfig, QueueConfig, TaskQueues, CronJob, litestar workers run, background jobs, schedules, or SAQ UI. Not for litestar-queues, Celery, RQ, or Dramatiq.

Install

npx vskill@latest install litestar-org/litestar-skills/litestar-saq

Semi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard

SKILL.md

litestar-saq

litestar-saq is the first-party plugin that integrates SAQ (Simple Async Queue) with Litestar. It provides:

  • SAQPlugin — registers queues, workers, lifespan management, and DI for TaskQueues
  • SAQConfig / QueueConfig — declarative plugin, queue, worker, broker, shutdown, polling, and OpenTelemetry configuration
  • litestar workers run — CLI to start worker processes, optionally filtered by queue
  • Optional web UI mounted under the Litestar app
  • DI injection of TaskQueues into route handlers for ergonomic enqueueing

Code Style Rules

  • Use PEP 604 unions: T | None, never Optional[T]
  • Async all I/O — task bodies and enqueue calls are async def.
  • First positional arg of every task is ctx: Context (from saq.types, not litestar_saq.typing.Context which is the OpenTelemetry context) or ctx: dict[str, Any].
  • Pass job payload as keyword arguments so task signatures and enqueue calls stay explicit.
  • Use NamedDependency[TaskQueues] for handler injection. TaskQueues is registered under the task_queues dependency key, and Litestar ≥ 2.24 deprecates implicit DI.

Quick Reference

Plugin Setup (canonical pattern)

The canonical pattern from litestar-fullstack (src/py/app/server/plugins.py) uses lazy initialization and use_server_lifespan=True so worker child processes start and stop with the Litestar server lifespan.

Redis broker: Use this when Redis is already in the stack or is the chosen SAQ backend:

python
from litestar_saq import CronJob, QueueConfig, SAQConfig, SAQPlugin
from app.lib.settings import get_settings
def create_saq_plugin() -> SAQPlugin:
settings = get_settings()
return SAQPlugin(
config=SAQConfig(
use_server_lifespan=True,
web_enabled=settings.saq.web_enabled,
enable_otel=settings.saq.otel_enabled,
queue_configs=[
QueueConfig(
name="default",
dsn=settings.redis.url,
tasks=["app.domain.system.tasks.send_email"],
scheduled_tasks=[
CronJob(
function="app.domain.system.tasks.cleanup_sessions",
cron="*/15 * * * *",
timeout=120,
),
],
),
],
),
)
saq_plugin = create_saq_plugin()

PostgreSQL broker: Install litestar-saq[psycopg] when PostgreSQL is the chosen backend. When sharing a database URL with SQLAlchemy (postgresql+psycopg:// or postgresql+asyncpg://), strip the driver suffix so QueueConfig.dsn starts with postgresql://:

python
from litestar_saq import (
CronJob,
PostgresQueueOptions,
QueueConfig,
SAQConfig,
SAQPlugin,
after_process_logger,
before_process_logger,
shutdown_logger,
startup_logger,
timing_after_process,
timing_before_process,
)
from app.lib.settings import get_settings
from app.lib.worker import after_process, before_process, on_shutdown, on_startup
def create_saq_plugin_pg() -> SAQPlugin:
settings = get_settings()
dsn = settings.db.url.replace("postgresql+psycopg", "postgresql").replace("postgresql+asyncpg", "postgresql")
return SAQPlugin(
config=SAQConfig(
use_server_lifespan=settings.saq.use_server_lifespan,
worker_processes=settings.saq.processes,
web_enabled=settings.saq.web_enabled,
enable_otel=settings.saq.otel_enabled,
queue_configs=[
QueueConfig(
name="background-tasks",
dsn=dsn,
concurrency=settings.saq.concurrency,
broker_options=PostgresQueueOptions(
jobs_table="task_queue",
stats_table="task_queue_stats",
versions_table="task_queue_ddl_version",
manage_pool_lifecycle=True,
),
tasks=[
"app.domain.reports.tasks.generate_report",
"app.domain.reports.tasks.reap_abandoned_reports",
],
scheduled_tasks=[
CronJob(
function="app.domain.reports.tasks.reap_abandoned_reports",
cron="0 */3 * * *",
timeout=60,
),
],
startup=[startup_logger, on_startup],
shutdown=[shutdown_logger, on_shutdown],
before_process=[timing_before_process, before_process_logger, before_process],
after_process=[after_process, timing_after_process, after_process_logger],
shutdown_grace_period_s=60,
cancellation_hard_deadline_s=10,
),
],
),
)

Choose the broker already supported by the deployment. PostgreSQL job writes use SAQ's own pool and transaction; they are not automatically atomic with writes made through an application ORM or SQL session.

Each QueueConfig accepts exactly one connection source: a supported redis:// (rediss://, redis+unix://), postgresql://, or http:// dsn, or a supported broker_instance. Supplying both or neither raises ImproperlyConfiguredException. PostgreSQL DSNs must start with postgresql:// (postgres:// raises ImproperlyConfiguredException). PostgreSQL requires litestar-saq[psycopg]; configure queue behavior with broker_options (RedisQueueOptions or PostgresQueueOptions; note that on saq>=0.24, PostgresQueue.__init__ takes jobs_table, stats_table, and versions_table) and connection/client construction with broker_instance_options. In litestar-saq 0.8.0, set enable_otel=True explicitly when litestar-saq[otel] is installed (SAQPlugin.get_workers() evaluates should_enable_otel() without the Litestar app instance, so enable_otel=None resolves to False).

Wire into Litestar
python
from litestar import Litestar
from app.server.plugins import saq_plugin
app = Litestar(
route_handlers=[...],
plugins=[saq_plugin],
)
Define a Task

Task functions live in app/domain/<domain>/tasks.py:

python
async def send_email(ctx: dict, *, recipient: str, subject: str, body: str) -> None:
"""Send an email as a background job.
Args:
ctx: SAQ context dict populated by worker hooks.
recipient: To address.
subject: Email subject.
body: Email body.
"""
email_service = ctx["email_service"]
await email_service.send(recipient, subject, body)

For long-running work, set the job's heartbeat stale threshold (at least 60s so it exceeds HeartbeatManager's default 30.0s batch flush interval) and decorate the task with monitored_job() so the plugin signals its batched HeartbeatManager while the task runs. heartbeat is not an update interval: SAQ marks an active job stuck when its last touch is older than that threshold.

python
from litestar_saq import monitored_job
@monitored_job()
async def rebuild_index(ctx: dict, *, index_name: str) -> dict[str, str]:
await run_rebuild(index_name)
return {"status": "complete"}
Enqueue from a Handler (DI of TaskQueues)
python
from litestar import Controller, post
from litestar.di import NamedDependency
from litestar_saq import TaskQueues
class NotificationController(Controller):
path = "/api/notifications"
@post("/")
async def queue_notification(
self,
data: NotificationCreate,
task_queues: NamedDependency[TaskQueues],
) -> dict[str, str]:
queue = task_queues.get("default")
job = await queue.enqueue(
"send_email",
recipient=data.email,
subject=data.subject,
body=data.body,
timeout=30,
retries=2,
key=f"notify-{data.email}",
)
return {"status": "queued" if job is not None else "duplicate"}

When a handler creates or updates database rows that the background job will read, explicitly commit the database session when queue.enqueue() succeeds (or roll back when a duplicate key returns None) so the worker never races ahead of Litestar's before_send autocommit hook:

python
if job is not None:
await reports_service.repository.session.commit()
else:
await reports_service.repository.session.rollback()
CLI
bash
# Run workers (uses the same Litestar app)
litestar --app app:app workers run
# Run multiple worker processes
litestar --app app:app workers run --workers 4
# Run only selected queues in this worker service
litestar --app app:app workers run --queues emails --queues reports
# Inspect queues
litestar --app app:app workers status
Web UI

When web_enabled=True, the SAQ web UI and JSON API are mounted at web_path (default "/saq"), including {web_path}/api/queues, job detail/retry/abort endpoints, and {web_path}/health. Protect the UI in production with web_guards=[...] and opt into OpenAPI schema inclusion with web_include_in_schema=True.

Job Options
OptionDefaultUse
timeout10Always set explicitly — SAQ's default is usually too low or too high for real jobs (0 disables)
retries1Retry count on exception
retry_delay0.0Seconds to wait before retrying
retry_backoffFalseTrue for exponential backoff with jitter, or a numeric maximum delay in seconds
ttl600Seconds to retain result after completion (0 retains indefinitely, -1 disables)
keygeneratedStable uniqueness key; enqueue returns None while that key already exists
heartbeat0Maximum seconds an active job may go without a touch; 0 disables stale detection
scheduled0Unix timestamp (epoch seconds) to delay start
priority0Dequeue priority on PostgreSQL queues (PostgresQueueOptions.priorities defaults to (0, 32767))
group_keyNoneConcurrency serialization key on PostgreSQL queues (at most one active job per group_key)
meta{}Arbitrary metadata dictionary attached to the job (CronJob also accepts meta)

<workflow>

Workflow

Step 1: Install
bash
pip install litestar-saq
pip install "litestar-saq[psycopg]" # PostgreSQL broker
pip install "litestar-saq[otel]" # OpenTelemetry spans
Step 2: Define Queues

Build QueueConfig instances for each logical queue ("default", "emails", "reports"). Put exactly one of dsn or broker_instance on each QueueConfig. Reference task functions by dotted path or callable; the plugin imports dotted paths at startup. When passing lifecycle hooks (startup, shutdown, before_process, after_process) as dotted strings, always wrap them in a list (startup=["app.domain.system.tasks.worker_startup"]) because str is a Collection.

Step 3: Configure Plugin

Wrap QueueConfigs in SAQConfig. Pick a supported broker DSN (redis://..., postgresql://..., or http://...) that matches the deployment. Set use_server_lifespan=True when the web process should own worker child processes. Toggle web_enabled / web_guards for the introspection UI and enable_otel=True when litestar-saq[otel] is installed.

Step 4: Define Tasks

Place task functions in app/domain/<domain>/tasks.py. Use ctx: dict as the first argument and pass job data as keywords. Add shared resources (DB, HTTP client, email service) in QueueConfig.startup / before_process hooks and read them from ctx.

Step 5: Schedule Cron Work

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Extension points

  • hookPre/post lifecycle hooks

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/litestar-org/litestar-skills/litestar-saq/badge)](https://verified-skill.com/skills/litestar-org/litestar-skills/litestar-saq)

Something wrong with this skill or its page? Tell us.