# Define functions

Deploy a PromptPack as a callable HTTP endpoint with a validated request body and a validated response, for jobs such as summarising a document or extracting structured data from free text.

## Prerequisites

- A PromptPack and a Provider that are `Ready` in the workspace namespace.
- A way to authenticate callers: `spec.externalAuth` on the AgentRuntime, or the development flag in step 3.

A function-mode AgentRuntime differs from an agent-mode one as follows. If you want a WebSocket chat agent, leave `spec.mode` at its default of `agent`.

| Concern | Agent mode | Function mode |
|---------|------------|---------------|
| Transport | WebSocket (`/ws`) | HTTP POST (`/functions/{name}`) |
| Conversation start | Human-initiated | System-initiated (HTTP POST) |
| Input validation | None at the boundary | Required JSON Schema |
| Output validation | None at the boundary | Required JSON Schema |
| Audit trail | `sessions` (+ `messages`, `tool_calls`, `provider_calls`, `eval_results`, `runtime_events`, `runtime_event_batches`) | The same tables: each invocation is a session tagged `function` |
| Browser UI | Console / dashboard chat | The dashboard Functions page (schemas, invocations, invoke form) |

## Steps

1. Write the AgentRuntime. Set `spec.mode: function`, declare one `rest` facade, and give both schemas.

   ```yaml
   apiVersion: omnia.altairalabs.ai/v1alpha1
   kind: AgentRuntime
   metadata:
     name: summarizer
     namespace: my-workspace
   spec:
     mode: function
     promptPackRef:
       name: summarizer-pack
     facades:
       - type: rest
         port: 8080
     providers:
       - name: default
         providerRef:
           name: claude-sonnet
     inputSchema:
       type: object
       required: ["text"]
       properties:
         text:
           type: string
           description: "Text to summarise"
         maxWords:
           type: integer
           minimum: 10
           maximum: 500
     outputSchema:
       type: object
       required: ["summary"]
       properties:
         summary:
           type: string
   ```

   The runtime constrains the model to `spec.outputSchema` by default. To change that, set `spec.outputFormat`. See the AgentRuntime reference for the [`outputFormat` values](/reference/core/agentruntime/#outputformat) and the [CEL rules](/reference/core/agentruntime/#mode) that tie `mode`, the schemas and the facades together.

2. Apply it and wait for `Ready`.

   ```bash
   kubectl apply -f summarizer.yaml
   kubectl wait --for=condition=Ready agentruntime/summarizer -n my-workspace --timeout=60s
   ```

3. Admit callers. The external route runs the validators built from `spec.externalAuth` (client keys, OIDC, edge trust). With none configured it answers every request with `401`. See [Configure authentication](/how-to/security/configure-authentication/) to set one up.

   For a development or CI cluster only, admit unauthenticated requests by setting the flag on the facade container:

   ```yaml
   spec:
     facades:
       - type: rest
         port: 8080
         extraEnv:
           - name: OMNIA_FACADE_ALLOW_UNAUTHENTICATED
             value: "true"
   ```

   Never set this flag in production. Management-plane JWTs (the dashboard) and ScheduledFunction dispatch are accepted only on the internal management-plane listener, not on this route.

4. Invoke the function. `{name}` is the AgentRuntime's `metadata.name`, and a pod serves only that one function.

   ```bash
   kubectl port-forward -n my-workspace svc/summarizer 8080:8080 &

   curl -X POST http://localhost:8080/functions/summarizer \
     -H "Content-Type: application/json" \
     -d '{"text": "Omnia is a Kubernetes operator for AI agent deployments.", "maxWords": 20}'
   ```

5. To watch a long invocation as it runs, ask for an event stream. Send the same request with `Accept: text/event-stream`. If the header also lists `application/json`, the stream is chosen only when its `q` value is at least as high:

   ```bash
   curl -N -X POST http://localhost:8080/functions/summarizer \
     -H "Content-Type: application/json" \
     -H "Accept: text/event-stream" \
     -d '{"text": "Omnia is a Kubernetes operator for AI agent deployments.", "maxWords": 20}'
   ```

   The response is Server-Sent Events. Each event's data is a JSON object that carries the invocation's `session_id`:

   | Event | When | Data, besides `session_id` |
   |-------|------|------|
   | `started` | first, before the run starts | none |
   | `tool_call` | the model called a tool | `id`, `name` |
   | `tool_result` | that tool finished | `id`, `name`, `ok`, `duration_ms` |
   | `output` | last, on success | the same fields as the JSON response |
   | `error` | last, on failure | `reason` (the JSON response's error code: `input_invalid`, `output_invalid`, `runtime_error`, `rate_limited`, `budget_exceeded` or `internal_error`; or `timeout`), `detail`, and `raw_output` or `retry_after_seconds` where the JSON response has them |

   The stream always ends with exactly one `output` or `error` event. The output is validated against `spec.outputSchema` exactly as it is for the JSON response. The model's text is not streamed, only tool progress. A `:ping` comment line every 15 seconds keeps idle proxies from closing a quiet connection.

   The 30-second synchronous limit does not apply to a stream. Two bounds apply instead, set in the primary facade's `extraEnv`:

   | Variable | Default | Effect |
   |----------|---------|--------|
   | `OMNIA_FUNCTION_STREAM_MAX_DURATION` | `270s` | The longest a stream may run, at most `290s`. At the limit the run is cancelled and the stream ends with an `error` event whose `reason` is `timeout`. |
   | `OMNIA_FUNCTION_STREAM_MAX_CONCURRENT` | `32` | How many streams the facade serves at once. Over it, the request gets `429 rate_limited` with `Retry-After: 5`. |

   A value that does not parse, or is out of range, stops the facade from starting.

   If the client disconnects, the run is cancelled. The session is still closed with its final status, so read the outcome back by `session_id` from the session API. A browser's `EventSource` sends only `GET`, so read the stream with `fetch` and `response.body.getReader()` instead.

6. To run work longer than 30 seconds without holding a connection open, invoke asynchronously. Set `spec.facades[].async.enabled: true` (it needs a Redis queue backend), then send `POST /functions/{name}?mode=async`. The facade answers `202` with an `invocation_id`. Read the result back with `GET /functions/{name}/invocations/{invocation_id}`.

## Verify

A successful call returns `200` with the model output and metadata. The values below are illustrative:

```json
{
  "output": { "summary": "Omnia is a K8s operator for managing AI agents." },
  "invocation_id": "9c0e2c1f-...",
  "duration_ms": 842,
  "usage": {
    "input_tokens": 41,
    "output_tokens": 12,
    "cost_usd": 0.0008
  }
}
```

An event-stream call returns `200` with `Content-Type: text/event-stream`. Its last event is `output` on success:

```text
event: started
data: {"session_id":"9c0e2c1f-..."}

event: output
data: {"duration_ms":842,"invocation_id":"9c0e2c1f-...","output":{"summary":"..."},"session_id":"9c0e2c1f-..."}
```

The invocation is recorded as a session tagged `function`. The dashboard Functions page lists the function and its invocations. See [Sessions](/explanation/agents/sessions/#function-invocations) for how function sessions are recorded.

## Troubleshooting

The facade validates the request against `spec.inputSchema` and the model output against `spec.outputSchema`. The error body is JSON with `error` and `detail`.

| Code | Meaning |
|------|---------|
| 400  | `input_invalid`: the request body failed `spec.inputSchema`. The body includes the validator error. |
| 401  | `unauthorized` (plain-text body): the listener's validator chain rejected the request. |
| 404  | `function_not_found`: no function-mode runtime matched `{name}` on this facade. |
| 405  | `method_not_allowed`: only `POST` is allowed. |
| 413  | `payload_too_large`: the request body exceeded 1 MiB. |
| 415  | `unsupported_media_type`: `Content-Type` must be `application/json`. |
| 429  | `rate_limited`: the runtime call returned `ResourceExhausted`, which is how the agent's `spec.rateLimit` refuses a call. `Retry-After` carries the wait in whole seconds when the limiter knows it. Also `budget_exceeded`, with no `Retry-After`: the workspace is over its budget and its `budgetExceededAction` is `block`. A call with `?mode=async` is refused the same way when it is submitted; one already accepted runs. |
| 501  | `async_not_enabled`: you sent `?mode=async` or read an invocation, and the agent has no async queue. |
| 502  | `runtime_error` (the runtime sidecar failed) or `output_invalid` (the model output failed `spec.outputSchema`). `output_invalid` includes `raw_output` so you can debug the pack. |
| 504  | `sync_timeout`: the synchronous call ran past its 30-second limit and was cancelled. Retry with `?mode=async`. |

An event-stream call answers `200` once the stream starts, whatever the outcome. The failure is the terminal `error` event, whose `reason` is the code in this table. Only a request refused before the stream starts (`401`, `404`, `413`, `415`, and `429` when the facade is already serving its limit of streams) gets a status code. A `?mode=async` request is queued even when it sends `Accept: text/event-stream`.

To tighten the contract, make `spec.outputSchema` strict: a loose schema lets model drift reach callers, and a tight one surfaces it as `502 output_invalid`.

Editing `spec.inputSchema` or `spec.outputSchema` rolls the Deployment. You do not need to restart the pod.

`examples/echo-function/` in the repository holds a PromptPack, a Provider and an AgentRuntime to apply as a starting point.

## Related pages

- [AgentRuntime reference](/reference/core/agentruntime/)
- [Configure authentication](/how-to/security/configure-authentication/)
- [Sessions](/explanation/agents/sessions/)
- [Schedule functions](/how-to/agents/schedule-functions/)
- [Expose functions as MCP](/how-to/agents/expose-functions-as-mcp/)