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

# Agents API

> Invoke the current agent or another agent from Lua runtime code

## Overview

The Agents API lets Lua runtime code — tools, webhooks, jobs, preprocessors, and postprocessors — invoke the current agent or another agent through the full chat pipeline. The invocation goes through billing, message persistence, skills and tools, preprocessors, postprocessors, and governance on the target agent.

```typescript theme={null}
import { Agents } from 'lua-cli';

// Simplified: returns plain text
const reply = await Agents.invoke('sales-agent', 'Summarize the last order.');

// Full options: returns structured output
const result = await Agents.invoke('sales-agent', {
  prompt: 'Draft a reply to the latest order',
  threadId: 'order-123',
  systemPrompt: 'Be concise.',
});
console.log(result.text, result.usage);
```

<Note>
  **Full Pipeline Execution:** Unlike `AI.generate`, `Agents.invoke` routes through the target agent's complete processing stack — including its skills, tools, preprocessors, postprocessors, and governance rules.
</Note>

<Tip>
  **Use this inside agent primitives.** `/chat/generate` and `/chat/stream` are external consumption APIs for apps and services. `Agents.invoke` is the runtime API for starting an agent turn from Lua code, so it does not require an API base URL or user bearer token.
</Tip>

## Run Your Own Agent on a Schedule

Self-invocation is supported: pass the current agent's ID as `targetAgentId`. This is useful when a `LuaJob` needs the agent itself to run a scheduled prompt with its normal skills and tools.

```typescript theme={null}
import { Agents, LuaJob, env } from 'lua-cli';

export default new LuaJob({
  name: 'weekly-pipeline-review',
  description: 'Run the agent weekly against the latest pipeline data',
  schedule: { type: 'cron', expression: '0 7 * * 1', timezone: 'UTC' },
  execute: async (job) => {
    const currentAgentId = env('CURRENT_AGENT_ID');
    if (!currentAgentId) throw new Error('CURRENT_AGENT_ID is required');

    return Agents.invoke(currentAgentId, {
      prompt: 'Review the current pipeline and produce the weekly digest.',
      userId: job.metadata.userId,
      threadId: 'weekly-pipeline-review',
    });
  },
  metadata: { userId: 'user_abc123' },
});
```

<Warning>
  `Agents.invoke` currently requires an explicit `targetAgentId`; there is no `self` sentinel or ambient current-agent ID in the runtime API. A reusable template that self-invokes must still receive its deployed agent ID as configuration. It does **not** need an API URL or bearer token.
</Warning>

## Import

```typescript theme={null}
import { Agents } from 'lua-cli';
// or
import { Agents } from 'lua-cli/skill';
```

## Calling Contexts

| Context                          | User identity                    | Recommended pattern                                                      |
| -------------------------------- | -------------------------------- | ------------------------------------------------------------------------ |
| **Tool** (user turn)             | Caller's user automatically used | `Agents.invoke(id, prompt)` — no `userId` needed                         |
| **Dynamic Job** (Jobs API)       | Caller's user automatically used | `Agents.invoke(id, prompt)` — no `userId` needed                         |
| **Webhook**                      | No ambient user                  | Pass `userId` from event payload, or omit to run without a user identity |
| **Pre-defined LuaJob**           | No ambient user                  | Pass `userId` from metadata, or omit to run without a user identity      |
| **PreProcessor / PostProcessor** | Caller's user automatically used | `Agents.invoke(id, prompt)` — no `userId` needed                         |

<Note>
  **No user identity** — when no `userId` is available (e.g. a webhook with no user context), the invocation runs without a user identity and no conversation history is stored. Use `userId` from your event payload whenever you have one.
</Note>

### Execution and delivery semantics

`Agents.invoke` waits for a complete agent turn and returns that turn's response to the calling code. During the turn, the target agent's tools execute normally and their side effects are real.

The returned response is **not automatically sent to a user or channel**. Delivery only occurs if the invoked agent calls a delivery tool, or if the calling code sends `result.text` through a runtime API such as `User` or `Channels`.

* In a user-authenticated tool, dynamic job, or processor, the ambient user is used automatically.
* In a pre-defined `LuaJob` or userless webhook, pass `userId` when the invoked tools or later delivery need a user. Without it, the turn runs in system scope with no user profile or conversation history.
* `channel` sets the target turn's channel context. It does not, by itself, deliver the returned text to that channel.

## Methods

### Simplified: `Agents.invoke(targetAgentId, prompt)`

Invoke an agent with a plain text prompt. Returns the assistant's response as a plain string.

<ParamField path="targetAgentId" type="string" required>
  The target agent's identifier (e.g. `'sales-agent'`, `'support-agent'`). Pass the current agent's own ID to self-invoke. There is currently no `self` sentinel or ambient self-ID.
