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

# External AI agent

> Connect an agent hosted outside Jelou and learn exactly what it receives and must reply

The **External AI agent** node delegates the conversation to an agent you host outside Jelou. With the **HTTP (Jelou contract)** provider (`http_sync`), Jelou sends a `POST` to your agent's URL on every turn and expects a JSON reply in an exact format. This guide describes that format, with examples you can copy.

<Note>
  The same format applies to an HTTP agent deployed on Amazon Bedrock AgentCore: what your handler returns is what Jelou validates.
</Note>

## What your agent receives

Jelou sends a `POST` with `content-type: application/json`, the credential headers you configured and, if enabled, the request signature. The body looks like this:

```json theme={null}
{
  "turnId": "trn_01J8A7K2M4XQ",
  "inputId": "inp_9f3c2a71",
  "conversation": { "key": "wa:593999999999", "channel": "whatsapp", "locale": "es-EC" },
  "user": { "ref": "usr_8d41c0aa" },
  "input": { "parts": [{ "type": "text", "text": "Hola, quiero saber mi saldo" }] },
  "context": { "variables": { "segmento": "premium" } },
  "deadlineMs": 25000,
  "trace": { "executionId": "exec_5b2e", "workflowId": "64083", "nodeId": "n_agent" },
  "interaction": "single_task"
}
```

| Field | Description |
| - | - |
| `turnId` | Turn identifier, `trn_` followed by letters or digits. |
| `inputId` | Identifier of the user's message (1 to 128 characters). |
| `conversation` | `key`, `channel` (`whatsapp`, `facebook`, `instagram`, `web`, `email`, `sms`, `other`) and an optional `locale`. |
| `user` | Pseudonymous `ref` (`usr_...`). `phone`, `name`, `email` and `legalId` arrive only if you allow them in `inputMapping.sendUserFields`. |
| `input.parts` | 1 to 10 parts. A text message is `{"type":"text","text":"..."}`. |
| `input.inbound` | Only for non-text messages (image, audio, location, Flow reply). |
| `context.variables` | Only the workflow variables you list in `inputMapping.variables`. |
| `deadlineMs` | Milliseconds your agent has to answer (1000 to 120000). |
| `trace` | `executionId` and, optionally, `workflowId` and `nodeId`. |
| `interaction` | `single_task` or `multi_turn`. |

## What your agent must reply

Reply with HTTP 200 and JSON. The reply is **strict**: any field not in this table is rejected.

| Field | Required | Description |
| - | - | - |
| `status` | Yes | `completed`, `action_required`, `failed`, `timed_out` or `blocked`. |
| `outputs` | Yes | Array of up to 20 parts. Use `[]` when there is nothing to say. |
| `signals` | No | Up to 5 signals. Jelou uses `[]` when omitted. |
| `turnId` | No | When omitted Jelou uses the request's. When sent it must equal the one received. |
| `provider` | No | Jelou fills in `provider.kind` for you. |
| `timing` | No | Jelou measures `timing.latencyMs` for you. |
| `usage` | No | `inputTokens`, `outputTokens` and `reportedBy: "provider"`. |
| `error` | Only with `failed` or `timed_out` | `code`, `retryable` and an optional `message` (up to 500 characters). Not allowed with any other `status`. |

Rules by status: `failed` and `timed_out` require `error` and `outputs: []`; `blocked` requires `outputs: []`; `action_required` requires an `action_required` signal.

<Warning>
  The names must be exactly `status` and `outputs`. Jelou does not accept aliases such as `output`, `messages`, `result` or `turnStatus`.
</Warning>

### `outputs` parts

| `type` | Fields |
| - | - |
| `text` | `text` required, 1 to 4096 characters. Split longer answers into several text parts. |
| `choices` | `choices` required, 1 to 10 items with `id` and `label` (label up to 24 characters). Optional `text` up to 1024. |
| `card` | `title` required (up to 80). Optional: `body` (1024), `imageUrl` (https) and `actions` (up to 3, with `label` and `url` or `choiceId`). |
| `media` | `url` (https) and `mediaType` (for example `image/png`); optional `caption`. |
| `data` | `data`, an object. |

