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:
| Class | Detection | Block produced |
|---|---|---|
| Image | Content-Type: image/* (png, jpeg, webp, gif) | Inline base64 image block |
Content-Type: application/pdf | Text block prefixed with [Attached PDF: <filename>] containing extracted plain text | |
| Text / code | Any 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 blank | Text 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 /messageresponse body: a top-levelowner_noticestring alongsideresponse.POST /message/stream/GET /attachSSE: aStreamEvent::OwnerNotice { text }event right before the finalDone.- WhatsApp gateway: when the
OWNER_JIDSenv 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-typeapplication/json.format=jsonl: NDJSON — first line is the metadata header, every subsequent line is one message; content-typeapplication/x-ndjson. Useful for streaming into log pipelines orjqfilters.
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: {{input}}",
"mode": "sequential",
"timeout_secs": 120,
"error_mode": "fail",
"output_var": "analysis"
}
]
}
Step configuration options:
| Field | Type | Description |
|---|---|---|
name | string | Step name |
agent_id | string | Agent UUID (use either this or agent_name) |
agent_name | string | Agent name (use either this or agent_id) |
prompt | string | Prompt template with {{input}} and {{output_var}} placeholders |
mode | string | "sequential", "fan_out", "collect", "conditional", "loop" |
timeout_secs | integer | Timeout per step (default: 120) |
error_mode | string | "fail", "skip", "retry" |
max_retries | integer | For "retry" error mode (default: 3) |
output_var | string | Variable name to store output for later steps |
condition | string | For "conditional" mode |
max_iterations | integer | For "loop" mode (default: 5) |
until | string | For "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 whentask.assignee_id == agent.id. Use this for "I should work on tasks assigned to me" agents."any"(default) — legacy behaviour; fires on everytask_postedevent 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: {{event}}",
"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: {{event}}. 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.
| Field | Type | Default | Description |
|---|---|---|---|
agent_id | UUID | — | Owner agent. The trigger is registered under this agent. |
pattern | object | — | Event pattern that activates the trigger (see pattern types below). |
prompt_template | string | — | Message sent to the agent when the event fires. Supports {{event}} placeholder. |
max_fires | integer | 0 | Maximum times this trigger may fire. 0 = unlimited. |
cooldown_secs | integer | engine default | Minimum seconds between fires. Omit to use the engine default. |
session_mode | "persistent" | "new" | agent default | Session continuity for each fire. Omit to inherit from the agent manifest. |
target_agent_id | UUID | null | Route the fired message to a different agent instead of the owner. |
Supported pattern types:
| Pattern | Description |
|---|---|
{"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: {{event}}",
"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/{id}).
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
| Situation | What happens |
|---|---|
| Agent is between tool calls | Loop exits at the next boundary check. The partial response up to that point is recorded. |
| Agent is mid-LLM-call | The LLM call completes normally, then the loop checks the flag and exits before executing the next tool. |
| Agent is mid-tool-execution | The tool runs to completion, then the loop exits. No tool call is abandoned mid-flight. |
| Agent has already finished | The interrupt flag is ignored; no error is returned. |
| No active turn | 204 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.