Agent & Workflow API

Endpoints for managing agents, workflows, triggers, schedules, goals, and cron jobs.

Agent Endpoints

GET /api/agents

List all running agents.

Response 200 OK:

[
  {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "hello-world",
    "state": "Running",
    "created_at": "2025-01-15T10:30:00Z",
    "model_provider": "groq",
    "model_name": "llama-3.3-70b-versatile"
  }
]

POST /api/agents

Spawn a new agent from a TOML manifest.

Request Body (JSON):

{
  "manifest_toml": "name = \"my-agent\"\nversion = \"0.1.0\"\ndescription = \"Test agent\"\nauthor = \"me\"\nmodule = \"builtin:chat\"\n\n[model]\nprovider = \"groq\"\nmodel = \"llama-3.3-70b-versatile\"\n\n[capabilities]\ntools = [\"file_read\", \"web_fetch\"]\nmemory_read = [\"*\"]\nmemory_write = [\"self.*\"]\n"
}

Response 201 Created:

{
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "my-agent"
}

POST /api/agents/bulk

Bulk create multiple agents in a single request.

DELETE /api/agents/bulk

Bulk delete multiple agents by ID list.

POST /api/agents/bulk/start

Start multiple stopped agents simultaneously.

POST /api/agents/bulk/stop

Stop multiple running agents simultaneously.

GET /api/agents/{id}

Returns detailed information about a single agent.

Response 200 OK:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "hello-world",
  "state": "Running",
  "created_at": "2025-01-15T10:30:00Z",
  "session_id": "s1b2c3d4-...",
  "model": {
    "provider": "groq",
    "model": "llama-3.3-70b-versatile"
  },
  "capabilities": {
    "tools": ["file_read", "file_list", "web_fetch"],
    "network": []
  },
  "description": "A friendly greeting agent",
  "tags": []
}

DELETE /api/agents/{id}

Terminate an agent and remove it from the registry.

Response 200 OK:

