Run, inspect & trace a workflow

The same loop works from three places: create a workflow from YAML, run it, read each step, and follow its events live. Pick the terminal, TypeScript, or plain HTTP. All three use your sk-tan-… API key.

The definition

triage.yml runs one agent in a sandbox each time its webhook is called:

yaml
name: triage-note
on:
  webhook: {}
do:
  - agent.run:
      profile:
        name: triager
        prompt:
          systemPrompt: "You triage bug reports into severity and owner."
      prompt: "Triage this report: ${trigger.text}"

See the YAML reference for every trigger and action.

From the terminal

bash
tangle workflows validate triage.yml          # compile check; exits 1 when invalid
tangle workflows apply triage.yml             # create, or update the workflow with this name
tangle workflows run <workflow-id> --wait \
  --input text="Login button does nothing on Safari"
tangle workflows runs <workflow-id>           # run history, newest first
tangle workflows run-detail <workflow-id> <run-id>   # every step, its output and cost
tangle workflows events <workflow-id> <run-id>       # follow a run live

--wait blocks until the run finishes and prints the run. A run that reaches a human decision step returns as waiting; resolve it with tangle workflows decision-resolve <workflow-id> <run-id> --choice approve.

Every workflows command accepts --json for scripts.

From TypeScript

Workflows live on the Tangle platform. @tangle-network/hub-sdk is its client:

bash
npm install @tangle-network/hub-sdk
ts
import { readFileSync } from "node:fs";
import { HubClient } from "@tangle-network/hub-sdk";

const hub = new HubClient({
  baseUrl: "https://id.tangle.tools",
  apiKey: process.env.TANGLE_API_KEY!,
});

const yaml = readFileSync("triage.yml", "utf8");
const check = await hub.workflows.validate(yaml);
if (!check.valid) throw new Error(JSON.stringify(check.errors));

const workflow = await hub.workflows.create(yaml);
// Keys are the trigger paths the YAML reads, after `trigger.`.
const { runId } = await hub.workflows.run(workflow.id, {
  text: "Login button does nothing on Safari",
});

for await (const event of hub.workflows.watchRun(workflow.id, runId)) {
  if (event.type === "action.started") console.log("step", event.index, event.kind);
  if (event.type === "token") process.stdout.write(event.delta);
  if (event.type === "action.finished") console.log("\nstep", event.index, event.status, event.costUsd);
  if (event.type === "run.waiting") console.log("waiting on decision", event.decisionId);
  if (event.type === "run.done") console.log("run", event.status, event.error ?? "");
}

const run = await hub.workflows.getRun(workflow.id, runId);
console.log(run.status, run.actionResults);

waitForRun(workflowId, runId, { timeoutMs }) polls instead of streaming. It returns as soon as the run leaves queued or running, which includes waiting on a decision.

Read an agent step's trace

An agent.run step records its execution timeline: messages, tool calls, and results, paged by round.

ts
let after: number | undefined;
do {
  const page = await hub.workflows.listRunSteps(workflow.id, runId, {
    action: 0,      // the step's position in the run
    iteration: 0,   // the round within that step
    after,
  });
  for (const step of page.steps) console.log(step);
  after = page.nextCursor ?? undefined;
} while (after !== undefined);

From HTTP

Base URL https://id.tangle.tools, header Authorization: Bearer $TANGLE_API_KEY. Responses are wrapped as { "success": true, "data": … }.

StepRequest
ValidatePOST /v1/workflows/validate with { "yaml": "…" }
CreatePOST /v1/workflows with { "yaml": "…" }
RunPOST /v1/workflows/{id}/run with { "inputs": { "text": "…" } }, returns { runId }
Run detailGET /v1/workflows/{id}/runs/{runId}
Live eventsGET /v1/workflows/{id}/runs/{runId}/events with Accept: text/event-stream
Agent stepsGET /v1/workflows/{id}/runs/{runId}/steps?action=0&iteration=0
CancelPOST /v1/workflows/{id}/runs/{runId}/cancel

A workflow with a webhook trigger also gets its own URL. Create returns the hook URL and its token once; the posted JSON object becomes ${trigger.*}, so { "text": "…" } fills ${trigger.text}:

bash
curl -X POST "https://id.tangle.tools/v1/hooks/$WORKFLOW_ID" \
  -H "Authorization: Bearer $HOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Login button does nothing on Safari"}'

Traces from sandboxes and fleets

A sandbox or fleet you drive directly keeps its own trace:

ts
const trace = await box.trace();                 // the sandbox's events and spans
await box.exportTrace({                          // push the bundle to your collector
  url: "https://otel.example.com/v1/traces",
  format: "otel-json",
  serviceName: "my-agents",
});

fleet.trace() and fleet.exportTrace() do the same for a fleet. format: "otel-json" sends OpenTelemetry JSON that any OTLP/HTTP collector accepts.