ZyrisDocs
attacca_api reference

Jobs and works

attacca_api autonomous work — queue a job and answer its questions, or plan a work into a task graph and let it through both approval gates.

A session is a conversation. When a node wants something done rather than discussed, it reaches for a job or a work. Both run on ordinary sessions, so everything on Sessions and turns — send_message, turn_events — works on them too.

JobWork
What it isOne message handed to an agent that runs it to completion on its own session, reporting long results as artifacts.A goal planned into a graph of tasks, each executed in its own git worktree and merged into a shared integration branch.
StartsOn creation — it comes back already processing.Planning starts on creation, then it stops at two approval gates.
Scopesjobs:read, jobs:writeworks:read, works:write

Jobs

ToolBehaviour
list_jobs(filter: ZJobFilter) -> Vec<ZJob>Newest first. ZJobFilter { project_id: Option, limit: Option<u32> }.
get_job(job_id) -> ZJobRead one.
create_job(job: ZNewJob) -> ZJobQueue and start. The first turn is enqueued before the call returns.
update_job(job_id, update: ZJobUpdate) -> ZJobEdit title, description or project_id; omitted fields are left alone. State cannot be set.
delete_job(job_id) -> ()Delete it.
ZNewJob fieldTypeNotes
messageStringWhat the agent is asked to do; also what the job is titled from.
agent_idOption<String>Omit to run on the account's Main Agent.
project_idOption<String>Omit for the default project.
timezoneOption<String>IANA name, e.g. Europe/Berlin — what the agent answers "what time is it" with. Omit for the deployment's own.
planningboolDraft a plan and turn it into a work instead of doing the work here.
plan_modeboolInvestigate, hand back a plan and wait for approval before executing — all within this one job.
dataVec<Datum>Attachments for the opening message. Text datums are folded into message; files land in the account's workspace.
ZJob fieldTypeNotes
id, titleStringThe title is generated in the background, so a job read back at once still carries Attacca's placeholder.
descriptionOption<String>
stateZJobStateDerived by Attacca from the job's session.
project_id, agent_idOption<String>
session_idOption<String>The session driving the job: send_message to it to answer a question, turn_events on it to watch.
last_activityOption<String>A one-line "what the agent is doing right now".
failure_reasonOption<String>Set only when failed.
planningboolA planning job.
created_at, updated_atOption<String>RFC 3339.
ZJobStateMeaning
backlogQueued.
processingRunning.
doneFinished.
ambiguousThe agent asked a question and is waiting — answer with send_message to session_id.
failedEnded; see failure_reason.
recurringA recurring job.
awaiting_planA plan_mode job has produced a plan and waits for it to be approved.

Watch a job with turn_events on its session_id, or poll get_job:

use std::time::Duration;
use zyris_attacca::{AttaccaApi, AttaccaApiClient, ZJobState, ZNewJob};

async fn run_job(api: &AttaccaApiClient) -> zyris::Result<()> {
    let job = api
        .create_job(ZNewJob {
            message: "Summarize this week's changelog".into(),
            agent_id: None,   // the account's Main Agent
            project_id: None, // the default project
            timezone: Some("Europe/Berlin".into()),
            planning: false,
            plan_mode: false,
            data: vec![],
        })
        .await?;

    loop {
        tokio::time::sleep(Duration::from_secs(5)).await;
        let job = api.get_job(job.id.clone()).await?;
        match job.state {
            ZJobState::Done => return Ok(()),
            ZJobState::Failed => {
                eprintln!("failed: {}", job.failure_reason.unwrap_or_default());
                return Ok(());
            }
            ZJobState::Ambiguous => {
                // The agent asked something; answer on the job's own session.
                let session_id = job.session_id.expect("a running job has a session");
                api.send_message(session_id, "Only the public changes.".into(), vec![]).await?;
            }
            _ => println!("{:?}: {}", job.state, job.last_activity.unwrap_or_default()),
        }
    }
}

A real node would remember that it already answered rather than replying on every poll while the state catches up.

Works

A work pauses twice for approval, which is why creating one is not enough to make it run: a node that creates a work and walks away has created something that never runs.

draft → checking_requirements → awaiting_goal_approval ─approve_work_goal→ planning
      → awaiting_plan_approval ─approve_work_plan→ executing ⇄ halted → verifying → done
                                                   (or failed / cancelled)
