NewWacht Bench is live — AI-assisted development for Wacht

Agent runtime objects

Object hierarchy and API verbs for driving the Wacht agent runtime from code.

Agent runtime objects

This reference is for engineers wiring the agent runtime through the platform API, @wacht/backend, or the bench CLI. If you're using the console UI, see the Agents guide instead.

Hierarchy

actor
└── actor project              (binds an actor + an agent)
    ├── board item (task)      (description + schedule)
    └── thread                 (runtime conversation + tool calls)
ObjectIdentityAPI verbs
Actorsubject_type + external_keycreateActor, listActors, lookupActor
Agentid (per deployment)createAiAgent, getAiAgentById, updateAiAgent, deleteAiAgent
Actor projectid (per deployment); binds one actor + one agentcreateActorProjectFlat (query: actor_id; body: agent_id, name)
Board itemid (per project)createProjectTaskBoardItem, updateProjectTaskBoardItem, archiveProjectTaskBoardItem, cancelProjectTaskBoardItem, delegateProjectTask, downloadProjectTaskBoardItemFilesystemFile
Threadid (per project)createAgentThread, executeAgentThreadAsync, getAgentThreadById, getAgentThreadMessages
Session ticketone-time ticket stringcreateBackendSessionTicket, createSessionTicket

The agent record has no instructions or schedule field. Task content lives on the board item; that's why a single agent can run many different tasks.

Resolving an actor

lookupActor finds an actor by its (subject_type, external_key) pair — the same pair used at creation. Use it when your service holds the external identifier and needs the Wacht actor id (e.g. to attach a project). Returns { actor: null } when no match exists rather than 404.

import { ai } from "@wacht/backend";

const { actor } = await ai.lookupActor({
  subject_type: "user",
  external_key: "ext-1234",
});
if (!actor) {
  // No actor for that external key yet — call createActor.
}
use wacht::models::LookupActorParams;

let response = client
    .ai()
    .actors()
    .lookup_actor(LookupActorParams {
        subject_type: "user".into(),
        external_key: "ext-1234".into(),
    })
    .send()
    .await?;
let actor = response.actor;

Schedule kinds

board_item.schedule_kind (from models::project_task_schedule::schedule_kind):

  • ONCE — runs at next_run_at, then stops.
  • INTERVAL — runs at next_run_at, then repeats every interval_seconds.

Tool types

AiTool.configuration is a discriminated union — the type field selects the variant:

typeUse forPersisted via
ApiAn HTTP endpoint you own. Stores endpoint, method, auth, request/URL schemas.createAiTool
CodeRunnerSandboxed code execution.createAiTool
InternalBuilt-in platform tools the runtime ships.(read-only catalog)
McpA self-hosted MCP server you control.createAiTool
PlatformEventTools that react to platform events.createAiTool
VirtualThird-party SaaS via Composio.Not created directly — surfaces at runtime once enableComposioApp is called and the actor authorizes the toolkit.

wacht api describe createAiTool shows the full per-variant payload schema for the persisted variants. See Composio for the Virtual flow.

Delegating between threads

A board item can delegate work to a different lane thread within the same project. The target thread takes over execution; the new board item carries forward the parent's project context and is routed based on capability_tags.

import { ai } from "@wacht/backend";

const delegation = await ai.delegateProjectTask(projectId, {
  target_lane_thread_id: "thread_id",
  title: "Run the data-validation lane",
  description: "Validate the rows produced by the ingestion thread.",
  capability_tags: ["data-validation"],
});
// delegation.task_key, delegation.board_item_id, delegation.assigned_agent_id
use wacht::models::DelegateProjectTaskRequest;

let delegation = client
    .ai()
    .actor_projects()
    .delegate_task(
        project_id,
        DelegateProjectTaskRequest {
            target_lane_thread_id: "thread_id".into(),
            title: "Run the data-validation lane".into(),
            description: Some("Validate the rows produced by the ingestion thread.".into()),
            capability_tags: Some(vec!["data-validation".into()]),
        },
    )
    .send()
    .await?;

Empty / omitted capability_tags match any agent on the target lane.

Downloading board-item artifacts

Each board item has a workspace filesystem. Listing and reading metadata uses listProjectTaskBoardItemFilesystem / getProjectTaskBoardItemFilesystemFile (returns JSON). To pull the bytes directly — including the server's Content-Type and a suggested filename — use the download variant:

import { ai } from "@wacht/backend";

const file = await ai.downloadProjectTaskBoardItemFilesystemFile(
  projectId,
  itemId,
  "/output/report.pdf",
);
// file.data is a Uint8Array; file.mime_type and file.file_name come from response headers.
let file = client
    .ai()
    .actor_projects()
    .download_board_item_filesystem_file(project_id, item_id, "/output/report.pdf")
    .send()
    .await?;
// file.data: Vec<u8>; file.mime_type / file.file_name parsed from headers.

Sharing a session

POST /session/tickets (createBackendSessionTicket server-side, createSessionTicket console-side) issues four ticket types — each redirects to a different vanity surface:

ticket_typeURLRequired fields
agent_accesshttps://{backend_host}/vanity/agents?ticket=…agent_ids, actor_id
impersonationhttps://{frontend_host}/sign-in?ticket=…user_id
api_auth_accesshttps://{backend_host}/vanity/api-auth?ticket=…api_auth_app_slug
webhook_app_accesshttps://{backend_host}/vanity/webhook?ticket=…webhook_app_slug

Tickets are one-time-redeemable and bounded by expires_in (seconds). See the Agent sessions guide for scoping fields and the per-product vanity-pages guides (API auth, Webhooks) for non-agent flows.

On this page