STATEK
Subtasks

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_job reference on the child job
  • a sub_task_handler local 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:

Define a Subtask Factory
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.

Start Child Tasks
summary_task = summarize_thread(thread)
calendar_task = inspect_calendar(calendar, today)

Under the hood, delegated jobs can receive explicit shared variables:

user
thread
calendar
today

Those 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 job
  • id: an optional subtask identifier
  • state: the subtask state
  • result: successful completion result, if one exists
  • error: TaskError, if completion failed

The public state is state, not job status.

Subtask states are:

  • WAITING: the child job exists but has not started
  • STARTED: the child job is active, suspended, or otherwise not explicitly completed
  • COMPLETED: the handler completed successfully
  • ERROR: 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 With a Result
complete_sub_task(result=summary)

or:

Complete With an Error
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.

Find a Completed Handler
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:

Collect One Child Result
summary_task = summarize_thread(thread, id="summary")
summary = summary_task.result

or for several children:

Collect Multiple Child Results
summary_task = summarize_thread(thread, id="summary")
calendar_task = inspect_calendar(calendar, today, id="calendar")
 
summary = summary_task.result
calendar_findings = calendar_task.result

Subtask 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:

Inspect a Subtask Error
if summary_task.state == "ERROR":
    print(summary_task.error.err_message)

Successful handlers expose their result:

Use a Completed 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
report

The child may have:

thread
question
sub_task_handler

The 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:

Return Child Findings
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_approval

Use 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.