</ParamField>

<ParamField path="prompt" type="string" required>
  Plain-text message to send to the target agent.
</ParamField>

**Returns:** `Promise<string>`

```typescript theme={null}
const summary = await Agents.invoke('sales-agent', 'Summarize the last order.');
console.log(summary); // "The last order was for 3 units of..."
```

### Full options: `Agents.invoke(targetAgentId, input)`

Invoke an agent with full control over the request. Returns a structured output object.

<ParamField path="targetAgentId" type="string" required>
  The target agent's identifier. Pass the current agent's own ID to self-invoke.
</ParamField>

<ParamField path="input" type="AgentInvocationInput" required>
  Full invocation options — see below.
</ParamField>

**Returns:** `Promise<AgentInvocationOutput>`

## `AgentInvocationInput`

<ParamField path="prompt" type="string">
  Plain-text user message. Mutually exclusive with `messages`.
</ParamField>

<ParamField path="messages" type="UserContent">
  AI SDK `UserContent` (array of `TextPart`, `ImagePart`, `FilePart`). Mutually exclusive with `prompt`.
</ParamField>

<ParamField path="systemPrompt" type="string">
  Override the target agent's system prompt for this invocation only.
</ParamField>

<ParamField path="runtimeContext" type="string">
  Additional runtime context string attached to the request (e.g. serialised metadata).
</ParamField>

<ParamField path="clientContext" type="object">
  Client-side context for the invocation. Supports `timezone` — an IANA timezone string (e.g. `'Africa/Nairobi'`) used as the user's local timezone for date/time-aware responses. When omitted, the target agent falls back to the user's stored profile, country, or UTC.
</ParamField>

<ParamField path="threadId" type="string">
  Thread ID suffix for conversation scoping. When omitted, the invocation flows into the caller-user's default chat thread with the target agent — the same behaviour as a direct message. Pass a custom value to isolate this invocation in its own thread.
</ParamField>

<ParamField path="channel" type="string">
  Channel context for the invoked turn. Defaults to `'agent-invocation'`. This does not automatically deliver the returned response to a channel.
</ParamField>

<ParamField path="identifier" type="string">
  Free-form request tag persisted on the stored message record (e.g. a UUID or external trace ID for correlation). Not a user identifier.
</ParamField>

<ParamField path="userId" type="string">
  Explicit user to invoke as. Use from context-less triggers — webhooks and pre-defined jobs — where there is no ambient user. When omitted from a webhook or pre-defined job, the invocation runs without user identity and no conversation history is stored. This field is **ignored** during a user-authenticated turn; the turn's user is always used.
</ParamField>

## `AgentInvocationOutput`

