Skip to main content
Use Tasks for work that should run independently of a model call, such as generating a report, processing an upload, or calling a slow service with retries. An Agent Tool decides when to queue the work. The task itself is not model-visible. Use an ordinary function call when the caller can do the work immediately and does not need queueing or recovery. See the harnest.task API reference for parameters, return types, and methods.

Configure Lifecycle storage

Before queueing work, register a task storage provider in the agent’s root lifecycle/ folder. Adding a @task function does not configure storage. Harnest discovers these Python factories automatically; there is no task-storage setting to add to config.yaml. For the durable tool example below, create or update lifecycle/storage.py:
lifecycle/storage.py
Set DATABASE_URL to your running PostgreSQL database before starting the agent. harnest_postgres ships with the managed Harnest runtime. Harnest calls the factory and owns starting and closing its provider. Replace any existing factories for the roles shown above; do not register a second provider for the same role. If you already have session and checkpoint providers that you want to keep, register only @lifecycle.storage.tasks on this new factory. For local experiments, a task factory can return harnest.task.MemoryTaskStore() instead. It loses queued work on process exit and does not provide session or checkpoint storage; register it only for task and optional cron roles. Use persistent storage for production. See Task and cron storage for Redis, custom providers, and complete storage setup.

Define and call a task

From the agent root, create a compilable task starter:
Use --project <agent-root> when running the command from elsewhere. The generated file is suitable when no other module imports the task; use the shared-library pattern below when tools also import it. When a tool needs the task callable, define it once in lib/ and re-export it from tasks/. The following three files work together:
1

Define the shared task

lib/report_tasks.py
2

Make the task discoverable

tasks/build_report.py
3

Queue work from an Agent Tool

tools/request_report.py
If nothing imports the task, you can define it directly in tasks/<name>.py. The file must export exactly one same-named @task callable. Tasks can be synchronous or asynchronous. Give each task a docstring and accept JSON-safe arguments; its result must also be JSON-safe. See the task parameter contracts for signature restrictions.

Decide whether to wait

For fire-and-forget work, defer the task and return its ID to the caller. Wait for its result when the agent needs the output to continue. An unfinished result requires an async @tool(durable=True) and a Harnest-owned checkpointer. The returned TaskHandle lets managed code inspect status, cancel work, and access its result. It belongs to the active compiled runtime; there is no public API to reconstruct or list handles by ID.

Make retries safe

Workers can retry a task. Make side effects idempotent, such as recording a report under a stable business key before notifying another service. An idempotency key deduplicates submission; it does not make every external effect happen exactly once. Inside a durable tool, Harnest derives a replay-stable submission key when you omit one. Use defer(schedule_in=...) for a one-time delay. For recurring work, use Cron schedules.

Runtime requirements

With Lifecycle storage configured and DATABASE_URL set, run from your agent folder. Replicas can share the same persistent provider:
Queued execution requires configured storage and an active compiled runtime. Calling .defer() outside it raises TaskUnavailableError. Call the task body directly for database-free unit tests.

Start a fresh agent session

A task can invoke the compiled root agent without an HTTP call through context.agent.create_session() and AgentSession.invoke(). Pass a stable session key when retries should derive the same child session and invocation identities. To continue an existing conversation, use context.agent.open_session(session_id) inside the task. It opens only sessions owned by the task’s user, including the session that queued the task. Choose the destination explicitly; defer() does not select one. For example, a one-time reminder can carry the current session ID into a delayed task:
The later agent turn is saved in that session’s transcript. A client that needs an immediate alert must watch that session or deliver the resulting message through its own chat transport. If the session was deleted before the task runs, opening it fails instead of creating a replacement. Deferred tasks inherit user identity and public metadata. Fixed schedules use the automation identity; dynamic schedules use their creator with fresh context and empty grants. When .defer() runs under an Agent Runtime Principal, Harnest stores only its permission names and reconstructs a fresh principal for each worker attempt. Agent calls made by the Task inherit that restriction unless trusted task code passes a narrower principal. A Task deferred without an active principal preserves compatibility behavior. Durable external waits return a typed in_progress response. Human approvals and client tools fail closed because their continuation is process-local after the task returns.
An unfinished handle.result() requires an async @tool(durable=True) and a Harnest-owned checkpointer. Harnest resumes framework execution; it does not restore a Python stack.
See Durable execution for cross-replica resume behavior.

Test Task behavior

Call the decorated function directly to unit-test its business logic without a database. Exercise .defer(), retries, cancellation, and durable resume through a compiled smoke test with your chosen persistent provider so the real queue and checkpoint boundaries run together. Custom providers can use the shared conformance suite.