{
  "status": "killed",
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

PATCH /api/agents/{id}

Partially update an agent's fields.

PUT /api/agents/{id}/update

Update an agent's configuration at runtime.

Request Body:

{
  "description": "Updated description",
  "system_prompt": "You are a specialized assistant.",
  "tags": ["updated", "v2"]
}

Response 200 OK:

{
  "status": "updated",
  "agent_id": "a1b2c3d4-..."
}

PUT /api/agents/{id}/mode

Set an agent's operating mode. Stable mode pins the current model and freezes the skill registry. Normal mode restores default behavior.

Request Body:

{
  "mode": "Stable"
}

Response 200 OK:

{
  "status": "updated",
  "mode": "Stable",
  "agent_id": "a1b2c3d4-..."
}

PATCH /api/agents/{id}/identity

Update an agent's identity fields (name, avatar, persona).

PATCH /api/agents/{id}/config

Partially patch an agent's configuration object.

POST /api/agents/{id}/clone

Clone an existing agent into a new one with a fresh session.

POST /api/agents/{id}/message

Send a message to an agent and receive the complete response.

Request Body:

{
  "message": "What files are in the current directory?",
  "session_id": "optional-session-uuid"
}

Optional session_id: per-request override of which session this message lands in. When omitted, the agent's current session is used. When supplied, the call is routed into that specific session (the session must already exist for the agent), letting one agent serve many parallel chats from a single client without POST /sessions/{id}/switch round-trips first. Useful for fan-out backends and multi-tab dashboards.

Response 200 OK:

{
  "response": "Here are the files in the current directory:\n- Cargo.toml\n- README.md\n...",
  "input_tokens": 142,
  "output_tokens": 87,
  "iterations": 1
}

POST /api/agents/{id}/message/stream

Send a message and receive the response as a Server-Sent Events stream (see SSE Streaming).

GET /api/agents/{id}/attach

Attach as an additional read-only subscriber to the current session's SSE stream — useful for multi-client co-watching when multiple surfaces (CLI, Tauri desktop, browser tabs) need to follow the same turn live.

POST /message/stream itself remains single-consumer (the originating caller); this endpoint is fed by a per-session SessionStreamHub that tees every produced StreamEvent to the original caller plus any number of attached subscribers. Subscribers do not advance the turn; they only observe.

Behaviour:

  • The connection stays open for the lifetime of the current turn. When the turn ends, the server closes the stream cleanly.
  • If no turn is in flight when you attach, the connection waits for the next one.
  • A single agent can have many concurrent attachers without slowing the producer (broadcast channel; fan-out is constant-cost per event).
  • The event format is identical to /message/stream, so existing parsers work unchanged.
# Co-watch the current turn from a second client
curl -N 'http://127.0.0.1:4545/api/agents/$AGENT/attach' \
  -H 'Authorization: Bearer $LIBREFANG_API_KEY'

GET /api/agents/{id}/session

Returns the agent's current conversation history.

Response 200 OK:

{
  "session_id": "s1b2c3d4-...",
  "agent_id": "a1b2c3d4-...",
  "message_count": 4,
  "context_window_tokens": 1250,
  "messages": [
    {
      "role": "User",
      "content": "Hello"
    },
    {
      "role": "Assistant",
      "content": "Hello! How can I help you?"
    }
  ]
}

GET /api/agents/{id}/sessions

List all sessions for an agent.

POST /api/agents/{id}/sessions

Create a new session for an agent.

POST /api/agents/{id}/sessions/{session_id}/switch

Switch the agent to use a different session by ID.

GET /api/agents/{id}/sessions/by-label/{label}

Find a session by its user-assigned label.

DELETE /api/agents/{id}/history

Clear all conversation history for an agent without creating a new session.

POST /api/agents/{id}/session/reset

Reset an agent's session, clearing all conversation history.

Response 200 OK:

{
  "status": "reset",
  "agent_id": "a1b2c3d4-...",
  "new_session_id": "s5e6f7g8-..."
}

POST /api/agents/{id}/session/compact

Trigger LLM-based session compaction. The agent's conversation is summarized by an LLM, keeping only the most recent messages plus a generated summary.

Response 200 OK:

{
  "status": "compacted",
  "message": "Session compacted: 80 messages summarized, 20 kept"
}

POST /api/agents/{id}/stop

Cancel the agent's current LLM run. Aborts any in-progress generation.

Response 200 OK:

{
  "status": "stopped",
  "message": "Agent run cancelled"
}

PUT /api/agents/{id}/model

Switch an agent's LLM model at runtime.

Request Body:

{
  "model": "claude-sonnet-4-20250514"
}

Response 200 OK:

{
  "status": "updated",
  "model": "claude-sonnet-4-20250514"
}

GET /api/agents/{id}/tools

Get an agent's current tool list.

PUT /api/agents/{id}/tools

Replace an agent's tool list at runtime.

GET /api/agents/{id}/skills

Get an agent's current skill list.

PUT /api/agents/{id}/skills

Replace an agent's skill list at runtime.

GET /api/agents/{id}/mcp_servers

Get the MCP servers attached to a specific agent.

PUT /api/agents/{id}/mcp_servers

Set the MCP servers attached to a specific agent.

GET /api/agents/{id}/traces

Get execution traces for an agent (tool calls, LLM iterations, timings).

GET /api/agents/{id}/metrics

Get runtime metrics for an agent (token counts, latency, error rates).

GET /api/agents/{id}/logs

Get recent log lines emitted by an agent's process.

GET /api/agents/{id}/deliveries

Get inbound message deliveries received by an agent from channels.

GET /api/agents/{id}/files

List files stored in an agent's private file workspace.

GET /api/agents/{id}/files/{filename}

Download a specific file from an agent's workspace.

PUT /api/agents/{id}/files/{filename}

Upload or overwrite a file in an agent's workspace.

DELETE /api/agents/{id}/files/{filename}

Delete a file from an agent's workspace.

POST /api/agents/{id}/upload

Upload a file to an agent's workspace (multipart form data).

GET /api/uploads/{file_id}

Retrieve a previously uploaded file by its upload ID.

Supported Attachment Types

When attachments are referenced from a POST /api/agents/{id}/message (or the streaming variant) body, the backend resolves each file_id to a content block before invoking the LLM. Three content classes are recognized:

ClassDetectionBlock produced
ImageContent-Type: image/* (png, jpeg, webp, gif)Inline base64 image block
PDFContent-Type: application/pdfText block prefixed with [Attached PDF: <filename>] containing extracted plain text
Text / codeAny text/* MIME, the listed application/* JSON/XML/YAML/TOML/JS/TS/SQL/GraphQL types, or a recognized file extension when the browser left the MIME blankText block prefixed with [Attached file: <filename>] containing the UTF-8 contents

Recognized text/code extensions include .txt, .md, .markdown, .rst, .csv, .tsv, .log, .json, .yaml, .yml, .toml, .xml, .ini, .conf, .cfg, .env, .html, .css, .js, .ts, .tsx, .jsx, .vue, .svelte, .py, .rs, .go, .java, .kt, .swift, .scala, .c, .cpp, .h, .rb, .php, .lua, .r, .sh, .bash, .sql, .graphql, .proto, .ipynb, Dockerfile, Makefile, and others.

Limits:

  • PDF text extraction and text-file reads are both truncated at 200,000 characters.
  • Scanned/image-only PDFs surface as a short note explaining that no text was extractable, so the model still sees the attachment exists.
  • Files that match none of the three classes are skipped with a warn log; the LLM call proceeds with the remaining attachments.

Note for local models: when a user message contains multiple text segments (e.g. attachment header + user prompt), the runtime coalesces adjacent same-role text blocks at the session-repair layer. This normalization happens before any driver call, so small chat-tuned models served via Ollama / vLLM / LM Studio see a single user text run plus images and continue to attend to the attachment correctly.

GET /api/agents/{id}/ws

WebSocket connection for real-time bidirectional chat (see WebSocket Protocol).

GET /api/agents/{id}/memory/export

Export all memory entries for an agent as JSON.

POST /api/agents/{id}/memory/import

Import memory entries for an agent from JSON.

Owner Notices

Agents can send the operator a structured private side-channel message in addition to the user-facing reply. The notify_owner built-in tool collects one or more notices during the turn; the runtime aggregates them onto a ReplyEnvelope.owner_notice: Option<String> field.

The notice surfaces in three places:

  • POST /message response body: a top-level owner_notice string alongside response.
  • POST /message/stream / GET /attach SSE: a StreamEvent::OwnerNotice { text } event right before the final Done.
  • WhatsApp gateway: when the OWNER_JIDS env var is set (comma-separated JIDs), each notice is fanned out to that owner set. Other channel adapters can opt in similarly.

Use it for "this user asked something I can't answer in public — escalate to ops" flows or any audit trail the user shouldn't see in the chat.

GET /api/agents/{id}/sessions/{session_id}/trajectory

Export a redacted trajectory (audit trail) for a single session. The response bundles the session messages plus minimal metadata (agent name, model, system-prompt fingerprint, librefang version) and runs every message through the privacy redactor before serialisation. Intended for support, audit, and compliance flows where you need to share a session without leaking secrets.

Query parameters:

  • format=json (default): single JSON object, content-type application/json.
  • format=jsonl: NDJSON — first line is the metadata header, every subsequent line is one message; content-type application/x-ndjson. Useful for streaming into log pipelines or jq filters.

Response statuses:

  • 200 — trajectory bundle.
  • 400 — agent or session id failed UUID parsing.
  • 404 — agent or session does not exist.
curl 'http://127.0.0.1:4545/api/agents/$AGENT/sessions/$SESSION/trajectory?format=jsonl' \
  -H 'Authorization: Bearer $LIBREFANG_API_KEY' > session.jsonl

Workflow Endpoints

GET /api/workflows

List all registered workflows.

Response 200 OK:

[
  {
    "id": "w1b2c3d4-...",
    "name": "code-review-pipeline",
    "description": "Automated code review workflow",
    "steps": 3,
    "created_at": "2025-01-15T10:30:00Z"
  }
]

POST /api/workflows

Create a new workflow definition.

Request Body (JSON):

{
  "name": "code-review-pipeline",
  "description": "Review code changes with multiple agents",
  "steps": [
    {
      "name": "analyze",
      "agent_name": "coder",
      "prompt": "Analyze this code for potential issues: {&lbrace;input&rbrace;}",
      "mode": "sequential",
      "timeout_secs": 120,
      "error_mode": "fail",
      "output_var": "analysis"
    }
  ]
}

Step configuration options:

FieldTypeDescription
namestringStep name
agent_idstringAgent UUID (use either this or agent_name)
agent_namestringAgent name (use either this or agent_id)
promptstringPrompt template with {&lbrace;input&rbrace;} and {&lbrace;output_var&rbrace;} placeholders
modestring"sequential", "fan_out", "collect", "conditional", "loop"
timeout_secsintegerTimeout per step (default: 120)
error_modestring"fail", "skip", "retry"
max_retriesintegerFor "retry" error mode (default: 3)
output_varstringVariable name to store output for later steps
conditionstringFor "conditional" mode
max_iterationsintegerFor "loop" mode (default: 5)
untilstringFor "loop" mode: stop condition

Response 201 Created:

{
  "workflow_id": "w1b2c3d4-..."
}

GET /api/workflows/{id}

Get a specific workflow definition.

PUT /api/workflows/{id}

Update an existing workflow definition.

DELETE /api/workflows/{id}

Delete a workflow definition.

POST /api/workflows/{id}/run

Execute a workflow.

Request Body:

{
  "input": "Review this pull request: ..."
}

Response 200 OK:

{
  "run_id": "r1b2c3d4-...",
  "output": "Code review summary:\n- No critical issues found\n...",
  "status": "completed"
}

GET /api/workflows/{id}/runs

List execution history for a workflow.

Response 200 OK:

[
  {
    "id": "r1b2c3d4-...",
    "workflow_name": "code-review-pipeline",
    "state": "Completed",
    "steps_completed": 3,
    "started_at": "2025-01-15T10:30:00Z",
    "completed_at": "2025-01-15T10:32:15Z"
  }
]

Trigger Endpoints

Trigger response routing

Trigger-fired responses route to the agent's home channel — the channel the agent is registered against under [channels]. Earlier behaviour silently dropped the response when no caller channel was on the call path (cron-style fires, scheduler events). Now the response always lands somewhere visible: the user-facing surface the agent owns.

assignee_match:self filter on task_posted triggers

A task_posted trigger can opt to fire only when the task is assigned to the agent itself (matching by agent id):

{
  "pattern": {
    "task_posted": {
      "assignee_match": "self"
    }
  }
}

assignee_match accepts:

  • "self" — fire only when task.assignee_id == agent.id. Use this for "I should work on tasks assigned to me" agents.
  • "any" (default) — legacy behaviour; fires on every task_posted event regardless of assignee.

GET /api/triggers

List all triggers. Optionally filter by agent.

Query Parameters:

  • agent_id (optional): Filter by agent UUID

Response 200 OK:

[
  {
    "id": "t1b2c3d4-...",
    "agent_id": "a1b2c3d4-...",
    "pattern": {"lifecycle": {}},
    "prompt_template": "Event: {&lbrace;event&rbrace;}",
    "enabled": true,
    "fire_count": 5,
    "max_fires": 0,
    "created_at": "2025-01-15T10:30:00Z",
    "cooldown_secs": 60,
    "session_mode": "persistent",
    "target_agent_id": null
  }
]

GET /api/triggers/{id}

Retrieve a single trigger by ID.

Response 200 OK: same shape as a single item from the list above.

Response 404 Not Found if the trigger does not exist.

POST /api/triggers

Create a new event trigger.

Request Body:

{
  "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "pattern": {
    "agent_spawned": {
      "name_pattern": "*"
    }
  },
  "prompt_template": "A new agent was spawned: {&lbrace;event&rbrace;}. Review its capabilities.",
  "max_fires": 0,
  "cooldown_secs": 60,
  "session_mode": "persistent",
  "target_agent_id": "b2c3d4e5-..."
}

All fields except agent_id, pattern, and prompt_template are optional.

FieldTypeDefaultDescription
agent_idUUIDOwner agent. The trigger is registered under this agent.
patternobjectEvent pattern that activates the trigger (see pattern types below).
prompt_templatestringMessage sent to the agent when the event fires. Supports {&lbrace;event&rbrace;} placeholder.
max_firesinteger0Maximum times this trigger may fire. 0 = unlimited.
cooldown_secsintegerengine defaultMinimum seconds between fires. Omit to use the engine default.
session_mode"persistent" | "new"agent defaultSession continuity for each fire. Omit to inherit from the agent manifest.
target_agent_idUUIDnullRoute the fired message to a different agent instead of the owner.

Supported pattern types:

PatternDescription
{"lifecycle": {}}All lifecycle events
{"agent_spawned": {"name_pattern": "*"}}Agent spawn events
{"agent_terminated": {}}Agent termination events
{"all": {}}All events

Response 201 Created:

{
  "trigger_id": "t1b2c3d4-...",
  "agent_id": "a1b2c3d4-..."
}

PATCH /api/triggers/{id}

Partially update a trigger. All fields are optional — only provided fields are changed.

Pass null for cooldown_secs, session_mode, or target_agent_id to clear the override and revert to the default. Omitting a field leaves it unchanged.

Request Body:

{
  "prompt_template": "Updated: {&lbrace;event&rbrace;}",
  "enabled": true,
  "max_fires": 10,
  "cooldown_secs": 120,
  "session_mode": "new",
  "target_agent_id": "b2c3d4e5-..."
}

Response 200 OK: the full updated trigger object (same shape as GET /api/triggers/&lbrace;id&rbrace;).

Response 404 Not Found if the trigger does not exist.

DELETE /api/triggers/{id}

Remove a trigger.

Response 200 OK:

{
  "status": "removed",
  "trigger_id": "t1b2c3d4-..."
}

Schedule Endpoints

Time-based agent activation using cron expressions or interval syntax.

GET /api/schedules

List all configured schedules.

POST /api/schedules

Create a new schedule.

Request Body:

{
  "agent_id": "a1b2c3d4-...",
  "cron": "0 9 * * 1-5",
  "prompt": "Good morning! Summarize overnight events.",
  "enabled": true
}

GET /api/schedules/{id}

Get a specific schedule.

PUT /api/schedules/{id}

Update a schedule.

DELETE /api/schedules/{id}

Delete a schedule.

POST /api/schedules/{id}/run

Trigger a schedule to run immediately (ignores its cron expression for this one invocation).


Goals Endpoints

Hierarchical goal tracking system for multi-step agent objectives.

GET /api/goals

List all goals.

POST /api/goals

Create a new goal.

Request Body:

{
  "title": "Migrate database to PostgreSQL",
  "description": "Complete migration with zero downtime",
  "agent_id": "a1b2c3d4-...",
  "parent_id": null,
  "due_at": "2025-02-01T00:00:00Z"
}

GET /api/goals/{id}

Get a specific goal.

PUT /api/goals/{id}

Update a goal (title, status, description, due date).

DELETE /api/goals/{id}

Delete a goal and all its children.

GET /api/goals/{id}/children

List sub-goals for a given parent goal.


Cron Endpoints

Schedule recurring jobs using standard cron expression syntax.

GET /api/cron/jobs

List all cron jobs.

POST /api/cron/jobs

Create a new cron job.

Request Body:

{
  "name": "daily-digest",
  "schedule": "0 8 * * *",
  "agent_id": "a1b2c3d4-...",
  "prompt": "Prepare the daily digest of overnight activity.",
  "enabled": true
}

GET /api/cron/jobs/{id}

Get details for a specific cron job.

PUT /api/cron/jobs/{id}

Update a cron job (schedule, prompt, agent, etc.).

DELETE /api/cron/jobs/{id}

Delete a cron job.

PUT /api/cron/jobs/{id}/enable

Toggle a cron job's enabled state.

Request Body:

{
  "enabled": false
}

GET /api/cron/jobs/{id}/status

Get the last run result and next scheduled time for a cron job.


Interrupting a Running Agent Turn

Source: librefang-runtime/src/interrupt.rs

A SessionInterrupt is an Arc<AtomicBool> signal that the agent loop checks at every tool-call boundary. Setting the flag causes the loop to exit cleanly after the current tool call completes — it does not forcibly kill threads or cancel in-flight network requests.

Cascade into agent_send subagents: a parent /stop now propagates to every subagent the parent spawned via the agent_send tool. The cascade walks the parent → subagent edges built when each agent_send call ran, so a single /stop against the user-facing agent stops the entire fan-out tree it kicked off — no orphaned subagent loops continuing after the user gave up. Fully recursive: subagents that themselves called agent_send propagate further.

Interrupt Endpoint

POST /api/agents/{agent_id}/sessions/{session_id}/interrupt

No request body is required. The response is 204 No Content when the interrupt signal was accepted.

curl -X POST \
  "http://127.0.0.1:4545/api/agents/${AGENT_ID}/sessions/${SESSION_ID}/interrupt"

Behavior

SituationWhat happens
Agent is between tool callsLoop exits at the next boundary check. The partial response up to that point is recorded.
Agent is mid-LLM-callThe LLM call completes normally, then the loop checks the flag and exits before executing the next tool.
Agent is mid-tool-executionThe tool runs to completion, then the loop exits. No tool call is abandoned mid-flight.
Agent has already finishedThe interrupt flag is ignored; no error is returned.
No active turn204 is still returned; the flag is set and will take effect on the next turn.

Automatic Clearing

The interrupt flag is automatically cleared when:

  • A new incoming message starts a fresh agent turn
  • The session is reset

You do not need to send a "resume" or "clear" request after the interrupted turn ends.

Use Cases

  • User-initiated stop: A user presses a "Stop" button in a UI. The frontend calls the interrupt endpoint and the agent stops after the current tool call.
  • Watchdog timeout: An external monitor detects a long-running turn and interrupts it to avoid token budget exhaustion.
  • Graceful shutdown: Before restarting the daemon, interrupt all active sessions so they record a clean stop rather than an abrupt disconnect.