| Field          | Type        | Description                                                                                                              |
| -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| `text`         | `string`    | Final response text (after the target agent's postprocessors).                                                           |
| `finishReason` | `string?`   | AI SDK `FinishReason` — `'stop'`, `'length'`, `'content-filter'`, `'preprocessor_blocked'`, `'governance_blocked'`, etc. |
| `usage`        | `object?`   | Token usage: `{ inputTokens, outputTokens, totalTokens, reasoningTokens, cachedInputTokens }`                            |
| `toolsUsed`    | `string[]?` | Names of tools invoked by the target agent during generation.                                                            |
| `threadId`     | `string?`   | Echoes back the `threadId` the caller passed in, if any.                                                                 |

## Error Handling

`Agents.invoke` throws an `Error` for any non-success response. Wrap calls in `try/catch` and inspect the message:

```typescript theme={null}
import { Agents } from 'lua-cli';

async function delegateToAgent(agentId: string, prompt: string) {
  try {
    const result = await Agents.invoke(agentId, { prompt });

    if (result.finishReason === 'preprocessor_blocked') {
      return { blocked: true, message: result.text };
    }
    if (result.finishReason === 'governance_blocked') {
      return { blocked: true, message: result.text };
    }

    return { success: true, text: result.text };
  } catch (error) {
    // Network, timeout, target disabled, insufficient credits, etc.
    return { success: false, error: error.message };
  }
}
```

**Common error causes:**

| Cause                 | Description                                                            |
| --------------------- | ---------------------------------------------------------------------- |
| Target agent disabled | The target agent is not available to the invoking user.                |
| Insufficient credits  | The account does not have enough credits to run the invocation.        |
| Timeout               | The target agent took longer than 120 s to respond.                    |
| Service unreachable   | The platform could not be reached (may occur during local `lua test`). |

## Complete Examples

### Tool routing to a specialist agent

```typescript theme={null}
import { LuaTool, Agents } from 'lua-cli/skill';
import { z } from 'zod';

export default class EscalateToLegalTool implements LuaTool {
  name = 'escalate_to_legal';
  description = 'Send a legal query to the dedicated legal-review agent';

  inputSchema = z.object({
    query: z.string().describe('The legal question to review'),
    contractId: z.string().optional(),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const result = await Agents.invoke('legal-review-agent', {
      prompt: input.query,
      threadId: input.contractId ? `contract-${input.contractId}` : undefined,
      systemPrompt: 'Respond in plain language suitable for a non-lawyer.',
    });

    return {
      legalAdvice: result.text,
      tokensUsed: result.usage?.totalTokens,
    };
  }
}
```

### Webhook delegating to an agent

When a webhook fires you typically have a user ID in the event payload — pass it via `userId` so the invocation runs in that user's context:

```typescript theme={null}
import { LuaWebhook, Agents } from 'lua-cli';

const orderWebhook = new LuaWebhook({
  name: 'order-shipped-webhook',
  description: 'Notify users when their order ships',

  execute: async (event) => {
    const { orderId, customerId, trackingNumber } = event.body ?? {};

    if (!customerId) {
      return { skipped: true, reason: 'no customerId in payload' };
    }

    // Pass userId so the invocation runs as that user — conversation history
    // is stored and any per-user agent rules apply.
    await Agents.invoke('notification-agent', {
      prompt: `Order ${orderId} has shipped. Tracking: ${trackingNumber}. Notify the customer.`,
      userId: customerId,
    });

    return { notified: true, customerId };
  },
});

export default orderWebhook;
```

### Pre-defined job with no user context

When no `userId` is available, omit it and the invocation runs without user identity. No conversation history is stored.

```typescript theme={null}
import { LuaJob, Agents } from 'lua-cli';

const dailyDigest = new LuaJob({
  name: 'daily-digest-generator',
  schedule: { type: 'cron', expression: '0 6 * * *' },

  execute: async (job) => {
    // No userId available — invocation runs without a user context
    const result = await Agents.invoke('digest-agent', {
      prompt: 'Generate the daily product digest for today.',
    });

    // Store the result for other processes to pick up
    return { digest: result.text, generatedAt: new Date().toISOString() };
  },
});

export default dailyDigest;
```

If the invoked agent uses `User.get()`, `User.send()`, or another user-scoped tool, include a real `userId` in the invocation. A system-scoped invocation cannot infer the template installer or a delivery recipient.

### Multi-modal input (image analysis)

```typescript theme={null}
import { LuaTool, Agents } from 'lua-cli/skill';
import { z } from 'zod';

export default class AnalyseReceiptTool implements LuaTool {
  name = 'analyse_receipt';
  description = 'Send a receipt image to the expense-processing agent';

  inputSchema = z.object({
    imageUrl: z.string().url(),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const result = await Agents.invoke('expense-agent', {
      messages: [
        { type: 'text', text: 'Extract line items and total from this receipt.' },
        { type: 'image', url: input.imageUrl },
      ],
    });

    return { extraction: result.text };
  }
}
```

## Limitations

* **No recursion guard** — avoid infinite agent-invoke loops in your code.
* **Explicit target required** — self-invocation works, but you must pass the current agent's ID. There is no `self` sentinel or ambient self-ID yet.
* **No native fire-and-forget** — `Agents.invoke` is always awaited. To run in the background, use the Jobs API to schedule a dynamic job.
* **`skillOverride` / `preprocessorOverride` not exposed** — the target agent always runs with its configured skills and preprocessors.
* **Persona override not exposed** — use `systemPrompt` to influence behaviour without replacing the full persona.
* **120 s timeout** — invocations that take longer than 120 seconds will throw a timeout error.

## Related APIs

<CardGroup cols={2}>
  <Card title="AI API" href="/api/ai" icon="brain">
    Generate text outside the agent pipeline
  </Card>

  <Card title="User API" href="/api/user" icon="user">
    Get or update user context
  </Card>

  <Card title="LuaWebhook" href="/api/luawebhook" icon="webhook">
    HTTP endpoints for external events
  </Card>

  <Card title="LuaJob" href="/api/luajob" icon="clock">
    Pre-defined scheduled tasks
  </Card>

  <Card title="Jobs API" href="/api/jobs" icon="plus">
    Dynamic job creation
  </Card>

  <Card title="PostProcessor" href="/api/postprocessor" icon="paper-plane">
    Format agent responses
  </Card>
</CardGroup>

## See Also

* [HTTP API](/channels/http-api) — external `/chat/generate` and `/chat/stream` endpoints for apps and services consuming an agent
* [AI API](/api/ai) — isolated text generation without the full agent pipeline
* [LuaWebhook](/api/luawebhook) — triggering agents from external events
* [LuaJob](/api/luajob) — scheduled agent invocations
