> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Syntax

> DEFINE AGENT reference: properties, ADD TOOL grants, WITH LEVEL write, delegation, topics, CALL AGENT, and management commands

Preview

> [!WARNING]
>
> `DEFINE AGENT` is a preview feature. It ships in broker **2.22**. The syntax and behaviour may still change.

An agent runs on a `TYPE LLM` connector. See [Agents Overview](/latest/lot-language/agents/overview) for how the two fit together, and [AI Connectors](/latest/lot-language/routes/ai-routes#the-llm-connector) for the connector itself.

## Define an agent

`USES LLM CONNECTOR` is the first line of the body. `DEFAULT_USER` is required. Every other line can follow in any order. Add the block with `-addAgent`. `-addConnector` refuses an agent block.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE CONNECTOR Gpt56 WITH TYPE LLM
    ADD LLM_CONFIG
        WITH PROVIDER "openai"
        WITH MODEL "gpt-5.6"
        WITH API_KEY GET SECRET "OPENAI_API_KEY"
        WITH API_SURFACE "auto"
        WITH MAX_TOKENS 4096
        WITH PRICE_INPUT "1.25"
        WITH PRICE_OUTPUT "10"

DEFINE AGENT LineOperator
    USES LLM CONNECTOR Gpt56
    WITH DESCRIPTION "Operates production lines 1-4 and files line reports"
    WITH DEFAULT_USER "line-ops"
    WITH SYSTEM_PROMPT "You operate lines 1-4. Report every intervention."
    WITH GUARDRAILS "Never restart a line that reports an operator on the floor."
    WITH AGENTS "AlertAnalyst, ShiftSupervisor"
    WITH MAX_CHAIN_DEPTH 4
    WITH MAX_ITERATIONS 10
    WITH MAX_TOKENS 50000
    WITH TASK_TIMEOUT 600
    WITH MAX_IN_FLIGHT 4
    WITH MEMORY "individual"
    WITH INTERACTION_TOPIC "plant/operator"
    WITH HUB_VISIBLE "true"
    ADD TOOL "read_mqtt"
        WITH TOPICS "lines/#"
    ADD TOOL "write_mqtt"
        WITH TOPICS "reports/line/#"
        WITH LEVEL "write"
    ADD TOOL "read_lot"
        WITH TARGET "action, route"
    ADD TOOL "execute_routes"
        WITH TARGET "SensorDB.InsertReading"
        WITH LEVEL "write"
    ADD TOOL "execute_actions"
        WITH TARGET "RestartLineX"
        WITH LEVEL "write"
```

Create `line-ops` before you add the agent. `GUARDRAILS` is prompt text. It steers the model. Grants and permissions are what stop a tool call.

| Clause | Required | Default | Description |
| - | - | - | - |
| `USES LLM CONNECTOR <name>` | **Yes** | — | Unquoted name of a `TYPE LLM` connector. First line of the body. `USES LLM ROUTE` is still accepted as an alias |
| `WITH DEFAULT_USER` | **Yes** | — | Broker user a task runs as when no MQTT client started it. There is no implicit admin |
| `WITH DESCRIPTION` | No | `Agent <name>.` | One line on the discovery card. The HUB and peer agents read it |
| `WITH SYSTEM_PROMPT` | No | — | Persona and operating instructions |
| `WITH GUARDRAILS` | No | — | Extra prompt text. The card only records that it is set |
| `WITH AGENTS` | No | none | Comma-separated peers this agent may delegate to. A self-listing is ignored |
| `WITH AGENT_COLLAB_MODE` | No | `delegate` | Only `delegate` is accepted |
| `WITH MAX_CHAIN_DEPTH` | No | `4` | Delegation hops a chain started by this agent may take |
| `WITH MAX_ITERATIONS` | No | `10` | Tool calls per run. Each agent in a chain has its own count |
| `WITH MAX_TOKENS` | No | `50000` | Token budget for the whole tree, delegations included. The connector's `MAX_TOKENS` caps one completion |
| `WITH TASK_TIMEOUT` | No | `600` | Seconds before the task fails with `deadline_exceeded` |
| `WITH MAX_IN_FLIGHT` | No | `4` | Tasks this agent runs at once. A further MQTT task or delegation is refused as `agent_busy` |
| `WITH QUEUE_SIZE` | No | `64` | `CALL AGENT` calls from Actions that may wait when the agent is at `MAX_IN_FLIGHT`. `0` means no queue |
| `WITH QUEUE_TIMEOUT` | No | `60` | Seconds a queued `CALL AGENT` waits before it is refused as busy. `0` means do not wait |
| `WITH MAX_HISTORY_TOKENS` | No | `12000` | Estimated tokens of conversation a `MEMORY "individual"` session keeps between runs and resends to the model. Oldest turns go first. `0` keeps only the 40-message cap |
| `WITH MEMORY` | No | `individual` | `individual` keeps a conversation per session. `none` makes every task stateless |
| `WITH INTERACTION_TOPIC` | No | — | Enables `{topic}/<session>/ask`, `…/response`, and `…/cancel` |
| `WITH HUB_VISIBLE` | No | `false` | Shows the agent in the HUB chat picker. Management and status topics stay available either way |

`AGENT_MODE`, `TOOL_EXECUTION_MODE`, and `EXTERNAL_ACCESS` accept only their defaults (`agent`, `auto`, and `false`). Other values are refused at `-addAgent`. Numbers may be bare or quoted. Any value may use `GET ENV` or `GET SECRET`.

> [!NOTE]
>
> **Models that reason before answering** spend part of each completion on hidden reasoning. If the connector's `MAX_TOKENS` is `1024`, the agent can get an empty answer. Set the `TYPE LLM` connector's `MAX_TOKENS` to `4096` or more for these models. The agent's own `MAX_TOKENS` is the budget for the whole tree, not one completion.

An agent that loads a large built-in skill resends that context on every model call. A panel-building run can need a `MAX_TOKENS` budget of several hundred thousand.

## Tools

Nothing is granted until `ADD TOOL` says so. Grants of the same tool are merged. A grant never adds a permission the user lacks, and a permission never widens a grant.

| Tool | Scope | Class | What it does |
| - | - | - | - |
| `read_mqtt` | `WITH TOPICS` (**required**) | read | Latest payload of known topics that match the filter, paged. A wildcard filter cuts payloads over 1,024 characters to a preview marked `payload_truncated`. The exact topic returns the whole payload |
| `write_mqtt` | `WITH TOPICS` (**required**) | write | Publishes one message to a concrete topic. Wildcards are refused |
| `read_lot` | `WITH TARGET` (optional) | read | Lists loaded LoT entities. Optional kind: `action`, `model`, `route`, `rule`, `panel`, `agent`, `python`. Or one name |
| `create_lot` | `WITH TARGET` (**required**) | write | Creates or replaces one entity from a full LoT block. Kinds: `action`, `model`, `route`, `rule`, `panel` |
| `execute_routes` | `WITH TARGET` (**required**) | write | Runs a connector event. A connector name grants every event on it. Use `<connector>.<event>` to name one |
| `execute_actions` | `WITH TARGET` (**required**) | write | Runs one LoT action |

`WITH TOPICS` is refused on tools that are not MQTT. `WITH TARGET` is refused on the MQTT tools. `+` and `#` must fill a whole topic level.

Every agent can also read a built-in LoT skill. That read changes no broker state and needs no `ADD TOOL`. The skills meant for external coding agents are also published as [Coreflux agent skills](/ai/agent-skills). Declaring skills with `WITH SKILLS` is not available yet.

> [!WARNING]
>
> A writing tool (`write_mqtt`, `create_lot`, `execute_routes`, `execute_actions`) must say `WITH LEVEL "write"`. Omitting `LEVEL` means `write_confirm`, and `-addAgent` refuses that until approvals exist. Read tools do not take `LEVEL`.

`create_lot` with `TARGET "skill"` or `"agent"` is refused. `write_mqtt` cannot publish anywhere under `$SYS/Coreflux/Agents/`. `execute_routes` refuses events on agents and events whose names start with `_`.

### Who a task runs as

Two checks run on every tool call. **RBAC** (`unauthorized`) asks whether this user may do it. **Scope** (`out_of_scope`) asks whether `ADD TOOL` granted it. Both the user the task runs as and the agent's `DEFAULT_USER` must be allowed. An agent never does more than its `DEFAULT_USER` may, and never more than the person who asked.

| Task started by | Authorizes as |
| - | - |
| An MQTT client on `…/task/<id>/request`, `{INTERACTION_TOPIC}/<session>/ask`, or an event source topic | That client's user (`anonymous` for an anonymous session) |
| A message the broker published (an Action `PUBLISH`, a connector writing device data) | The agent's `DEFAULT_USER` |
| `CALL AGENT` from an Action | The agent's `DEFAULT_USER` |
| Another agent, by delegation | Whoever started the chain |

Publishing to an agent's topics does not lend the publisher the agent's `DEFAULT_USER`. A cancel is honoured only from the user the task runs as.

| `outcome` on the stream | Meaning |
| - | - |
| `ok` | The tool ran |
| `unauthorized` | The user the task runs as may not do this |
| `out_of_scope` | The agent was not granted this topic or target |
| `refused` | A limit refused it (budget, deadline, busy peer, hop limit, cycle) |
| `error` | The tool ran and failed, or the model named a tool that does not exist |

For `read_mqtt`, `write_mqtt`, and `execute_routes`, a call that fails both checks reports `out_of_scope`.

### Insight mode

A request body can ask for a read-only run:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{ "text": "Why is line 3 slow?", "session_id": "operator-7", "mode": "insight" }
```

`"mode": "insight"` refuses every writing tool as `out_of_scope`, including inside delegated peers. This is the HUB Insight toggle. A per-agent default of insight mode is not available yet. The agent's `AGENT_MODE` stays `agent`.

***

## Delegation: WITH AGENTS

`WITH AGENTS` is the allow-list. Each registered peer is offered to the model as a delegation tool. An agent with no `WITH AGENTS` reaches no one. A peer that is not registered yet is offered once it registers.

```lot wrap focus={10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE CONNECTOR Gpt56 WITH TYPE LLM
    ADD LLM_CONFIG
        WITH PROVIDER "openai"
        WITH MODEL "gpt-5.6"
        WITH API_KEY GET SECRET "OPENAI_API_KEY"

DEFINE AGENT ShiftSupervisor
    USES LLM CONNECTOR Gpt56
    WITH DEFAULT_USER "shift-ops"
    WITH AGENTS "AlertAnalyst, MaintenancePlanner"
    WITH MAX_TOKENS 80000
    WITH INTERACTION_TOPIC "plant/supervisor"
    ADD TOOL "read_lot"
```

Declare `AlertAnalyst` and `MaintenancePlanner` the same way, each with the grants its job needs. The whole tree shares the root task's token budget and deadline. `MAX_CHAIN_DEPTH` and cycle detection bound it. Each hop publishes a `hop` event on the caller's stream. The child runs with its own task id on the peer's stream, and every tool call in the tree authorizes as the user who started the chain.

In-process delegation needs no extra `PublishSys` rule. An explicit `DENY` on the peer's `…/task/+/request` topic for that user still refuses the hop as `unauthorized`.

Share one `TYPE LLM` connector across agents that use the same model. Changing the key or the model on the connector updates every agent that names it. Set prices once and every agent's `run_end` can report `cost`.

Match the model to the job: a local model for high-volume classification, a cloud model where the answer has to be sharper. `WITH MEMORY "none"` on a classifier so an earlier conversation cannot steer it.

***

## Agent topics

The broker publishes these topics. Reading them needs `SubscribeSys` on the path.

| Topic | Retained | Carries |
| - | - | - |
| `$SYS/Coreflux/Agents/<agent>` | yes | The `DEFINE AGENT` source |
| `$SYS/Coreflux/Agents/<agent>/card` | yes | Discovery card, schema version 1 |
| `$SYS/Coreflux/Agents/<agent>/status` | yes | `online`, `offline`, or `error`, plus `reason` |
| `$SYS/Coreflux/Agents/<agent>/task/<id>/request` | no | Inbound work. You choose `<id>` |
| `…/task/<id>/response` | no | Final answer |
| `…/task/<id>/stream` | no | Event stream, schema version 1 |
| `…/task/<id>/status` | no | `running`, `done`, `canceled`, `failed`, or `rejected` |
| `…/task/<id>/cancel` | no | Cancel this task |
| `{INTERACTION_TOPIC}/<session>/ask` | no | Chat request. The session id is the task id |
| `{INTERACTION_TOPIC}/<session>/response` | no | Chat answer |
| `{INTERACTION_TOPIC}/<session>/cancel` | no | Cancel that session |

The card includes `schemaVersion`, `name`, `description`, `status`, `llmRoute`, `provider`, `model`, `hubVisible`, `interactionTopic`, `peers`, `tools`, and `memory`. Ignore unknown fields. `read_lot` with no `TARGET` appears as an empty object. The card never carries the API key or the guardrail text.

`-removeAgent` clears the card and the status with empty retained payloads. At startup the broker also clears retained topics under `$SYS/Coreflux/Agents/` that no running or defined agent owns (skipped in a cluster), including leftovers from a removed `TYPE AGENT` connector.

### How work arrives

**Task topic.** Publish to `$SYS/Coreflux/Agents/<agent>/task/<id>/request`. Subscribe to that task's `status`, `stream`, and `response` first. Publishing under `$SYS` is default-deny, including for `root`. Without a `PublishSys` rule the publish is dropped before the agent sees it.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE AllowAskPlanner WITH PRIORITY 100 FOR PublishSys TO TOPIC "$SYS/Coreflux/Agents/Planner/task/+/request"
    IF USER IS "planner_client" THEN
        ALLOW
    ELSE
        DENY
```

Cancel needs the same rule on `…/task/+/cancel`.

**Interaction topic.** With `INTERACTION_TOPIC "plant/operator"`, publish plain text or JSON to `plant/operator/<session>/ask`. These are ordinary topics, so ordinary publish rules apply. Restrict who may subscribe to `{INTERACTION_TOPIC}/#` when answers are sensitive.

A request body is plain text, or JSON with `text`, optional `session_id`, optional `mode`, and optional `image` or `pdf` data URIs (`image/*` or `application/pdf`, at most 10 MiB decoded). A bad attachment is dropped with a broker warning and the question is still answered.

**`CALL AGENT`.** See [below](#call-an-agent-from-an-action).

Stream events are QoS 0 and not retained. Each one has `v: 1` and a `type`: `run_start`, `delta`, `tool_call`, `tool_result`, `hop`, or `run_end`. Ignore unknown types. `run_start.requestPreview` is the request text, redacted and cut at 200 characters. It is the only copy of a `CALL AGENT` task argument or a delegated child's question, because those do not publish a `…/request`. A refusal before the run starts publishes a `rejected` status and no stream events.

`…/task/<id>/status` uses `done` when the model answered, `canceled` when someone published to `…/cancel`, `failed` with a `code` such as `deadline_exceeded` or `budget_exhausted`, and `rejected` with a `code` such as `unauthorized`, `unknown_agent`, `agent_busy`, `out_of_scope`, `cycle_detected`, or `hop_limit`.

An agent on an Ollama connector can also fail with `llm_timeout`, `llm_unreachable`, `model_not_found`, or `context_overflow`. The message names the model and the URL. See [Ollama context and timeouts](/latest/lot-language/routes/ai-routes#ollama-context-and-timeouts).

### Working memory

`MEMORY "individual"` keeps one conversation per session. The session is the `/ask` topic segment, or `session_id` in a task-request body. A request with neither is stateless. A conversation belongs to the user it runs as: another user who reuses the session id starts a separate conversation.

Each agent keeps up to 256 sessions (least recently used are dropped), each at most 256 KiB and 40 messages. Memory is saved on shutdown and loaded at the next agent start. `-removeAgent` forgets that agent's sessions. A task still running when the agent stops is cancelled and its turn is not kept. `CALL AGENT "<agent>.clear_history"` forgets one of the `DEFAULT_USER`'s sessions.

Delegated children do not inherit the caller's conversation. A second question on a session whose task is still running is refused as `agent_busy`.

***

## Call an agent from an Action

`CALL AGENT` waits until the task finishes. It runs as the agent's `DEFAULT_USER`, through the same limits, status, and stream as any other task. `execute` returns the answer text, or a JSON object `{ "success": false, "status", "code", "message" }` when the task is refused or fails. `use_history` on the call is ignored. The session is the memory.

```lot wrap focus={11-16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE CONNECTOR LocalLlama WITH TYPE LLM
    ADD LLM_CONFIG
        WITH MODEL "llama3.2"

DEFINE AGENT Classifier
    USES LLM CONNECTOR LocalLlama
    WITH DEFAULT_USER "agent_svc"
    WITH SYSTEM_PROMPT "Answer with one word: safety, quality or other."
    WITH MEMORY "none"

DEFINE ACTION ClassifyNote
ON TOPIC "operators/notes" DO
    CALL AGENT "Classifier.execute"
        WITH (task = PAYLOAD)
        RETURN AS {label}
    PUBLISH TOPIC "operators/notes/classified" WITH {label}
```

| Method | Arguments | Returns |
| - | - | - |
| `execute` | `task` (required), `session_id` (optional) | The answer text, or a JSON failure |
| `clear_history` | `session_id` | JSON confirming that session was cleared |

A name that is not a `DEFINE AGENT`, or an agent that exists but is `offline` or in `error`, returns `unknown_agent`. Read a failure with `GET JSON`:

```lot wrap focus={10-15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE CONNECTOR LocalLlama WITH TYPE LLM
    ADD LLM_CONFIG
        WITH MODEL "llama3.2"

DEFINE AGENT Planner
    USES LLM CONNECTOR LocalLlama
    WITH DEFAULT_USER "agent_svc"
    ADD TOOL "read_lot"

DEFINE ACTION AskPlannerFailureCode
ON TOPIC "planner/ask" DO
    CALL AGENT "Planner.execute"
        WITH (task = PAYLOAD)
        RETURN AS {result}
    PUBLISH TOPIC "planner/code" WITH (GET JSON "code" IN {result} AS STRING)
```

On success `{result}` is plain text, so `code` is empty. On failure it is the JSON object above.

***

## Disable an agent

Add `DISABLE true` on its own line to keep an agent defined but not started:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE AGENT NightShift
    DISABLE true
    USES LLM ROUTE LocalLlama
    WITH DEFAULT_USER "agent_svc"
    ADD TOOL "read_mqtt"
        WITH TOPICS "plant/#"
```

A disabled agent stays listed and keeps its definition, but takes no tasks. `-disableAgent <name>` and `-enableAgent <name>` switch it without editing the code. In a cluster, agents are local to each node, so these two commands change only the node that receives them.

***

## Managing agents

| Command | What it does |
| - | - |
| `-addAgent <DEFINE AGENT …>` | Adds the agent, or updates an existing one in place and keeps its conversations. Refused when a connector already has that name |
| `-removeAgent <name>` | Cancels running tasks, removes the agent, clears the card and status, forgets its sessions |
| `-listAgents` | Returns `name`, `llmRoute`, `status`, and `description` for each agent |
| `-disableAgent <name>` / `-enableAgent <name>` | Redeploys the agent with `DISABLE true`, or without it. An agent already in that state is left alone |

`-addAgent` needs `AgentManagementCreation`. `-removeAgent` needs `AgentManagementRemove`. Seeded rules allow `root`, `admin`, and `AllowedSystemConfiguration`, the same gate as connectors. See [Rules syntax](/latest/lot-language/rules/syntax#entity-management).

Agents are absent from `-listConnectors`, `-checkConnectorConnection`, and `$SYS/Coreflux/Routes/`. Their source is retained on `$SYS/Coreflux/Agents/<name>`. Ask an agent on its task topic or its interaction topic. Do not publish to `$SYS/Coreflux/Routes/<agent>/Events/…/Trigger`.

`-removeConnector` and `-removeAllConnectors` refuse to remove a `TYPE LLM` connector an agent uses, and they name those agents. Point the agents at another connector, or remove them, first. To change the model, key, or provider, send `-addConnector` with the new block. The connector updates in place and its agents stay bound. They show `error` for the moment the connector restarts, then come back online.

`-loadProject` and `-loadFolder` skip a project's agents, with a warning, when the caller lacks `AgentManagementCreation`. Projects deploy agents after connectors.

***

## Troubleshooting

> **The agent stays in error**
>
> Read `$SYS/Coreflux/Agents/<agent>/status`. The usual `reason` is that the LLM connector is missing, stopped, or not a running `TYPE LLM` connector. Add or fix the connector. The agent comes online on its own.

> **-addAgent is refused**
>
> `USES LLM CONNECTOR` or `DEFAULT_USER` is missing, a writing tool has no `WITH LEVEL "write"`, a scope setting is missing, or the name is already a connector or an agent. Agents and connectors share one namespace.

> **A task publish disappears**
>
> No `PublishSys` rule for that user on `…/task/+/request`, so the broker dropped the publish. Add the rule in [How work arrives](#how-work-arrives), or use the interaction topic.

> **Tool calls are refused**
>
> `unauthorized`: grant the permission to the user the task runs as (the originator, for a delegated task). `out_of_scope`: widen `ADD TOOL`. A tool the model never calls was never granted, or the task is in insight mode.

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.