### Signals (`signals`)

| `type` | Effect |
| - | - |
| `handoff_requested` | Hands the conversation to a person. Optional `reason` (up to 500) and `queue`. The node leaves through `agent_handoff`. |
| `conversation_ended` | Marks the conversation as finished. |
| `variables_set` | `variables`, an object with 1 to 30 keys stored in the node variable. |
| `action_required` | `actionId`, `name` and optional `arguments`. |
| `action_selected` | `name` and optional `data`. |

## Example replies

### Minimal reply

```json theme={null}
{ "status": "completed", "outputs": [{ "type": "text", "text": "Hola, con gusto te ayudo con tu saldo." }] }
```

It is equivalent to sending the full form:

```json theme={null}
{
  "turnId": "trn_01J8A7K2M4XQ",
  "status": "completed",
  "outputs": [{ "type": "text", "text": "Hola, con gusto te ayudo con tu saldo." }],
  "signals": [],
  "provider": { "kind": "http_sync" },
  "timing": { "latencyMs": 0 }
}
```

### With choices

```json theme={null}
{
  "status": "completed",
  "outputs": [
    { "type": "choices", "text": "Que necesitas?", "choices": [{ "id": "saldo", "label": "Ver saldo" }, { "id": "asesor", "label": "Hablar con asesor" }] }
  ]
}
```

### Hand off to an agent

```json theme={null}
{
  "status": "completed",
  "outputs": [{ "type": "text", "text": "Te comunico con un asesor." }],
  "signals": [{ "type": "handoff_requested", "reason": "El cliente pidio hablar con una persona" }]
}
```

### Save variables and end

```json theme={null}
{
  "status": "completed",
  "outputs": [{ "type": "text", "text": "Listo, registre tu solicitud. Gracias por escribirnos." }],
  "signals": [
    { "type": "variables_set", "variables": { "ticket": "A-1042", "categoria": "reclamo" } },
    { "type": "conversation_ended" }
  ]
}
```

### Error on the agent side

The node leaves through `agent_failed`.

```json theme={null}
{
  "status": "failed",
  "outputs": [],
  "error": { "code": "provider_error", "retryable": true, "message": "El sistema de saldos no respondio" }
}
```

## HTTP status codes

If your agent answers with a non-2xx status the turn fails: `401` and `403` as `auth`, `404` and `410` as `not_active`, `429` as `rate_limited`, and `408`, `504` or `5xx` as `provider_error`. `429` and `5xx` are retried if you configured retries.

## What happens when the reply breaks the format

The turn fails with code `malformed` and the message says what to fix, using field names and paths only (never values). For example:

```text theme={null}
response does not satisfy AgentTurnResult: missing status; missing outputs; unknown field output (did you mean outputs?); unknown field turnStatus (did you mean status?)
```

You can read the message in `externalAgent.error.message` of the node result in the execution or in the test.

## Example: HTTP handler on Amazon Bedrock AgentCore (Python)

It reads the user's message from `input.parts` and returns a valid reply. The function's return value is the JSON body of the response.

```python theme={null}
from bedrock_agentcore.runtime import BedrockAgentCoreApp

app = BedrockAgentCoreApp()


def answer_for(text):
    if "asesor" in text.lower():
        return {
            "status": "completed",
            "outputs": [{"type": "text", "text": "Te comunico con un asesor."}],
            "signals": [{"type": "handoff_requested", "reason": "El cliente pidio un asesor"}],
        }
    return {
        "status": "completed",
        "outputs": [{"type": "text", "text": "Recibi tu mensaje: " + text[:500]}],
    }


@app.entrypoint
def invoke(payload, context=None):
    parts = payload.get("input", {}).get("parts", [])
    text = " ".join(p.get("text", "") for p in parts if p.get("type") == "text").strip()
    try:
        result = answer_for(text)
    except Exception:
        return {
            "status": "failed",
            "outputs": [],
            "error": {"code": "provider_error", "retryable": True, "message": "El agente no pudo responder"},
        }
    result["turnId"] = payload["turnId"]
    return result


if __name__ == "__main__":
    app.run()
```


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