STATEK Subtasks
A STATEK subtask is delegated work running as another job.
It is not an inline function call. The parent job creates or receives a child job, the child job runs with its own durable Python workspace, and a SubTaskHandler represents the child work back in the parent.
Conceptually:
analysis = ask_specialist(dataset, question)
report.add_section(analysis.result)The specialist does not share the parent's local variables by accident. It gets the objects you pass into its own job context, runs independently, and reports completion or failure back through durable job history.
When to Use a Subtask
Use a subtask when another job should own part of the work.
Good reasons:
- a specialist agent should analyze or produce something separately
- the child work needs its own durable Python variables and history
- the parent should be notified when the child finishes
- the task has a separate result, permission boundary, or lifecycle
- a fleet should run many independent child jobs under a coordinator
Do not delegate just because a line of Python is long. If the parent can directly use existing objects and tools, keep the work in the same job.
What a Subtask Creates
At the object level, a subtask is built from:
- a child
Job - a
SubTaskHandler - an optional subtask
id - a
parent_jobreference on the child job - a
sub_task_handlerlocal inside the child job
The child job has its own PyEnv.local_state, console output, chat log, status, error state, and continuation fields. It can run at the same time as other jobs without sharing local variables unless you deliberately pass the same dbzero object or external resource.
Declaring a Subtask Factory
@subtask marks a Python callable as a subtask factory.
The callable returns either a child Job or an existing SubTaskHandler. STATEK wraps child jobs into handlers and makes the handler visible to the child job as sub_task_handler.
Conceptually:
from statek import subtask
@subtask
def summarize_thread(thread, **kwargs):
"""Start a specialist job that summarizes a thread."""
return create_summary_job(thread)Subtask factories must accept **kwargs. The framework manages an optional id parameter in the LLM-facing signature, so application subtask functions should not define their own id parameter.
Passing Context to the Child
A child job should receive only the context it needs.
summary_task = summarize_thread(thread)
calendar_task = inspect_calendar(calendar, today)Under the hood, delegated jobs can receive explicit shared variables:
user
thread
calendar
todayThose names become part of the child job's Python state. The parent keeps its own locals. If both jobs reference the same dbzero-backed object, they are intentionally working with the same durable application object.
SubTaskHandler
SubTaskHandler is the parent's handle for child work.
It stores:
job: the child jobid: an optional subtask identifierstate: the subtask stateresult: successful completion result, if one existserror:TaskError, if completion failed
The public state is state, not job status.
Subtask states are:
WAITING: the child job exists but has not startedSTARTED: the child job is active, suspended, or otherwise not explicitly completedCOMPLETED: the handler completed successfullyERROR: the handler completed with an error
The child job still has its own job status such as READY, STARTED, SUSPENDED, or DONE. The handler state is the parent's view of the delegated task outcome.
Completing a Subtask
A child job completes its handler with either a result or an error.
complete_sub_task(result=summary)or:
complete_sub_task(error="Could not summarize the thread.")A completion cannot mix a result and an error. A handler cannot be completed twice.
When a child job has a parent, completion notifies the parent. The parent can then inspect notification history or find a specific handler by id.
summary_task = find_sub_task_handler(id="summary")If the parent is active at the moment of notification, STATEK may buffer the notification until a safe boundary in the parent's history.
Waiting and Collecting Results
Parent jobs can wait conceptually for one child:
summary_task = summarize_thread(thread, id="summary")
summary = summary_task.resultor for several children:
summary_task = summarize_thread(thread, id="summary")
calendar_task = inspect_calendar(calendar, today, id="calendar")
summary = summary_task.result
calendar_findings = calendar_task.resultSubtask waiting can involve futures internally. That means the same caveat applies: current future readiness uses polling. For high-scale external event delivery, prefer callbacks and event queues where possible. Use subtasks when the work should genuinely run as another job.
Failure Behavior
Unfinished handlers are not results yet.
If code tries to coerce an unfinished handler to a string or result too early, STATEK raises instead of pretending the result exists.
Errored handlers expose a TaskError:
if summary_task.state == "ERROR":
print(summary_task.error.err_message)Successful handlers expose their result:
if summary_task.state == "COMPLETED":
report.add_section(summary_task.result)When a child job is created with a parent job, parent error handlers can be inherited by the child. That lets operational cleanup or failure handling follow delegated work. See Error Handling for ownership and duplicate-cleanup caveats.
Parent and Child Isolation
The parent and child are separate jobs.
The parent may have:
user
thread
reportThe child may have:
thread
question
sub_task_handlerThe child does not automatically see every local variable from the parent. Pass what it needs. This keeps delegated work inspectable and limits accidental coupling.
Subtasks Are Still Python Jobs
A subtask is useful because the child agent can execute normal Python:
events = calendar.events_for(today)
conflicts = find_conflicts(events)
complete_sub_task(result=conflicts)The child can call tools, use dbzero-backed objects, print output, suspend, resume, and keep its own history. The parent receives the result as a durable notification instead of managing every child step inline.
Subtasks coordinate durable jobs, but they do not make delegated side effects safe or deterministic. Use clear ownership, idempotency, permissions, auditability, and bounded concurrency for delegated work. See Security before giving child jobs broad tool access.
When Not to Delegate
Keep work in one job when the agent just needs to continue using the same Python variables.
Use a tool when the operation is a controlled capability:
move_meeting(meeting, new_slot)Use callbacks or events when the application is waiting for external input:
approval = incoming_human_approvalUse a subtask when another agent or child job should own the work, maintain its own durable state, and report a result back to the parent.
Where to go next
Read Jobs for lifecycle details, Futures for lower-level waiting, Callbacks and Interruptions for external events, and Practical Examples for dispatcher and worker patterns.