Skip to main content
Use PostgreSQL, Redis, or your own storage without changing your Tasks. Harnest runs the workers, retries, and schedules; your provider persists their state. Queued tasks require @lifecycle.storage.tasks. Add @lifecycle.storage.cron to the same factory for recurring work. There is no default database.

Choose and register a provider

harnest_postgres and harnest_redis ship with the Harnest runtime, not as separate PyPI distributions. Managed ADK and LangGraph environments include both drivers. From your agent folder, synchronize the matching runtime:
For direct Python development from source, install the driver you need:
Use [postgres,redis] to install both drivers. Imports do not open connections. Register one shared factory in lifecycle/storage.py, replacing the corresponding existing storage factories:
Harnest calls the factory, starts the provider, and closes it once. Remove unused decorators; keep any file that still provides sessions or checkpoints.
PostgreSQL creates Harnest-owned task and cron tables. For Redis, configure persistence, replication, backups, and no eviction; a cache-only deployment is not durable. Task records do not inherit session/checkpoint TTLs.

Share connection configuration

Start your database service before the agent. Workers run inside Harnest; no separate queue engine is needed. For reusable configuration, move the factory into lib/. Use this instead of the inline task/cron factory above:
1

Create lib/task_storage.py

lib/task_storage.py
2

Create lifecycle/task_storage.py

lifecycle/task_storage.py
For PostgreSQL, substitute harnest_postgres.PostgresStore and a PostgreSQL URL. Declare custom adapter dependencies in your agent’s pyproject.toml. From your agent folder, point the factory at your database and start the service:
Match environment-variable names to your factory and inject production credentials through deployment secrets. Replicas must share the same application identity and persistent provider.

Write a custom database adapter

Implement the public structural contracts; subclassing a Harnest store is not required: Follow TaskStore and, for schedules, CronStore. Their method references define required operations, parameters, and results. Follow the protocol signatures and method docstrings. Records use UTC epoch seconds, JSON-safe payloads, and detached snapshots. Register the adapter with @lifecycle.storage.tasks and, when supported, @lifecycle.storage.cron.

Test a custom adapter

Put this in your adapter package’s tests and run it against an isolated real database, not only a mock:
The suite checks isolation, concurrent claims, leases, deduplication, revisions, and atomic scheduling. Add database-specific crash, rollback, connection-loss, and restart tests. Verify deployment persistence separately.

Execution and recovery guarantees

Delivery is at least once: a crash after an external effect can cause a retry. Make effects idempotent. Cancellation blocks later commits but cannot undo effects or forcibly stop synchronous code. Durable handle.result() waits also require a persistent Harnest-owned checkpointer. Task storage alone is insufficient. Queue ordering is best-effort across replicas and recovery. Cron cursors survive restart. Missed occurrences catch up in bounded batches; there is no skip/coalesce misfire option in this version. Cancelling a schedule before its atomic occurrence commit prevents that enqueue; already committed jobs continue. Removed static declarations are paused at the next deployment startup, while a retargeted declaration gets a new identity. Do not run old and new conflicting static declarations concurrently during a rolling deployment.

Move an existing application safely

The Procrastinate backend and automatic PostgreSQL fallback have been removed. If you used them, drain work on your currently deployed version before upgrading. The new runtime cannot process the old queue tables.
  1. Back up your database and inventory recurring schedules for each owner.
  2. Stop new submissions and pause recurring schedules on the old runtime.
  3. Drain or explicitly cancel queued/running Tasks and resolve their waiting continuations before stopping the old workers.
  4. Configure the new provider, preserving session/checkpoint storage when it contains history you need. Synchronize the runtime and recompile your agent. Start the new deployment and recreate dynamic schedules under their original user scopes. Static declarations reconcile automatically.
  5. Verify task execution, recovery, and owner isolation before reopening traffic.
Old queue data and continuation references are not migrated automatically. Keep the old database for recovery, and drain work before removing, renaming, or changing a task’s queue.

API reference

Reference: harnest.testing.

Optional development inspection

The Playground Work view uses optional list_task_metadata and list_cron_metadata methods. Existing TaskStore and CronStore implementations remain compatible without them: users can look up work by ID, cancel tasks, and manage known dynamic schedules. Both methods are async and accept keyword-only application_id, user_id, after=None, and limit=100 (1–100). Apply owner and cursor predicates, ordering, projection, and limit inside the datastore. Task IDs and schedule IDs are exclusive cursors sorted lexicographically. Return {"items": [...], "after": next_id_or_none}; task pages also include "indexing": false. A provider rebuilding an index can return an empty task page with "indexing": true until it can produce a complete listing. Task items contain only job_id, task_name, queue, status, scheduled_at, attempt, max_retries, created_at, updated_at, and failure_code. Cron items contain only schedule_id, key, expression, task_name, status, timezone, next_run_at, revision, max_runs, max_consecutive_failures, run_count, consecutive_failures, and read_only. Set read_only for keys beginning with static:. Never return task arguments, results, invocation snapshots, credentials, permissions, or lease tokens in metadata listings. Built-in memory, PostgreSQL, and Redis providers implement these queries. PostgreSQL adds owner indexes during schema setup; deployments using setup_schema=False should provision matching (application_id, user_id, job_id) and (application_id, user_id, schedule_id) indexes. Redis maintains the task index in the enqueue transaction and incrementally indexes older records when the Work view first lists them.