Status: planned, nothing coded. Each sprint ends with a manual test the user runs before the next one starts.
Opt-in background jobs and cron for production, using native TaskIQ. Developers write plain @broker.task functions. 10xGraph owns the broker, loads the same graph and InjectQ container in the worker, and adds HTTP and webhook triggers plus build support.
from tenxgraph_api.jobs import broker
@broker.task(http=True, retry_on_error=True, max_retries=5)
async def summarize_interview(interview_id: str): ...
@broker.task(schedule=[{"cron": "0 2 * * *"}])
async def nightly_cleanup(): ..."worker": {
"jobs": "jobs",
"broker": "redis",
"url": null
}There is no worker section by default. Without it no routes are added, no worker runs and TaskIQ is not imported. TaskIQ comes only with pip install "10xgraph-api[worker]".
The worker section is separate from cache_store. Jobs can use their own Redis or Redis database.
| Key | Meaning |
|---|---|
jobs (required) |
Module to import, so its @broker.task functions are registered. |
broker |
"redis" (default) builds a RedisStreamBroker. "module:attr" points at any TaskIQ broker the user built (RabbitMQ, NATS, Kafka and so on); 10xGraph uses it as is. |
url |
Redis URL for the built-in broker, with $VAR and ${VAR} expanded. It is ignored when broker is "module:attr". |
The URL is resolved in this order: worker.url, then the WORKER_REDIS_URL env var, then REDIS_URL. A user with one Redis sets only REDIS_URL, and it is used everywhere. A user who wants jobs on a separate Redis or database sets WORKER_REDIS_URL.
With a custom broker, tasks are written against the user's own broker object, not tenxgraph_api.jobs.broker. Everything else (loader, HTTP labels, build) works the same.
taskiq-redis 1.2.4 requires redis>=8.0.0,<9, but our redis extra pins redis>=5.0.7,<7. Sprint 0 must settle this, either by raising our pin (which must be checked against core's Redis usage) or by pinning an older taskiq-redis.
In a scratch venv with taskiq 0.13.0 and taskiq-redis 1.2.4, check the following:
- How to list a broker's registered tasks and find one by name. Build detection and the HTTP route both depend on this.
- Custom labels such as
http=Truereach the task's labels without errors. -
SmartRetryMiddlewareworks withRedisStreamBroker(retry, delay, max retries). - When the worker is killed mid-task, the task is redelivered (stream acks).
- Graceful shutdown: SIGTERM finishes running tasks within
--shutdown-timeout. -
TaskiqSchedulerwithLabelScheduleSourcefires a cron task once. - The redis version conflict above is resolved.
Exit: a short findings note. The plan is updated where reality differs.
- Add the
[worker]extra:taskiq,taskiq-redis, with versions from sprint 0. - Add the
workerconfig section (jobs,broker,url), parsed ingraph_config.py. Unknown keys are config errors, the same as other sections. -
tenxgraph_api.jobs.broker: a lazily configuredRedisStreamBrokerwith retry middleware and a result backend (with expiry). Its URL isworker.url, thenWORKER_REDIS_URL, thenREDIS_URL. It is independent ofcache_store. - Support
broker: "module:attr"by loading the user's TaskIQ broker and checking that it is a TaskIQ broker. - Replace the legacy, fully commented-out TaskIQ stubs in
src/app/worker.pyandsrc/app/tasks/. They come from an old template (Tortoise,fastapi_injector) and nothing imports them. -
10xgraph workercommand. It loads env, thenload_container, thenload_graph, then thejobsmodule, then runs the TaskIQ worker. - InjectQ resolution inside tasks, so
Inject[...]works the same as in graph code. - Startup error when the
workersection is set but the extra is missing. - Tests: unit tests plus one integration test against real Redis.
User test:
- Run
10xgraph workeron the example project. - Call
.kiq()from a script. - Confirm the task gets an injected dependency and can invoke the graph.
- Confirm a failing task retries.
- Kill the worker mid-task and confirm the task reruns.
- Add
POST /v1/jobs/{name}, only for tasks labeledhttp=True.- Uses the existing auth plus an
AuthorizationBackendcheck on resourcejobs, actiontrigger. - Validates the body against the task signature.
- Calls
.kiq()and returns202with thejob_id.
- Uses the existing auth plus an
- Add
GET /v1/jobs/runs/{job_id}for status and result, read from the result backend. - Add
GET /v1/jobsto list the tasks exposed over HTTP. - Unknown tasks and unlabeled tasks return 404.
User test:
- Call the endpoint with curl, with and without a token.
- Send a bad body and confirm it is rejected.
- Poll the status endpoint until the result arrives.
- Add a
webhook=Verifierlabel, which uses signature checks instead of user auth. - Ship a built-in HMAC verifier with the secret read from env.
- Add a base class for custom verifiers, such as LiveKit's.
- Return 401 on a bad signature. The endpoint enqueues only and never runs the job inline.
User test:
- Send a signed request and an unsigned one.
- Run the LiveKit-style example: webhook, then summarize job.
- Add the
10xgraph schedulercommand (loader plusTaskiqSchedulerwithLabelScheduleSource). -
10xgraph buildimports the jobs module and reads the broker's tasks:- No
workersection:apionly. - Tasks:
apiandworker. - Any task with a
schedulelabel: alsoscheduler, with replicas pinned to 1.
- No
- Document the one-scheduler rule.
User test:
- Build three sample projects (no jobs, jobs only, cron) and check the compose files.
- Run the cron project and confirm the job fires once per tick.
- Admin-only schedule edit (
ListRedisScheduleSourceplusPATCH /v1/jobs/{name}/schedule). - Redis event triggers.
- Docs page and a client SDK method for triggering a job and reading its status.
- Jobs are registered only in code. HTTP can trigger a job but never register one.
- Delivery is at least once, so idempotency is the developer's job (key the work by, for example,
interview_id). This goes in the docs from sprint 1. - Cron jobs run as
system_user_id. HTTP jobs run as the caller. - Chat streaming and WebSocket code stay untouched.