ZWorkStateMeaning
draft, checking_requirementsBeing set up.
awaiting_goal_approvalGate 1. The goal is drafted; approve it before planning starts.
planningDrawing up the task graph.
awaiting_plan_approvalGate 2. The task graph is drafted; approve it before anything executes.
executingTasks running.
haltedA phase set to stop for review finished. continue_work resumes.
verifying, done, failed, cancelled
ToolBehaviour
list_works(filter: ZWorkFilter) -> Vec<ZWork>Newest first. ZWorkFilter { project_id: Option, limit: Option<u32> }.
get_work(work_id) -> ZWorkRead one.
create_work(work: ZNewWork) -> ZWorkCreate and start planning. Settles at awaiting_goal_approval.
update_work(work_id, update: ZWorkUpdate) -> ZWorkEdit title, description or project_id. State cannot be set.
delete_work(work_id) -> ()Delete it.
approve_work_goal(work_id) -> ZWorkThrough gate 1: accept the drafted goal and start planning. Read final_goal and requirements_report first — that is what is being approved.
approve_work_plan(work_id) -> ZWorkThrough gate 2: accept the task graph and begin executing. Read it first with work_tasks.
work_tasks(work_id) -> ZWorkTasksThe task graph. Empty, not an error, before planning has run.
stop_work(work_id) -> ()Stop a running work and every task under it.
continue_work(work_id) -> ZWorkResume a stopped, halted or failed work: finished tasks are kept, the rest re-run.
work_message(work_id, message, data: Vec<Datum>) -> ()Talk to the planner — steer the goal at gate 1 or the plan at gate 2. Exactly send_message on planner_session_id, without reading the work first; the reply streams back the same way.
ZNewWork fieldTypeNotes
messageStringThe goal, in prose. The title is derived from its first line.
agent_idOption<String>Omit to plan with the Main Agent.
project_idOption<String>Omit for the default project. Tasks run against the project's checkout, so this decides what the work may change.
ZWork fieldTypeNotes
id, titleString
descriptionOption<String>
stateZWorkState
project_id, agent_idOption<String>
planner_session_idOption<String>The planning conversation, an ordinary session.
final_goalOption<String>The measurable success criterion, settled at gate 1.
requirements_reportOption<String>Blockers found up front — a missing toolchain, a login the agent cannot perform.
integration_branchOption<String>The git branch task worktrees merge into. Set once execution starts.
failure_reason, last_activityOption<String>
created_at, updated_atOption<String>RFC 3339.

The task graph

ZWorkTasks { tasks: Vec<ZTask>, deps: Vec<ZTaskDep> }. An edge ZTaskDep { upstream_task_id, downstream_task_id } means the downstream task may not start until the upstream one is done; tasks with no edge between them run in parallel.

ZTask fieldTypeNotes
id, work_id, title, descriptionString
stateZTaskState
phase_idOption<String>
main_session_idOption<String>The session doing the work — turn_events on it follows one task.
branchOption<String>The task's own git branch.
micro_measurablesserde_json::Value[{ "text": String, "met": bool }], the checks the task must satisfy. Untyped because it is the planner's shape.
verification_result, failure_reason, last_activityOption<String>
created_at, updated_atOption<String>RFC 3339.

ZTaskState: pending; blocked (an upstream task failed); awaiting_input (the agent asked a question in the task's own session); creating, self_verifying, verifying, recording (the agent role holding it); merging; done; failed; conflicted (the merge into the integration branch hit a conflict).

Letting a work through both gates

use std::time::Duration;
use zyris_attacca::{AttaccaApi, AttaccaApiClient, ZNewWork, ZWork, ZWorkState};

async fn settle(api: &AttaccaApiClient, work_id: &str, gate: ZWorkState) -> zyris::Result<ZWork> {
    loop {
        let work = api.get_work(work_id.to_string()).await?;
        if work.state == gate || matches!(work.state, ZWorkState::Failed | ZWorkState::Cancelled) {
            return Ok(work);
        }
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
}

async fn run_work(api: &AttaccaApiClient, project_id: String) -> zyris::Result<()> {
    let work = api
        .create_work(ZNewWork {
            message: "Make every fixture round-trip\n\nAdd the missing serde tests.".into(),
            agent_id: None,
            project_id: Some(project_id),
        })
        .await?;

    // Gate 1: the goal.
    let work = settle(api, &work.id, ZWorkState::AwaitingGoalApproval).await?;
    println!("goal: {:?}", work.final_goal);
    println!("blockers: {:?}", work.requirements_report);
    api.approve_work_goal(work.id.clone()).await?;

    // Gate 2: the task graph.
    let work = settle(api, &work.id, ZWorkState::AwaitingPlanApproval).await?;
    let graph = api.work_tasks(work.id.clone()).await?;
    for task in &graph.tasks {
        println!("{} {:?} {}", task.id, task.state, task.title);
    }
    for dep in &graph.deps {
        println!("{} -> {}", dep.upstream_task_id, dep.downstream_task_id);
    }
    api.approve_work_plan(work.id).await?;
    Ok(())
}

In practice a person reads the goal and the plan before each approval; work_message is how to ask for changes instead of approving.

On this page