# Engine SDK
Source: https://docs.idun-group.com/advanced/sdk
Drive Idun Engine directly from Python: boot the engine in-process, embed its FastAPI app into an existing service, or build configs programmatically with ConfigBuilder.
The standalone runtime is the recommended way to ship an Idun agent (chat UI, admin panel, traces viewer, hot reload, SSO, all in one process). When you already have a FastAPI service and just want the engine's agent routes inside it, or when you want to boot the engine from Python without `idun init`, drop down to the SDK below.
## Public API
`idun_agent_engine` exports its surface from the package root:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine import (
create_app, # build a FastAPI app from config
run_server, # uvicorn wrapper for that app
run_server_from_config, # load YAML, create_app, run_server
run_server_from_builder, # same, from a ConfigBuilder
ConfigBuilder, # fluent / programmatic config
BaseAgent, # protocol for custom adapters
get_prompt, # read managed prompts
)
```
Source: [`libs/idun_agent_engine/src/idun_agent_engine/__init__.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/__init__.py).
## Boot the engine from Python
The lowest-friction path is `run_server_from_config`. It loads `config.yaml`, builds the FastAPI app, and starts uvicorn in one call. The port comes from `server.api.port` in the config unless you override.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine import run_server_from_config
run_server_from_config("config.yaml", reload=False, log_level="info")
```
`run_server_from_config` accepts every kwarg `run_server` does (`host`, `port`, `reload`, `log_level`, `workers`). It blocks until uvicorn exits.
For programmatic configuration, swap the YAML for a `ConfigBuilder`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine import ConfigBuilder, run_server_from_builder
builder = (
ConfigBuilder()
.with_langgraph_agent(name="my-agent", graph_definition="agent.py:graph")
.with_api_port(8080)
)
run_server_from_builder(builder, reload=False)
```
`ConfigBuilder` validates each section as it is added; `.build()` returns a fully validated `EngineConfig` you can also pass directly to `create_app(engine_config=...)`.
## Embed the engine into an existing FastAPI app
`create_app(...)` returns a regular FastAPI instance with the agent routers (`/agent/run`, `/agent/stream`, `/agent/sessions`, `/agent/capabilities`, the deprecated `/agent/invoke`) and the base routes (`/health`, `/reload`, `/_engine/info`, `/openapi.json`) already attached. Mount that into your existing app:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from fastapi import FastAPI
from idun_agent_engine import create_app
my_service = FastAPI(title="My existing service")
# Existing routes from your app
@my_service.get("/billing/usage")
async def billing_usage():
...
# Build the engine app from a config file (or pass engine_config=).
idun_app = create_app(config_path="config.yaml")
# Mount the engine under a prefix so its routes don't collide with yours.
my_service.mount("/idun", idun_app)
```
After mounting, `POST /idun/agent/run` hits the engine's AG-UI streaming endpoint and `GET /idun/health` returns the engine's health payload. Your own routes at `/billing/*` keep working.
Three things to know when embedding:
1. **CORS.** `create_app` adds wide-open CORS middleware to the engine's app (`allow_origins=["*"]`). Mount it inside a parent that owns CORS for the rest of your surface, otherwise the engine's permissive middleware applies to every embedded route.
2. **`POST /reload`.** Pass `reload_auth=...` to `create_app` to gate it behind a FastAPI dependency. Without that, the route is unauthenticated. The standalone runtime injects its own `reload_disabled` callable here. Reference: [Architecture / Reload auth](/architecture#reload-auth).
3. **Lifespan.** The engine ships its own `lifespan` context manager. When you mount it into a parent FastAPI app, FastAPI runs the mounted app's lifespan automatically; you do not need to wire it manually.
## Unconfigured boot
`create_app()` with no config (no `config_path`, no `config_dict`, no `engine_config`, no `./config.yaml` in cwd) boots in unconfigured mode: every route is registered, but `/agent/*` returns `503 agent_not_ready` until you supply a config. Embedders use this to start the process before they know which agent to serve, then call the engine's reload pipeline once the config is ready.
The standalone runtime uses this shape during the first-run wizard.
## Run the server explicitly
When you have an `engine_config` in hand and want full control over the uvicorn lifecycle, build the app first and call `run_server` separately:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine import create_app, run_server, ConfigBuilder
engine_config = ConfigBuilder.load_from_file("config.yaml")
app = create_app(engine_config=engine_config)
# ... add your own middleware, routes, or dependencies to `app` here ...
run_server(app, host="0.0.0.0", port=8080, workers=4)
```
`run_server` is a thin wrapper around `uvicorn.run` with sensible defaults. Skip it entirely if you already run uvicorn (or Gunicorn, or Hypercorn) yourself; `create_app` returns a FastAPI you can hand to any ASGI server.
## Source pointers
| Concern | File |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public exports | [`libs/idun_agent_engine/src/idun_agent_engine/__init__.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/__init__.py) |
| `create_app` | [`core/app_factory.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/core/app_factory.py) |
| `run_server`, `run_server_from_config`, `run_server_from_builder` | [`core/server_runner.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/core/server_runner.py) |
| `ConfigBuilder` | [`core/config_builder.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/core/config_builder.py) |
| Lifespan | [`server/lifespan.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/server/lifespan.py) |
## Next steps
The HTTP shape the chat UI uses, including AG-UI request body and SSE events.
Implement `BaseAgent` for a framework Idun does not bundle yet.
How the standalone runtime hot-reloads on top of these same primitives.
# Check Memory Connection
Source: https://docs.idun-group.com/api-reference/agent-configuration/check-memory-connection
/standalone/openapi.json post /admin/api/v1/memory/check-connection
Probe the configured memory backend.
Returns 404 if no memory row exists; otherwise calls the probe
against the stored config. The probe never raises — failures land
in the response body as ``ok=False``.
# Create From Detection
Source: https://docs.idun-group.com/api-reference/agent-configuration/create-from-detection
/standalone/openapi.json post /admin/api/v1/onboarding/create-from-detection
Materialize a detection picked by the wizard.
# Create Guardrail
Source: https://docs.idun-group.com/api-reference/agent-configuration/create-guardrail
/standalone/openapi.json post /admin/api/v1/guardrails
Create a new guardrail row.
Slug is derived from ``name`` and made unique with numeric
suffixes on collision. Empty post normalization slug returns 422.
# Create Prompt
Source: https://docs.idun-group.com/api-reference/agent-configuration/create-prompt
/standalone/openapi.json post /admin/api/v1/prompts
Create a new prompt version.
First write for a given ``promptId`` is version 1. Subsequent POSTs
with the same ``promptId`` allocate the next version. The whole
SELECT MAX + INSERT runs under ``_reload_mutex`` so concurrent
admin POSTs serialize.
# Create Starter
Source: https://docs.idun-group.com/api-reference/agent-configuration/create-starter
/standalone/openapi.json post /admin/api/v1/onboarding/create-starter
Scaffold a starter project + register the singleton agent row.
# Delete Guardrail
Source: https://docs.idun-group.com/api-reference/agent-configuration/delete-guardrail
/standalone/openapi.json delete /admin/api/v1/guardrails/{guardrail_id}
Remove a guardrail row.
Engine assembly drops the row from the active set. If no enabled
rows remain, the engine continues without a guardrails layer.
# Delete Memory
Source: https://docs.idun-group.com/api-reference/agent-configuration/delete-memory
/standalone/openapi.json delete /admin/api/v1/memory
Remove the singleton memory row.
After delete, the engine assembly falls back to the default
in-memory backend. The reload pipeline reassembles + validates
that fallback so a misconfigured agent row surfaces as a 422
rather than leaving the engine in a half-deleted state.
# Delete Prompt
Source: https://docs.idun-group.com/api-reference/agent-configuration/delete-prompt
/standalone/openapi.json delete /admin/api/v1/prompts/{prompt_row_id}
Remove a single prompt version.
Deleting the latest version of a logical prompt promotes the
previous version at the next assembly. Deleting all versions
drops that prompt from the engine config entirely.
# Get Agent
Source: https://docs.idun-group.com/api-reference/agent-configuration/get-agent
/standalone/openapi.json get /admin/api/v1/agent
Return the current singleton agent.
# Get Guardrail
Source: https://docs.idun-group.com/api-reference/agent-configuration/get-guardrail
/standalone/openapi.json get /admin/api/v1/guardrails/{guardrail_id}
Return a single guardrail row or 404.
# Get Memory
Source: https://docs.idun-group.com/api-reference/agent-configuration/get-memory
/standalone/openapi.json get /admin/api/v1/memory
Return the singleton memory row or 404 if absent.
# Get Prompt
Source: https://docs.idun-group.com/api-reference/agent-configuration/get-prompt
/standalone/openapi.json get /admin/api/v1/prompts/{prompt_row_id}
Return a single prompt version row or 404.
# List Guardrails
Source: https://docs.idun-group.com/api-reference/agent-configuration/list-guardrails
/standalone/openapi.json get /admin/api/v1/guardrails
Return all guardrail rows ordered by position then sort_order.
# List Prompts
Source: https://docs.idun-group.com/api-reference/agent-configuration/list-prompts
/standalone/openapi.json get /admin/api/v1/prompts
Return all prompt versions ordered by prompt id then version.
# Patch Agent
Source: https://docs.idun-group.com/api-reference/agent-configuration/patch-agent
/standalone/openapi.json patch /admin/api/v1/agent
Update metadata fields on the singleton agent.
Empty body short-circuits with no DB write and no reload. Any
non-empty mutation flows through the 3-round reload pipeline.
# Patch Guardrail
Source: https://docs.idun-group.com/api-reference/agent-configuration/patch-guardrail
/standalone/openapi.json patch /admin/api/v1/guardrails/{guardrail_id}
Apply a shallow update to an existing guardrail row.
Only fields present in the body are touched. Slug is sticky on
rename so existing references stay valid. Inner ``guardrail`` is
replaced wholesale when provided. Reordering happens via
``sort_order``.
# Patch Memory
Source: https://docs.idun-group.com/api-reference/agent-configuration/patch-memory
/standalone/openapi.json patch /admin/api/v1/memory
Upsert the singleton memory row.
First write requires both ``agentFramework`` and ``memory``.
Updates apply only the fields explicitly present in the body.
The reload pipeline reassembles + validates the engine config; a
framework/memory mismatch rolls back with a 422.
# Patch Prompt
Source: https://docs.idun-group.com/api-reference/agent-configuration/patch-prompt
/standalone/openapi.json patch /admin/api/v1/prompts/{prompt_row_id}
Update tags on an existing prompt version.
Content changes are not accepted; clients POST a new version
instead. Empty body is a no op (no DB write, no reload).
# Scan
Source: https://docs.idun-group.com/api-reference/agent-configuration/scan
/standalone/openapi.json post /admin/api/v1/onboarding/scan
Classify the project and return the scanner result.
Always runs the scanner so the response carries truthful
``has_python_files`` / ``has_idun_config`` / ``detected`` values
even when an agent row already exists — useful for direct-curl
callers inspecting state. When the row exists, ``current_agent``
is populated so the UI can short-circuit to chat without a
follow-up ``GET /agent`` call.
# Change Password
Source: https://docs.idun-group.com/api-reference/auth-&-sso/change-password
/standalone/openapi.json post /admin/api/v1/auth/change-password
Replace the admin password hash.
# Delete Sso
Source: https://docs.idun-group.com/api-reference/auth-&-sso/delete-sso
/standalone/openapi.json delete /admin/api/v1/sso
# Get Sso
Source: https://docs.idun-group.com/api-reference/auth-&-sso/get-sso
/standalone/openapi.json get /admin/api/v1/sso
# Login
Source: https://docs.idun-group.com/api-reference/auth-&-sso/login
/standalone/openapi.json post /admin/api/v1/auth/login
Verify the password and set the signed session cookie.
# Logout
Source: https://docs.idun-group.com/api-reference/auth-&-sso/logout
/standalone/openapi.json post /admin/api/v1/auth/logout
Drop the session row and clear the cookie.
# Me
Source: https://docs.idun-group.com/api-reference/auth-&-sso/me
/standalone/openapi.json get /admin/api/v1/auth/me
Return the current auth state.
# Patch Sso
Source: https://docs.idun-group.com/api-reference/auth-&-sso/patch-sso
/standalone/openapi.json patch /admin/api/v1/sso
# Sso Info
Source: https://docs.idun-group.com/api-reference/auth-&-sso/sso-info
/standalone/openapi.json get /sso/info
Return the public OIDC parameters for the SPA, or ``enabled=false``.
# Get Dashboard
Source: https://docs.idun-group.com/api-reference/dashboard/get-dashboard
/standalone/openapi.json get /admin/api/v1/dashboard
Return all five v1 dashboard widgets for the requested time range.
# Create Integration
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/create-integration
/standalone/openapi.json post /admin/api/v1/integrations
Create a new integration row.
Slug is derived from ``name`` and made unique with numeric
suffixes on collision. Empty post normalization slug returns 422.
# Create Mcp Server
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/create-mcp-server
/standalone/openapi.json post /admin/api/v1/mcp-servers
Create a new MCP server row.
Slug is derived from ``name`` and made unique with numeric
suffixes on collision. Empty post normalization slug returns 422.
# Delete Integration
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/delete-integration
/standalone/openapi.json delete /admin/api/v1/integrations/{integration_id}
Remove an integration row.
Engine assembly drops the row from the active set. If the row was
the last one, the engine continues without an integrations layer.
# Delete Mcp Server
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/delete-mcp-server
/standalone/openapi.json delete /admin/api/v1/mcp-servers/{mcp_id}
Remove an MCP server row.
Engine assembly drops the row from the active set. If the row was
the last one, the engine continues without an MCP layer.
# Get Integration
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/get-integration
/standalone/openapi.json get /admin/api/v1/integrations/{integration_id}
Return a single integration row or 404.
# Get Mcp Server
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/get-mcp-server
/standalone/openapi.json get /admin/api/v1/mcp-servers/{mcp_id}
Return a single MCP server row or 404.
# List Integrations
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/list-integrations
/standalone/openapi.json get /admin/api/v1/integrations
Return all configured integration rows ordered by created_at.
# List Mcp Server Tools
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/list-mcp-server-tools
/standalone/openapi.json post /admin/api/v1/mcp-servers/{mcp_id}/tools
Discover tools exposed by a single MCP server.
Doubles as a connection check — if the server cannot be reached or
cannot speak MCP, ``ok=False`` carries the upstream error in the
response body. ``details.tools`` carries the discovered tool names
on success.
# List Mcp Servers
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/list-mcp-servers
/standalone/openapi.json get /admin/api/v1/mcp-servers
Return all configured MCP server rows ordered by created_at.
# Patch Integration
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/patch-integration
/standalone/openapi.json patch /admin/api/v1/integrations/{integration_id}
Apply a shallow update to an existing integration row.
Only fields present in the body are touched. Slug is sticky on
rename so existing references stay valid. Inner ``integration``
is replaced wholesale when provided.
# Patch Mcp Server
Source: https://docs.idun-group.com/api-reference/integrations-&-tools/patch-mcp-server
/standalone/openapi.json patch /admin/api/v1/mcp-servers/{mcp_id}
Apply a shallow update to an existing MCP server row.
Only fields present in the body are touched. Slug is sticky on
rename so existing references stay valid. Inner ``mcp_server`` is
replaced wholesale when provided.
# Introduction
Source: https://docs.idun-group.com/api-reference/introduction
HTTP API of every idun-agent-standalone deployment. 42 endpoints, rendered from a snapshot of the live FastAPI app's OpenAPI spec.
Every `idun-agent-standalone` deployment exposes the same HTTP API. This reference is rendered from a snapshot of the live FastAPI app's OpenAPI spec (refreshed manually on releases — see [Spec source](#spec-source) below), so every shape on every page matches what the server returns at that snapshot.
## Authentication
The auth model is pinned at boot via `IDUN_ADMIN_AUTH_MODE`. Pick the row that matches your deployment.
Default for local dev. Every endpoint is open. No header, no cookie, no token.
Admin endpoints need a signed session cookie. Set it with `POST /admin/api/v1/auth/login`. Runtime stays open.
Admin and runtime accept an OIDC Bearer token. Runtime falls back to `X-Idun-User-Id` for trace scoping.
Full setup, claim mapping, and edge cases on the [auth overview](/auth/overview).
## Surfaces
The 42 endpoints group into seven surfaces. Pick one from the left sidebar.
The chat surface. `POST /agent/run` (AG-UI streaming), session listing, graph introspection, health.
CRUD on the live agent config: model, prompts, tools. Writes hot-reload the engine.
Password login and logout, OIDC provider config, `GET /auth/me`.
Aggregated counters and traffic series sourced from the local trace store.
MCP servers, prompts, guardrails, integrations. Everything a tool needs to be reachable from the agent.
Wire and unwire external exporters: Phoenix, Langfuse, OTel collectors.
Read, filter, and delete locally-captured traces and spans.
## A first call
The shortest "hello world": stream a single message through the AG-UI protocol.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -N -X POST http://localhost:8000/agent/run \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"threadId": "demo-thread",
"runId": "demo-run",
"messages": [
{ "role": "user", "content": "Say hello in one sentence." }
]
}'
```
Each event is a JSON line; pipe through `jq` to follow the run. Drill into [`POST /agent/run`](/api-reference/runtime/run) on the left for the full event vocabulary.
## Spec source
This reference reflects a snapshot of `GET /openapi.json` from a running standalone. To refresh the spec before a release, run `curl -sS http://localhost:8000/openapi.json > docs/standalone/openapi.json` and commit the result; the API pages regenerate automatically on the next deploy.
How the standalone wraps the engine, what ships in the wheel, when to use it.
# Invoke
Source: https://docs.idun-group.com/api-reference/invoke
/standalone/openapi.json post /agent/invoke
Invoke the agent with a message and get a response.
# Check Observability Connection
Source: https://docs.idun-group.com/api-reference/observability/check-observability-connection
/standalone/openapi.json post /admin/api/v1/observability/check-connection
Probe the configured observability provider.
Returns 404 if no provider is configured. The probe never raises —
failures land in the response body as ``ok=False``.
# Delete Observability
Source: https://docs.idun-group.com/api-reference/observability/delete-observability
/standalone/openapi.json delete /admin/api/v1/observability
Remove the singleton observability row.
After delete, the engine assembly drops the observability list. The
reload pipeline reassembles + validates so a misconfigured agent
surfaces as a 422 rather than leaving the engine in a half deleted
state.
# Get Observability
Source: https://docs.idun-group.com/api-reference/observability/get-observability
/standalone/openapi.json get /admin/api/v1/observability
Return the singleton observability row or 404 if absent.
# Patch Observability
Source: https://docs.idun-group.com/api-reference/observability/patch-observability
/standalone/openapi.json patch /admin/api/v1/observability
Upsert the singleton observability row.
First write requires ``observability``. Updates apply only the
fields explicitly present in the body. The reload pipeline
reassembles + validates the engine config; failures roll back
with a 422.
# Capabilities
Source: https://docs.idun-group.com/api-reference/runtime/capabilities
/standalone/openapi.json get /agent/capabilities
Return the agent's capability descriptor for UI auto-configuration.
# Copilotkit Stream
Source: https://docs.idun-group.com/api-reference/runtime/copilotkit-stream
/standalone/openapi.json post /agent/copilotkit/stream
Process a message with the agent, streaming ag-ui events.
# Engine Info
Source: https://docs.idun-group.com/api-reference/runtime/engine-info
/standalone/openapi.json get /_engine/info
Engine info endpoint — basic information about the service.
# Get Config
Source: https://docs.idun-group.com/api-reference/runtime/get-config
/standalone/openapi.json get /agent/config
Get the current agent configuration.
Returns 503 ``agent_not_ready`` when the engine booted unconfigured
and ``configure_app`` hasn't run yet (no agent → no config to expose).
# Get Graph Ascii
Source: https://docs.idun-group.com/api-reference/runtime/get-graph-ascii
/standalone/openapi.json get /agent/graph/ascii
ASCII art rendering.
# Get Graph Ir
Source: https://docs.idun-group.com/api-reference/runtime/get-graph-ir
/standalone/openapi.json get /agent/graph
Framework-agnostic JSON IR — primary contract for UI rendering.
# Get Graph Mermaid
Source: https://docs.idun-group.com/api-reference/runtime/get-graph-mermaid
/standalone/openapi.json get /agent/graph/mermaid
Mermaid source string.
# Get Session
Source: https://docs.idun-group.com/api-reference/runtime/get-session
/standalone/openapi.json get /agent/sessions/{session_id}
Return a single session's reconstructed text-only message thread.
Returns 501 when the adapter doesn't support detail retrieval and
404 when the session id is unknown (or when the SSO-scoped user
isn't allowed to see it — the adapter enforces that and returns
``None``).
# Health Check
Source: https://docs.idun-group.com/api-reference/runtime/health-check
/standalone/openapi.json get /health
Health check endpoint for monitoring and load balancers.
Returns ``status: "ok"`` only when an agent is registered and
``/agent/*`` will accept requests. Returns ``status: "degraded"`` with
``agent_ready: false`` when no agent is configured — e.g. standalone
admin-only mode after an ``assemble_engine_config`` error, or the
pre-onboarding wizard state. When ``app.state.boot_error`` is set by
the standalone's assembly failure handler, it is surfaced as ``reason``
so operators can diagnose without grepping logs.
# List Sessions
Source: https://docs.idun-group.com/api-reference/runtime/list-sessions
/standalone/openapi.json get /agent/sessions
List session summaries from the active memory backend.
Returns 501 when the adapter doesn't support listing (an ADK agent,
or a LangGraph agent without a checkpointer). When SSO is enabled,
the user id from the JWT is forwarded to the adapter for per-user
scoping.
# Reload Config
Source: https://docs.idun-group.com/api-reference/runtime/reload-config
/standalone/openapi.json post /reload
Reload the agent configuration from the manager or a file.
The optional ``_auth`` dependency consults
``app.state.reload_auth`` (configured via ``create_app(reload_auth=...)``)
and, if set, invokes it. The configured callable is responsible for
raising :class:`fastapi.HTTPException` to deny the request.
# Run
Source: https://docs.idun-group.com/api-reference/runtime/run
/standalone/openapi.json post /agent/run
Canonical AG-UI interaction endpoint.
Accepts RunAgentInput, returns SSE stream of AG-UI events.
# Runtime Config Js
Source: https://docs.idun-group.com/api-reference/runtime/runtime-config-js
/standalone/openapi.json get /runtime-config.js
Return a tiny script that seeds ``window.__IDUN_CONFIG__``.
# Stream
Source: https://docs.idun-group.com/api-reference/runtime/stream
/standalone/openapi.json post /agent/stream
Process a message with the agent, streaming ag-ui events.
# Bulk Delete Traces
Source: https://docs.idun-group.com/api-reference/traces/bulk-delete-traces
/standalone/openapi.json delete /admin/api/v1/traces
Bulk-delete traces matching the same filters as ``GET ``.
Strategy: select the matching trace ids first, then DELETE both
tables by id. Two queries instead of a join keeps the SQL portable
across PG and SQLite and avoids ``DELETE ... USING`` syntax that
SQLite does not support.
# Delete Trace
Source: https://docs.idun-group.com/api-reference/traces/delete-trace
/standalone/openapi.json delete /admin/api/v1/traces/{otel_trace_id}
Delete a single trace and cascade to its spans.
The trace row's PK is composite ``(started_at, otel_trace_id)`` so
we look it up first to grab ``started_at``, then delete by full PK.
Spans match on the 8-byte trace id slice. The 404 path leaves both
tables untouched.
# Get Trace Detail
Source: https://docs.idun-group.com/api-reference/traces/get-trace-detail
/standalone/openapi.json get /admin/api/v1/traces/{otel_trace_id}
Return the trace + its span tree.
Trace lookup uses the full 16-byte W3C id stored on
``standalone_trace``. The span query uses the trailing 8-byte slice
(``trace_id[8:]``) — that's the form the exporter persists onto
``standalone_span.otel_trace_id``.
# List Traces
Source: https://docs.idun-group.com/api-reference/traces/list-traces
/standalone/openapi.json get /admin/api/v1/traces
List traces sorted by ``started_at DESC`` with cursor pagination.
Filters compose AND-style. ``cursor`` is opaque (base64url JSON);
callers should treat it as a token to round-trip back. The list
fetches ``limit + 1`` rows so it can detect whether a next page
exists without a separate ``COUNT`` query.
# Trace Pipeline Health
Source: https://docs.idun-group.com/api-reference/traces/trace-pipeline-health
/standalone/openapi.json get /admin/api/v1/traces/_health
Return queue depth + drop count from the running exporter.
Safe-default: when no exporter is attached to ``app.state`` (engine
booted without a trace pipeline, or T7 wiring not present), return
zeroes plus ``writer_running=False`` so the UI panel stays
renderable instead of erroring.
Declared **before** the ``/{otel_trace_id}`` route so the literal
path takes precedence over the path-parameter route at match time
— FastAPI iterates in declaration order.
Also surfaces ``database_dialect`` (read off the sessionmaker bind)
so the UI can conditionally render the SQLite operational banner
without a second round-trip.
# Architecture
Source: https://docs.idun-group.com/architecture
How idun-agent-engine and idun-agent-standalone connect to turn LangGraph or ADK agent code into a production FastAPI service.
## System overview
Idun ships as two packages. **`idun-agent-engine`** is the SDK that wraps your agent into a FastAPI service. **`idun-agent-standalone`** is a self-sufficient process that bundles the engine, a Next.js chat UI, an admin panel, and a traces viewer.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
subgraph Idun["idun-agent-standalone (one process)"]
direction TB
UI["Chat UI / Admin / Traces"] --> ENG["Engine SDK"]
ENG --> DB[(Postgres / SQLite)]
end
Users --> UI
Admin --> UI
ENG --> Agent["Your LangGraph / ADK agent"]
Agent --> LLM["LLMs / MCP / tools"]
```
## Components
### Idun Agent Schema
The shared Pydantic model library published to PyPI as `idun-agent-schema`. It defines the config structures, API payloads, and resource schemas the other packages consume. Schema changes start here and propagate to the engine and standalone.
### Idun Agent Engine
The Python SDK that wraps agent frameworks (LangGraph and Google ADK) into production-ready FastAPI services. You provide your agent code and a configuration; the engine adds AG-UI streaming, memory and checkpointing, observability, guardrails, and MCP tool management.
The engine exposes these endpoints:
| Endpoint | Method | Purpose |
| ------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/health` | GET | Health check; see contract below |
| `/_engine/info` | GET | Engine introspection (version, capabilities, mounted endpoints) |
| `/reload` | POST | Engine-only hot-reload entry point. See [Hot reload (engine-only)](#hot-reload-engine-only) below. **Disabled in standalone**: returns HTTP 403; reloads go through `/admin/api/v1/*` instead. |
| `/agent/run` | POST | AG-UI interaction (SSE streaming) |
| `/agent/sessions` | GET | List sessions for the live agent |
| `/agent/sessions/{session_id}` | GET | Inspect a single session |
| `/agent/graph` | GET | Agent graph in framework-agnostic IR form |
| `/agent/graph/mermaid` | GET | Same graph rendered as Mermaid source |
| `/agent/graph/ascii` | GET | Same graph rendered as ASCII art |
| `/agent/config` | GET | Current agent configuration |
| `/agent/capabilities` | GET | Agent capability discovery |
`POST /agent/stream` and `POST /agent/copilotkit/stream` are also exposed but deprecated; new clients should use `POST /agent/run`.
#### `/health` response contract
`/health` is meant for load balancers and Kubernetes liveness/readiness probes. The response shape is fixed (`libs/idun_agent_engine/src/idun_agent_engine/server/routers/base.py`):
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"status": "ok",
"service": "idun-agent-engine",
"version": "0.6.2",
"agent_ready": true,
"agent_name": "my-adk-agent"
}
```
| Field | Type | Notes |
| ------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | `"ok"` or `"degraded"` | `degraded` whenever no agent is registered: pre-onboarding state, standalone admin-only mode after an assembly error, or engine boot that bailed on a bad config. |
| `service` | string | Always `idun-agent-engine`. |
| `version` | string | Engine semver, useful for catching stale rollouts behind a load balancer. |
| `agent_ready` | bool | `true` only when `/agent/*` will accept requests. |
| `agent_name` | string or null | Pulled from the loaded agent's config; null when none is loaded. |
| `reason` | string (optional) | Only present on `degraded` when the standalone's assembly handler captured a boot error; carries the short diagnostic. Field is absent (not `null`) on the happy path. |
For probes, gate readiness on `status == "ok"` (or equivalently `agent_ready == true`). The endpoint is unauthenticated by design so external probes can reach it; this is intentional and called out in [Authentication](/auth/overview).
#### Hot reload (engine-only)
`POST /reload` rebuilds the agent in place from a YAML file the process can read:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sX POST http://localhost:8000/reload \
-H 'Content-Type: application/json' \
-d '{"path": "/etc/idun/config.yaml"}'
```
Sequence (`server/routers/base.py`): parse the new config, then `cleanup_agent(app)`, then `configure_app(app, new_config)`. If config parsing fails the old agent is still alive (cleanup never ran), so the engine returns HTTP 500 with the upstream error and keeps serving. If `configure_app` fails after cleanup, the engine is left without an agent and `/agent/*` returns 503 `agent_not_ready` until a successful reload or process restart. There is no automatic rollback.
`POST /reload` is **unauthenticated by default**. `create_app(reload_auth=...)` accepts a FastAPI-dependency callable that runs before the handler; raise `HTTPException` from it to reject a request. In the **standalone**, this hook is set to a built-in `reload_disabled` that returns HTTP 403, so `POST /reload` is unusable on the standalone surface. The standalone owns reloads through the validated `/admin/api/v1/*` pipeline (see [Reload pipeline outcomes](/troubleshooting#reload-pipeline-outcomes)). The endpoint is therefore a knob for engine-only deployments (`idun agent serve --source file`).
[Learn more about supported frameworks](/frameworks/overview)
### Idun Agent Standalone
The self-sufficient app that bundles the engine, a Next.js UI (chat + admin + traces), an admin REST API, password or unauthenticated admin auth, DB seeding, and a validate-rebuild reload pipeline, all in one process.
Standalone adds two things on top of the engine:
* **Embedded UI.** Chat at `/`, admin at `/admin/`, traces at `/admin/traces`. The UI is built as a static Next.js export and bundled into the standalone wheel, so there is no separate frontend to deploy.
* **Reload-over-restart.** Edits made through the admin UI route through a validate-rebuild-reload pipeline; engine init failures roll back the DB write so the running agent stays serving the previous config.
[Learn more about Idun Agent Standalone](/standalone/overview)
## Config flow
Configuration drives everything. A single `EngineConfig` object determines server settings, agent framework, observability providers, guardrails, memory, and MCP servers.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
YAML["config.yaml"] -->|first boot| Seed["seed step"]
Seed --> DB[(DB: steady-state truth)]
AdminAPI["Admin REST API"] -->|edits| DB
DB --> Reload["validate → rebuild → reload"]
Reload --> Engine["Engine instance"]
```
YAML is the seed shape; the database is the runtime source of truth. After first boot, mutations flow through the admin REST and trigger a validated reload of the engine instance. Init failures roll back to the previous config, so the running agent never crashes mid-flight on a bad edit.
## Next steps
LangGraph or Google ADK
The single-process app that bundles the engine, UI, admin, and traces
Deploy your first agent in under 30 minutes
# Authentication
Source: https://docs.idun-group.com/auth/overview
Admin-panel auth modes (`none` / `password`) and the environment variables that drive them.
The standalone has two distinct authentication surfaces. This page covers the admin panel; for agent-route SSO, see [SSO](/auth/sso).
| Surface | Routes | Mechanism | Where it lives |
| ----------------- | ---------------------------------------- | ----------------------------------------------------- | --------------------------------------------- |
| **Admin panel** | `/admin/*`, `/login/`, `/admin/api/v1/*` | `none` or `password` (bcrypt + signed session cookie) | Environment variables |
| **Agent runtime** | `/agent/*` (chat side) | OIDC JWT validation with per-user allowlists | `sso:` block in config — see [SSO](/auth/sso) |
Health and metadata routes (`/health`, `/_engine/info`) stay open on both surfaces. Treat the engine as a process running inside a trusted boundary; see [Production hardening](/deployment/hardening) for network-layer controls.
## Environment variables
The admin panel is configured by environment, not config. The mode is selected by `IDUN_ADMIN_AUTH_MODE`.
| Variable | Default | Description |
| -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IDUN_ADMIN_AUTH_MODE` | `none` | Admin gate. `none` for laptop dev; `password` for containers and shared deployments. |
| `IDUN_ADMIN_PASSWORD_HASH` | empty | Bcrypt hash that seeds the singleton admin row on first boot. Required when `auth_mode = password`. Generate with `idun hash-password`. |
| `IDUN_SESSION_SECRET` | empty | At least 32 characters. Signs the `idun_session` cookie. Required when `auth_mode = password`; startup fails fast if shorter. |
| `IDUN_SESSION_TTL_HOURS` | `24` | Session cookie lifetime in hours (range `1..720`). Sliding renewal extends the cookie on every request. |
| `IDUN_ALLOW_OPEN_ADMIN` | `false` | Opt-in flag that lets `auth_mode=none` bind `0.0.0.0` / `::`. Containers and trusted networks only. Without this, an open-admin process is forced to loopback. |
## `IDUN_ADMIN_AUTH_MODE=none`
Every admin route is open. No login screen. Anyone who can reach the process can read and write configuration through `/admin/`.
Intended for laptop development. The runtime binds to `127.0.0.1` by default so the open admin is not exposed beyond `localhost`. To bind to `0.0.0.0` (e.g., inside a container) you must set `IDUN_ALLOW_OPEN_ADMIN=1` — the runtime refuses otherwise.
## `IDUN_ADMIN_AUTH_MODE=password`
A single admin user logs in at `/login/` with a password. The standalone signs an `idun_session` cookie (`HttpOnly`, `SameSite=Lax`, `Secure` when behind HTTPS) and gates `/admin/*` on it. The published container image flips this mode on by default.
Setup is two env vars at first boot:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# 1. Generate the bcrypt hash on a workstation (never on the server).
idun hash-password
# Password: ********
# Confirm: ********
# $2b$12$abc... <- copy this
# 2. Inject both at deploy time.
IDUN_ADMIN_AUTH_MODE=password \
IDUN_ADMIN_PASSWORD_HASH='$2b$12$abc...' \
IDUN_SESSION_SECRET="$(openssl rand -hex 32)" \
idun serve
```
`IDUN_ADMIN_PASSWORD_HASH` is consumed only on first boot to seed the singleton admin row. Subsequent boots ignore the env var. Rotate the password from the admin UI, or truncate the `standalone_admin_user` table and reboot to re-seed from the env.
The standalone supports **exactly one admin user**. The `standalone_admin_user` table has a fixed primary key of `"singleton"` and the admin URL never carries an id. There is no allowlist or multi-user concept for the admin panel today. For per-user policy, restrict access at the network layer (VPN, mTLS gateway, IP allowlist on the load balancer). See [Production hardening](/deployment/hardening) for the full checklist.
## What's next
Per-agent OIDC for `/agent/*` runtime routes, with `allowed_domains` / `allowed_emails` allowlists.
Env-var checklist for `password` mode + reverse-proxy / TLS notes.
What `agent_not_ready` and reload-pipeline errors mean.
# SSO
Source: https://docs.idun-group.com/auth/sso
Per-agent OIDC SSO that validates a JWT on every `/agent/*` request, with per-user allowlists by domain or email.
Idun Engine's agent runtime supports per-agent OIDC SSO. When enabled, every request to `/agent/*` requires a valid `Authorization: Bearer ` header. The canonical AG-UI entry point is `POST /agent/run`; deprecated shims (`/agent/invoke`, `/agent/stream`, `/agent/copilotkit/stream`) are gated by the same dependency. Tokens are validated against the issuer's JWKS endpoint discovered via `{issuer}/.well-known/openid-configuration`; nothing is stored or cached beyond the JWKS rotation window.
Health and metadata routes (`/health`, `/_engine/info`) stay open. SSO does not gate the admin panel; see [Authentication](/auth/overview) for that surface.
## Configuration
Add an `sso` block to your engine `config.yaml`:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sso:
enabled: true
issuer: "https://accounts.google.com"
client_id: "123456789012-abc123def456ghi789.apps.googleusercontent.com"
audience: "123456789012-abc123def456ghi789.apps.googleusercontent.com"
allowed_domains:
- "example.com"
allowed_emails:
- "auditor@partner.example"
```
Open the running standalone at `/admin/sso/`. Pick a provider preset (Google, Microsoft Entra ID, Okta, or Custom OIDC), paste the issuer URL, client ID, audience, and any domain or email allowlists, then save. The reload pipeline validates the config and re-instantiates the engine with the new SSO settings.
## Fields
| Field | Required | Description |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `enabled` | yes | Toggle SSO enforcement on protected routes. |
| `issuer` | yes | OIDC issuer URL. Used to discover the JWKS endpoint via `.well-known/openid-configuration`. |
| `client_id` | yes | OAuth 2.0 client ID. Used as the default audience for JWT validation. |
| `audience` | no | Expected JWT `aud` claim. Defaults to `client_id` if not set. (Okta client-credentials tokens use `api://default`.) |
| `allowed_domains` | no | Allow only tokens whose `email` claim matches one of these email domains. Example: `["company.com"]`. |
| `allowed_emails` | no | Allow only tokens whose `email` claim exactly matches one of these addresses. Example: `["admin@partner.com"]`. |
## Restricting access to specific users
Use `allowed_domains` and `allowed_emails` together. Both lists are applied after JWT signature verification; a valid token from an allowed provider is still rejected if its email doesn't match either list.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sso:
enabled: true
issuer: "https://accounts.google.com"
client_id: "123456789012-abc123def456ghi789.apps.googleusercontent.com"
allowed_domains:
- "example.com" # everyone with a @example.com Google account
allowed_emails:
- "auditor@partner.example" # plus this one outside contractor
```
If both lists are unset, any valid token from the configured issuer is accepted (still scoped by `client_id` / `audience`).
## Supported providers
Any OIDC-compliant provider works as long as it publishes a JWKS endpoint via `.well-known/openid-configuration`. Tested presets in the admin UI:
* **Google Workspace** — issuer `https://accounts.google.com`
* **Microsoft Entra ID / Azure AD** — multi-tenant `https://login.microsoftonline.com/common`, or single-tenant `https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555`
* **Okta** — issuer typically `https://example.okta.com/oauth2/default`; audience usually `api://default`
* **Custom OIDC** — any provider that publishes a `.well-known/openid-configuration` document
## How validation works
1. The engine discovers the JWKS endpoint from the issuer's `.well-known/openid-configuration` at startup.
2. Per request, the engine reads `Authorization: Bearer `.
3. JWT signature validated against JWKS. Algorithm and key come from the JWT header.
4. `aud` and `iss` claims matched against `audience` and `issuer`.
5. If `allowed_domains` or `allowed_emails` is set, the token's `email` claim is checked against both lists.
6. On success: request proceeds. On any failure: `401 Unauthorized`.
## What users see
With SSO enabled, the bundled chat UI prompts users to sign in with the configured provider before any conversation starts. The standalone runs the OAuth flow against your issuer and stores the resulting session locally.
After successful sign-in, the chat connects to `/agent/run` with the token attached. If the user's email doesn't match `allowed_domains` / `allowed_emails`, the engine returns `401` and the chat shows the sign-in screen again. For programmatic clients, obtain the token through your provider's OAuth 2.0 + PKCE flow and call `/agent/run` with the resulting access token in `Authorization: Bearer `.
## What's next
Admin-panel auth (`none` / `password`) for `/admin/*`.
Env-var checklist + reverse-proxy / TLS notes.
What `agent_not_ready` and reload-pipeline errors mean.
# Changelog
Source: https://docs.idun-group.com/changelog/overview
Latest updates and releases for Idun Engine.
Track the latest changes, improvements, and bug fixes. The canonical per-package changelogs live in the repo: [`idun-agent-engine`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/CHANGELOG.md), [`idun-agent-standalone`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_standalone/CHANGELOG.md).
Patch release. Hardens the standalone reload path under concurrency, makes config-from-API async for enrolled-mode boot, and adds a central telemetry sink so enrolled agents report traces to the manager instead of a local DB.
### Added
* **Central telemetry sink (enrolled mode).** When `IDUN_MANAGER_HOST` and `IDUN_AGENT_API_KEY` are both set, the standalone trace writer ships span/trace batches over HTTP to the manager's `/collect` endpoint instead of writing to the local database. Bootstrap selects between local-DB and manager-HTTP modes from settings, and the local retention scheduler is skipped in manager mode.
### Fixed
* **Reload concurrency.** `POST /reload` now serializes on a per-app lock, builds the new agent **before** tearing down the old one, and swaps it in atomically. Two HIGH-severity bugs are resolved: a deadlock when reloads overlapped, and a mid-run `'NoneType' object is not a mapping` crash when a run was in flight during a swap. In-flight streaming runs are now drained before the old agent is closed — and counted at stream construction rather than first iteration, closing the residual window where a fast reload could shut an agent down out from under a run whose stream hadn't started yet.
### Changed
* **`with_config_from_api` is now async**, built on `httpx`, with trailing-slash normalization and consistent error handling — supporting the enrolled-mode boot flow.
### Maintenance
* The optional `idun-agent-engine[guardrails]` extra installs cleanly from PyPI again now that the upstream `guardrails-ai` project was restored from quarantine. The published engine wheel is unchanged; only the dev/CI install path was affected.
Patch release. No engine code changes — the engine wheel just bundles the standalone UI at 0.6.2, which fixes a cluster of SPA-navigation bugs in the admin surface under Next.js 15 `output: "export"` + FastAPI SPA-rewrite.
### Fixed
* **AuthGuard `?next=` preservation.** Login now round-trips back to the admin page the operator tried to reach instead of defaulting to chat.
* **Hard-nav after login.** The success path forces a full navigation so the next request reissues with the freshly-set session cookie.
* **Trace detail on soft nav.** Switched trace-id resolution to the reactive `usePathname()` hook so `` clicks load the right trace instead of rendering "No spans recorded" until refresh.
* **Sidebar Traces & post-delete hard-nav.** Avoids a Next.js router-cache collision that could mount the trace-detail component at the list URL.
Patch release. Opens the password-mode chat surface that the v0.6.0 hardening accidentally locked behind `/login`, and propagates a stable per-session user identity end-to-end.
### Added
* **Per-user scoping under password auth.** The engine accepts a per-request `X-Idun-User-Id` header and binds it to a `current_user_id` ContextVar that adapters and the standalone trace writer read at the start of every `/agent/*` invocation, so chat history and traces can be scoped per user without a full OIDC ladder.
### Fixed
* **Password-mode chat.** The chat shell is reachable again under password mode instead of being gated behind `/login`.
The release where Idun Engine becomes **the third path between LangGraph Cloud and DIY**. One `pip install` ships your LangGraph or Google ADK agent as a production-ready FastAPI service with a bundled Next.js chat / admin / traces UI, 15+ guardrails, multi-provider observability via OpenTelemetry, MCP tool governance, 5 memory backends, and OIDC. Self-hosted, open source, no vendor lock-in.
[Read the launch post →](https://idun-group.com/blog/2026-05-17-third-path-engine-v0.6)
### Highlights
* **One wheel, three services collapsed.** `pip install idun-agent-engine` bundles the Next.js admin / chat / traces UI and the `idun` CLI. The separate `idun-agent-manager` (FastAPI) and `idun-agent-web` (React) services are gone — \~151,000 lines removed in one cut so the install path is `pip` + `idun setup` + `idun serve`, nothing else.
* **Standards composed, not invented.** AG-UI streaming (CopilotKit-compatible), OpenTelemetry tracing across 5 providers, MCP tool servers (Linux Foundation governance since 2025), OIDC auth (Google + Microsoft). Pick Idun and your investment moves with you.
* **Traces v1 lands.** Trace store (asyncpg COPY), REST endpoints, list + detail UI with span tree and waterfall views. ADK spans projected to OpenInference attributes so they flow through the same pipeline as LangGraph spans.
* **Brand + docs rebrand.** "Idun Platform" → **Idun Engine** across the public surface. Docs moved to [docs.idun-group.com](https://docs.idun-group.com) with a paper/ink editorial design that matches the website.
### Added
* Standalone trace store: schema, asyncpg writer, REST endpoints, list + detail UI.
* ADK adapter spans projected onto OpenInference attributes so they flow through the same trace pipeline as LangGraph spans.
* `GoogleGenAIInstrumentor` auto-attached for Gemini cache-bucket capture.
* `BaseAgent.register_run_event_observer(observer)`: async callbacks receive every AG-UI event from `/agent/run` before SSE encoding.
* `/health` now reflects engine assembly state, returning `agent_ready: bool` and `status: "degraded"` when configured agents failed to come online.
* `IDUN_UI_DIR` env var: mount a custom static UI at `/`; previous JSON info payload moves to `/_engine/info`.
* `create_app(..., reload_auth=...)`: pluggable FastAPI dependency for the `POST /reload` endpoint.
* Admin dashboard counters now sourced from the trace store (session count, run count, recent activity timeline).
* Standalone seeder persists all top-level YAML config blocks (agent, guardrails, mcp, prompts, integrations, observability) on first boot.
* Admin link surfaced on chat layouts; new Developer sidebar group links to `/docs` and `/redoc`.
* PostHog tracking + masked session replay (opt-in) in the chat UI.
* LangGraph auto-detection scanner: detects agents built via known factories and `CompiledStateGraph` annotations, so the `graph_definition` config can be inferred rather than hand-written.
* `/admin` activity dashboard: traces-driven KPIs across 24h / 7d / 30d, requests-per-minute sparkline, p50 / p95 latency dual-line chart, top-errors table.
* `get_langchain_tools_sync()` for module-load callers: makes MCP tools available in synchronous import-time contexts (e.g. `create_deep_agent` factories) that can't `await`.
* Microsoft OIDC alongside Google for SSO. Multi-provider auth, multi-tenant support.
* Google Chat integration joins WhatsApp, Discord, Slack, and Microsoft Teams.
* SSE / HTTP transport for ADK MCP toolsets, alongside the existing stdio.
* Real-LLM end-to-end test suite (`pytest-e2e`): LangGraph + ADK adapter coverage with real LLM calls in CI, plus Playwright specs for chat and admin reload flows.
* Coding-guidelines drift advisory CI: rules-as-YAML pipeline, headless-Claude review check.
### Changed
* **The engine wheel bundles standalone + schema.** `pip install idun-agent-engine` ships the `idun` console script (mapped to `idun_agent_standalone.cli:main`), the chat/admin/traces Next.js bundle, alembic migrations, and `idun-agent-schema` as a runtime dependency.
* **`guardrails-ai` is now an optional extra (`[guardrails]`)**. Install with `pip install idun-agent-engine[guardrails]` if you use the Guardrails-AI integration.
* **Standalone admin DB rework.** Run `idun setup` after upgrade to apply the new migrations.
* **Secure-by-default host binding.** `idun serve` no longer binds `0.0.0.0` by default; bind-all requires explicit opt-in.
* **Standalone "admin-only mode" fails loud** instead of logging a single WARNING and serving 503s silently.
* **Catch-all 404 for `/admin/api/v1/`** returns JSON 404 instead of falling through to the chat UI HTML.
* **Rebrand: "Idun Platform" → "Idun Engine"** across `docs.json`, OG metadata, navbar, search prompt, and the docs naming guidelines. The GitHub repo name (`idun-agent-platform`) is unchanged.
* **Docs moved from MkDocs to Mintlify** at `docs.idun-group.com`. New paper / ink editorial design matches the website's Engine product page.
* **`ObservabilityConfig` schema restructured.** The discriminated union now stamps a `provider` literal onto each inner provider config; legacy YAML keeps working because validators auto-sync the parent and inner fields.
* **`AdkAgentConfig.app_name` now optional.** When omitted, it auto-derives from `name` (lowercase + non-alphanumeric → underscore). Non-breaking; explicit values still win.
* **BREAKING: `RestrictToTopicConfig.topics`** is split into `valid_topics` and `invalid_topics`. v0.5.x configs using `topics:` will fail validation; rename the key in your YAML.
### Deprecated
* **`/agent/invoke`** is marked `deprecated=True` in OpenAPI. Migrate to `POST /agent/run` (AG-UI SSE stream). Removal targeted for 0.7.
### Removed
* **`services/idun_agent_manager/`** + **`services/idun_agent_web/`** — the FastAPI manager service and React admin UI. \~90,219 lines across 367 files. Replaced by the bundled Next.js admin / chat / traces UI at `services/idun_agent_standalone_ui/`, served by the engine wheel.
* **TUI (`idun init` Textual UI) and the Streamlit demo.** 4,093 lines across 26 files. Replaced by the setup wizard inside the bundled Next.js admin at `/admin`.
* **MkDocs site (`old-docs/`, `mkdocs.yml`).** 11,638 lines across 160 files. Replaced by Mintlify at `docs.idun-group.com`.
* **Haystack agent adapter, `HaystackAgentConfig` schema, `langfuse-haystack` runtime dependency, and all related tests, docs, and UI surfaces.** Migrate Haystack agents to LangGraph or ADK before upgrading.
* Total: \~151,820 lines deleted in one commit (`9a54145d`, "finalize standalone migration"); 712 file deletions; net reduction of 150,883 lines.
### Fixed
* MCP tool calls wrapped so AG-UI can serialize event payloads end-to-end.
* Guardrail install failures surface to the reload pipeline instead of silently degrading the running agent.
* Trace pipeline no longer drops trace rows when the runtime OTel context leaks a parent.
* Prompts resolve from the `EngineConfig` snapshot in standalone, mirroring the MCP registry pattern.
* Tool calls in the chat UI render their args and result instead of the literal string `"null"`.
* Chat history sidebar auto-refreshes after a run and sorts newest first.
### Security
* Third-party GitHub Actions pinned to SHAs; dep-audit job; `SECURITY.md` hardened.
* Socket security gates wired into PRs and both publish workflows.
* Next.js and aiohttp CVE patches; `pinact` pre-commit hook.
* `guardrails-ai` moved out of the default install footprint.
### Standards composed
Idun Engine composes open standards rather than inventing new protocols. Your investment in any of these moves with you if you ever leave:
* **LangGraph** and **Google ADK** — agent frameworks
* **AG-UI** — streaming protocol, compatible with any CopilotKit client
* **OpenTelemetry** — tracing, exported to Langfuse, Phoenix, LangSmith, or GCP Trace
* **MCP** — tool servers, under Linux Foundation governance since 2025
* **OIDC** — auth (Google, Microsoft, any compliant provider)
* **FastAPI**, **Pydantic** — HTTP layer and config models
### Upgrading from 0.5.x
1. Uninstall any separate `idun-agent-standalone` wheel, since it is now bundled in the engine wheel.
2. Install with `pip install idun-agent-engine[guardrails]==0.6.0` if you use Guardrails-AI; otherwise plain `pip install idun-agent-engine==0.6.0`.
3. Migrate Haystack agent configs to LangGraph or ADK.
4. **Update `RestrictToTopicConfig` YAML**: rename `topics:` to either `valid_topics:` or `invalid_topics:` (or both). The old key is no longer accepted by the schema.
5. Switch smoke checks from `curl /health` to `POST /agent/run` (the deprecated `/agent/invoke` still works but will be removed in 0.7).
6. Run `idun setup` after upgrade so the new standalone admin DB migrations apply.
7. If you wrote scripts against `docs.idunplatform.com`, point them at `docs.idun-group.com` (the old domain redirects until further notice).
* Mintlify docs migration and UI rework
* Guardrails hub URL fixes
* LangSmith run name support for LangGraph and ADK agents
* Check connection for observability and memory providers
* MCP discover tools timeout
* ADK database session service support
* Observability V2 configuration schema
* Multi-provider observability support
* Prompt management system
* MCP servers
* Initial public release
* LangGraph and ADK framework support
* Guardrails AI integration
* Langfuse and Phoenix observability
* SQLite and PostgreSQL checkpointing
* Discord, Slack, and WhatsApp integrations
* SSO/OIDC authentication
* Docker Compose deployment
For full details on any release, see the [GitHub releases](https://github.com/Idun-Group/idun-agent-platform/releases).
# CLI
Source: https://docs.idun-group.com/cli/overview
The Idun CLI: one console script (idun) for serving the standalone, scaffolding a new project, hashing passwords, and running engine-only mode.
`idun` is the single console script installed by `pip install idun-agent-engine`. It covers the full lifecycle: scaffold a project, serve the standalone, run engine-only, hash a password, pre-stage migrations. All commands accept `--help`.
## Quick reference
| Verb | Migrates schema | Seeds from `config.yaml` | Serves uvicorn | Notes |
| -------------------- | --------------- | -------------------------- | -------------- | -------------------------------------------------------------------------------------------- |
| `idun init` | Yes | Yes (only if rows missing) | Yes | First-run bootstrap. Opens the browser to the wizard if no agent row exists yet. Idempotent. |
| `idun setup` | Yes | Yes (only if rows missing) | No | Pre-stage the DB during a deploy, or re-run the seeder after wiping rows. |
| `idun serve` | No | No | Yes | Steady-state startup. DB is the source of truth; `config.yaml` is not consulted. |
| `idun agent serve` | No | No | Yes | Engine-only mode. No DB, no admin surface, just the engine routes. |
| `idun hash-password` | No | No | No | Print a bcrypt hash for `IDUN_ADMIN_PASSWORD_HASH`. |
The seeder writes a row only if the corresponding table is empty. To re-seed after `config.yaml` changes, clear the row first (or delete `idun_standalone.db` for a full reset) and re-run `idun setup`. See [Troubleshooting](/troubleshooting) for the full re-seed recipe.
## Common flows
```bash New project theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir my-agent && cd my-agent
idun init # runs migrations, opens the onboarding wizard in your browser
```
```bash Existing project theme={"theme":{"light":"github-light","dark":"github-dark"}}
# IDUN_CONFIG_PATH points at config.yaml (default ./config.yaml)
idun setup # one-time: migrations + seed the DB
idun serve # bind 127.0.0.1:8000, admin at /admin/, traces at /admin/traces/
```
```bash Engine-only mode theme={"theme":{"light":"github-light","dark":"github-dark"}}
# No DB, no admin REST, no chat UI. Engine routes only.
idun agent serve --source file --path config.yaml
```
```bash Password auth theme={"theme":{"light":"github-light","dark":"github-dark"}}
HASH=$(idun hash-password)
SECRET=$(openssl rand -hex 32)
IDUN_ADMIN_AUTH_MODE=password \
IDUN_ADMIN_PASSWORD_HASH="$HASH" \
IDUN_SESSION_SECRET="$SECRET" \
idun serve
```
## Commands
### `idun init`
Initialize Idun in the current folder and launch chat + admin in one step. Runs migrations, seeds from `config.yaml` if present, opens the browser, and boots the standalone server. Idempotent, so re-running on an already-initialized folder just re-launches.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun init
```
| Flag | Default | Description |
| -------------- | ---------------------------- | ----------------------------------------------------------------- |
| `--port ` | `IDUN_PORT` env, then `8000` | Bind port. |
| `--no-browser` | off | Skip the automatic browser open. Useful for headless / Cloud Run. |
### `idun serve`
Run the standalone server. The DB is the source of truth in steady state; on first boot, if the DB is empty and `IDUN_CONFIG_PATH` points to a YAML file, the file seeds the DB.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun serve
```
No flags. All configuration is read from environment variables (see [Environment variables](#environment-variables)).
### `idun setup`
Create the DB schema and seed it from YAML if the DB is empty. Called automatically by `serve` and `init`, but operators can run it directly to pre-stage migrations during a deploy.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun setup
```
| Flag | Default | Description |
| ----------------- | -------------------------------------------- | -------------------- |
| `--config ` | `IDUN_CONFIG_PATH` env, then `./config.yaml` | Bootstrap YAML file. |
### `idun agent serve`
Run an engine-only server with no DB and no admin surface. Useful when you have your own admin stack and only want the runtime layer.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun agent serve --source file --path ./config.yaml
```
| Flag | Default | Description |
| --------------- | ----------------------------- | ----------------------------------------------- |
| `--source` | required | Must be `file` (load config from a local YAML). |
| `--path ` | required when `--source=file` | Path to the YAML. |
`--source manager` is still present in `idun agent serve --help` as a leftover from the pre-0.6.0 manager-tier deployment model. It is deprecated and unsupported: the manager service no longer ships with the platform. Use `--source file` only.
### `idun hash-password`
Print a bcrypt hash suitable for `IDUN_ADMIN_PASSWORD_HASH`. Used once at deploy time when setting up `password` auth mode.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun hash-password
```
| Flag | Default | Description |
| ------------------------ | ---------------------------- | ------------------------------------------------ |
| `--password ` | prompted (with confirmation) | Plaintext password to hash. Prompted if omitted. |
## Environment variables
`serve` (and the server side of `init`) reads every setting from the environment. The defaults below match what the standalone uses out of the box.
### Server binding
| Variable | Default | Description |
| ------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `IDUN_HOST` | `127.0.0.1` | Bind address. Set `0.0.0.0` for containers (requires `IDUN_ADMIN_AUTH_MODE=password` or `IDUN_ALLOW_OPEN_ADMIN=1`). |
| `IDUN_PORT` | `8000` | Bind port. |
| `IDUN_UI_DIR` | bundled static export | Override the chat/admin UI directory. |
### Database
| Variable | Default | Description |
| ------------------ | ------------------------------------------ | ------------------------------------------------------------ |
| `DATABASE_URL` | `sqlite+aiosqlite:///./idun_standalone.db` | SQLAlchemy URL. Use `postgresql+asyncpg://…` for Postgres. |
| `IDUN_CONFIG_PATH` | `./config.yaml` | Bootstrap YAML file used by `setup` / `init` on an empty DB. |
### Admin auth
| Variable | Default | Description |
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IDUN_ADMIN_AUTH_MODE` | `none` | Admin gate. `none` for laptop dev; `password` for containers. |
| `IDUN_ADMIN_PASSWORD_HASH` | empty | Bcrypt admin hash. Required on the first boot under `IDUN_ADMIN_AUTH_MODE=password` to seed the singleton admin row; subsequent boots ignore this variable and read the hash from the DB. Generate with `idun hash-password`. |
| `IDUN_SESSION_SECRET` | empty | At least 32 characters. Signs the `idun_session` cookie. Required when `IDUN_ADMIN_AUTH_MODE=password`; startup fails fast if shorter. |
| `IDUN_SESSION_TTL_HOURS` | `24` | Session cookie lifetime in hours (range `1..720`). |
| `IDUN_ALLOW_OPEN_ADMIN` | `false` | Opt-in flag that lets `auth_mode=none` bind `0.0.0.0` / `::`. Containers and trusted networks only. |
### Trace store
| Variable | Default | Description |
| ----------------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `IDUN_TRACE_RETENTION_DAYS` | `14` | Days of trace events the daily retention task keeps before dropping. |
| `IDUN_TRACES_INPUT_VALUE_MAX_BYTES` | `65536` | Per-attribute byte cap before the exporter truncates trace input/output values. |
| `IDUN_PRICES_REFRESH` | `false` | When `true`, fetch the LiteLLM model-prices snapshot at boot (5 s timeout, snapshot fallback). |
### Telemetry
| Variable | Default | Description |
| ------------------------------- | ------------- | -------------------------------------------------------------------------------- |
| `IDUN_TELEMETRY_ENABLED` | `true` | Set to `false` to disable anonymous usage telemetry. |
| `IDUN_DEPLOYMENT_TYPE` | `self-hosted` | Tag events with `cloud`, `self-hosted`, or `dev`. |
| `IDUN_TELEMETRY_IDENTIFY_USERS` | `true` | Set to `false` to keep all browser events anonymous (no email-level `identify`). |
| `IDUN_TELEMETRY_SESSION_REPLAY` | `true` | Set to `false` to ship browser analytics without recording sessions. |
## Verifying a running agent
Once `idun serve` is up, open the bundled chat UI at `http://localhost:8000/`. You land on the welcome screen.
Send a message. The chat surface streams the AG-UI response in real time and renders tool calls inline.
Open `/admin/traces/` to inspect the run after it completes. If you need to hit `/agent/run` directly from a script or external client, see [Programmatic chat](/guides/programmatic-chat) for the request shape and SSE event stream.
## API documentation
The OpenAPI schema is auto-published at `/docs` on the running standalone. Open `http://localhost:8000/docs` in your browser:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# macOS
open http://localhost:8000/docs
# Linux
xdg-open http://localhost:8000/docs
# Windows (cmd)
start http://localhost:8000/docs
```
## Next steps
Hit `/agent/run` directly with the request shape and SSE event stream.
Theme, layout, and full UI replacement.
Run `idun serve` on Google Cloud Run with a managed container.
# Configuration
Source: https://docs.idun-group.com/configuration
Engine configuration reference for server settings, agent framework, observability, guardrails, memory, MCP, SSO, and integrations.
A single `EngineConfig` controls every aspect of your agent service. Define it as a YAML file (the bootstrap path) or edit it through the standalone admin panel at `/admin/`. The structure is identical in both cases; the admin panel is just a UI over the same fields.
## Complete config example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
server:
api:
port: 8001
agent:
type: "LANGGRAPH"
config:
name: "Support Agent"
graph_definition: "./agent.py:graph"
checkpointer:
type: "sqlite"
db_url: "sqlite:///checkpoints.db"
observability:
- provider: "LANGFUSE"
enabled: true
config:
host: "https://cloud.langfuse.com"
public_key: "${LANGFUSE_PUBLIC_KEY}"
secret_key: "${LANGFUSE_SECRET_KEY}"
guardrails:
input:
- config_id: "DETECT_PII"
on_fail: "reject"
reject_message: "Request contains personal information."
output:
- config_id: "TOXIC_LANGUAGE"
on_fail: "reject"
mcp_servers:
- name: "time"
transport: "stdio"
command: "docker"
args: ["run", "-i", "--rm", "mcp/time"]
prompts:
- prompt_id: "system-prompt"
version: 1
content: "You are a support agent for {{ company_name }}."
tags: ["latest"]
sso:
enabled: true
issuer: "https://accounts.google.com"
client_id: "123456789.apps.googleusercontent.com"
allowed_domains: ["yourcompany.com"]
integrations:
- provider: "WHATSAPP"
enabled: true
config:
access_token: "${WHATSAPP_ACCESS_TOKEN}"
phone_number_id: "${WHATSAPP_PHONE_ID}"
verify_token: "${WHATSAPP_VERIFY_TOKEN}"
```
Values support `${ENV_VAR}` syntax for referencing environment variables at load time.
## Config sections
**`server`** -- HTTP server binding. Exposes `api.port` (default: `8000`). CORS allows all origins. The engine adds `Access-Control-Allow-Private-Network: true` so hosted UIs can reach local agents.
**`agent`** -- Framework type and framework-specific settings. The `config` fields change based on the `type` value. Supported types: `LANGGRAPH`, `ADK`. See the [frameworks overview](/frameworks/overview) for per-framework config details.
For LangGraph, provide a `StateGraph` via `graph_definition` (`path/to/file.py:variable_name`). The engine compiles it with the configured checkpointer. A `CompiledStateGraph` is also accepted (the engine extracts `.builder` and recompiles).
**`observability`** -- A list of providers, each with `provider`, `enabled`, and `config`. Multiple providers can be active simultaneously. Supported: `LANGFUSE`, `PHOENIX`, `GCP_TRACE`, `GCP_LOGGING`, `LANGSMITH`. See [observability guides](/observability/overview).
**`guardrails`** -- Split into `input` (validated before invocation) and `output` (validated after). Uses [Guardrails AI Hub](https://hub.guardrailsai.com/) guards, downloaded and run locally. Available guards: `BAN_LIST`, `DETECT_PII`, `NSFW_TEXT`, `COMPETITION_CHECK`, `BIAS_CHECK`, `CORRECT_LANGUAGE`, `GIBBERISH_TEXT`, `TOXIC_LANGUAGE`, `RESTRICT_TO_TOPIC`. See [guardrails reference](/guardrails/reference).
**`mcp_servers`** -- Model Context Protocol servers that provide tools to your agent. Supports `stdio`, `sse`, `streamable_http`, and `websocket` transports. See [MCP Servers](/mcp-servers/overview).
**`prompts`** -- Versioned prompt templates with Jinja2 variable support (`{{ variable }}`). Each entry has `prompt_id`, `version`, `content`, and `tags`.
**`sso`** -- OIDC JWT validation on agent endpoints. When enabled, requests to `/agent/run` must include a valid token. Supports `allowed_domains` and `allowed_emails` filtering. See [SSO](/auth/sso).
**`integrations`** -- Messaging platform connections. Each integration adds webhook endpoints to the engine. Supported: `WHATSAPP`, `DISCORD`.
**`checkpointer`** (inside `agent.config` for LangGraph) -- Conversation memory. Types: `memory` (in-process), `sqlite`, `postgres`. For ADK, use `session_service` and `memory_service` instead. See [memory guides](/memory/overview).
## Config sources
The standalone runtime has two ways to land config in the DB:
* **YAML bootstrap.** On first boot, if the DB is empty, the file at `IDUN_CONFIG_PATH` (default `./config.yaml`) seeds it. After that, the DB is the source of truth.
* **Admin panel.** Open `/admin/` and edit any section. Each save flows through the reload pipeline and re-instantiates the engine atomically; a bad save rolls back without disturbing the running agent.
For the engine-only mode (no DB, no admin), serve directly from a YAML file:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun agent serve --source file --path config.yaml
```
## Environment variables
| Variable | Purpose |
| ------------------------------- | ------------------------------------------------------------------------------- |
| `IDUN_CONFIG_PATH` | Path to config.yaml. Used by `idun setup` and `idun init` on first boot. |
| `IDUN_TELEMETRY_ENABLED` | Set to `false` to disable anonymous usage telemetry |
| `IDUN_DEPLOYMENT_TYPE` | Tag events with `cloud`, `self-hosted`, or `dev` |
| `IDUN_TELEMETRY_IDENTIFY_USERS` | Set to `false` to keep all browser events anonymous (no email-level `identify`) |
| `IDUN_TELEMETRY_SESSION_REPLAY` | Set to `false` to ship browser analytics without recording sessions |
The engine resolves `${VAR_NAME}` references in YAML values at config load time, so you can keep secrets out of your config files.
For the full list of browser events captured and PostHog masking conventions, see [Telemetry events](/observability/telemetry-events). For the complete list of standalone environment variables, see the [CLI reference](/cli/overview#environment-variables).
# Production hardening
Source: https://docs.idun-group.com/deployment/hardening
Production deployment checklist for the standalone server: bind address, admin auth, session secret, cookie security, TLS termination, secrets management, and trace retention.
This page lists the minimum production configuration for the standalone server. Every item here applies to any deployment exposed beyond `localhost`.
The settings model is in [`libs/idun_agent_standalone/src/idun_agent_standalone/core/settings.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_standalone/src/idun_agent_standalone/core/settings.py). All values below are validated at startup; missing or undersized secrets cause the process to refuse to boot.
## Required production values
| Env var | Default | Production value |
| --------------------------- | ----------- | ------------------------------------------------------------------------------- |
| `IDUN_HOST` | `0.0.0.0` | `0.0.0.0` (containers) or `127.0.0.1` (behind a reverse proxy on the same host) |
| `IDUN_ADMIN_AUTH_MODE` | `none` | `password` |
| `IDUN_SESSION_SECRET` | `""` | A random string, min 32 chars, from a secrets manager |
| `IDUN_ADMIN_PASSWORD_HASH` | `""` | A bcrypt hash from `idun hash-password`, set once at first boot |
| `IDUN_SESSION_TTL_HOURS` | `24` | `24` or shorter; `8` is reasonable for desk-staff admins |
| `DATABASE_URL` | SQLite file | Postgres async URL (`postgresql+asyncpg://...`) |
| `IDUN_TRACE_RETENTION_DAYS` | `14` | `14` to `90`, depending on your compliance window |
`IDUN_HOST` defaults to `0.0.0.0`. On a laptop or single-tenant box without a reverse proxy, set `IDUN_HOST=127.0.0.1` so the admin panel is not reachable from your LAN.
## Step 1: Switch admin auth to password mode
Generate the hash once on a workstation, never on the server:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun hash-password
# Password: ********
# Confirm: ********
# $2b$12$abc... ← copy this
```
Set it as `IDUN_ADMIN_PASSWORD_HASH` on the server. The hash is consumed only at first boot to seed the singleton admin row. Subsequent boots ignore the env var; rotate the password through the admin UI or by truncating the `standalone_admin_user` table and rebooting.
## Step 2: Generate a session secret
The session secret signs the `idun_session` cookie. Use any 32+ char random string:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
python -c "import secrets; print(secrets.token_urlsafe(48))"
```
Store the value in your secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault, Doppler, 1Password, etc.) and inject it as `IDUN_SESSION_SECRET` at deploy time. **Do not commit it to source control or your `.env` file.**
A secret shorter than 32 chars in `password` mode causes the process to fail at startup.
## Step 3: Set the bind address
Bind to localhost and terminate TLS at your proxy (Nginx, Caddy, Traefik, GCP Load Balancer, AWS ALB):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
IDUN_HOST=127.0.0.1
IDUN_PORT=8000
```
The proxy adds `X-Forwarded-Proto: https` and the standalone sets the `Secure` flag on the session cookie automatically when it sees that header (or when the request scheme is `https`).
Cloud Run injects a `$PORT` env var and routes HTTPS traffic directly to your container. Bind to all interfaces:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
IDUN_HOST=0.0.0.0
IDUN_PORT=$PORT
```
Cloud Run terminates TLS at the load balancer and forwards `X-Forwarded-Proto: https` to the container, so the `Secure` cookie flag activates correctly.
## Step 4: Switch the database to Postgres
SQLite is the default and fine for evaluation, but production deployments should use Postgres for concurrency and external backup:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
DATABASE_URL=postgresql+asyncpg://idun:strong-password@db.internal:5432/idun
```
The standalone runs Alembic migrations automatically at boot via `idun setup`. Run it once against the new database:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun setup
```
For backups, run `pg_dump` against the database; Idun stores admin state and trace events there.
## Step 5: Trace retention
By default, trace rows older than 14 days are dropped by a daily scheduler. Tune this to your compliance window:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
IDUN_TRACE_RETENTION_DAYS=30
```
Per-attribute byte cap (defends against unbounded trace row size from large LLM outputs):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
IDUN_TRACES_INPUT_VALUE_MAX_BYTES=65536
```
## Cookie security
The standalone sets cookie flags automatically:
* `HttpOnly`: always set, blocks JavaScript access to the session cookie
* `SameSite=Lax`: always set, blocks cross-site cookie use except top-level navigations
* `Secure`: set when the request is over HTTPS (scheme `https` or `X-Forwarded-Proto: https`)
If your reverse proxy is not forwarding `X-Forwarded-Proto`, the `Secure` flag will not activate and the cookie can leak over plain HTTP. Verify your proxy config.
## Engine route protection
Admin-panel auth (`IDUN_ADMIN_AUTH_MODE=password`) gates the `/admin/*` UI and admin REST. It does **not** gate the `/agent/*` runtime routes. To require an OIDC JWT on the agent API, enable per-agent SSO. See [SSO](/auth/sso).
## CORS
The engine ships with a wildcard CORS allowlist for local development. Tighten it before exposing the runtime to a browser on a different origin. Configure your reverse proxy to strip permissive CORS headers, or set the allowlist in the engine config.
## Pre-flight checklist
Before a production deploy:
* [ ] `IDUN_ADMIN_AUTH_MODE=password`
* [ ] `IDUN_SESSION_SECRET` is set to a 32+ char random string from your secrets manager
* [ ] `IDUN_ADMIN_PASSWORD_HASH` is generated via `idun hash-password` and the plaintext is not stored anywhere
* [ ] `IDUN_HOST` matches your deployment topology (localhost behind a proxy, `0.0.0.0` in a container)
* [ ] TLS terminates at a reverse proxy or managed load balancer
* [ ] `X-Forwarded-Proto: https` is forwarded so the `Secure` cookie flag activates
* [ ] `DATABASE_URL` points at Postgres (not SQLite on ephemeral disk)
* [ ] Backups run against the database
* [ ] `IDUN_TRACE_RETENTION_DAYS` matches your compliance window
* [ ] `/agent/*` routes are gated by per-agent SSO if the runtime is exposed to untrusted callers
## What's next
Require an OIDC JWT on the agent API.
What to do when reload fails or the admin panel says `agent_not_ready`.
# Deployment
Source: https://docs.idun-group.com/deployment/overview
Where to run the standalone in production. Cloud Run, Docker on any host, or engine-only mode for users with their own admin stack.
The standalone is a single FastAPI process. Anywhere you can run a Python container, you can run Idun. The pages under this section cover the supported deployment surfaces.
## Pick a target
Managed container with HTTPS, autoscaling, and Cloud SQL Postgres. The shortest path to a production deployment.
`Dockerfile.example` + `cloud-run.example.yaml` adapt to AWS Fargate, Azure Container Apps, GKE, a VM with Docker, or any container host.
Skip the DB and admin REST. Use `idun agent serve --source file --path config.yaml` when you have your own admin stack and only need the runtime.
The minimum production checklist: admin auth, TLS termination, bind address, Postgres, secrets management.
## What you're deploying
`pip install idun-agent-engine` produces one wheel containing:
* The engine runtime (`idun-agent-engine`)
* The standalone admin / chat / traces app (`idun-agent-standalone`)
* The shared schema (`idun-agent-schema`)
* The `idun` console script
In production you typically run `idun serve` inside a container, behind a TLS-terminating proxy or managed load balancer. SQLite is the default DB; Postgres is enabled by setting `DATABASE_URL`.
## Docker on any host
The standalone wheel installs cleanly into any minimal Python 3.12 image. A typical Dockerfile looks like:
```dockerfile Dockerfile theme={"theme":{"light":"github-light","dark":"github-dark"}}
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir idun-agent-engine
ENV IDUN_HOST=0.0.0.0
ENV IDUN_PORT=8000
CMD ["idun", "serve"]
```
The standalone repo ships a `Dockerfile.example` and a `cloud-run.example.yaml` you can copy as a starting point. See [Deploy to Cloud Run](/standalone/cloud-run) for the full walkthrough, including secrets management and the Cloud SQL annotations.
For any other container host:
* **AWS Fargate / ECS**: build the image, push to ECR, point the task definition at the image. Set `IDUN_HOST=0.0.0.0` and the `$PORT` mapping to whatever the load balancer expects.
* **Azure Container Apps**: same shape; set `IDUN_HOST=0.0.0.0` and `IDUN_PORT=80`.
* **Kubernetes**: a `Deployment` with one replica, a `Service` for the port, and a `Secret` for `IDUN_ADMIN_PASSWORD_HASH` + `IDUN_SESSION_SECRET` + `DATABASE_URL` is enough. Mount the secret as env. Scale to one replica per agent (the standalone is single-tenant).
* **A VM with Docker or Podman**: copy the image, `docker run` with the env file and a reverse proxy in front.
For all of these, walk through [Production hardening](/deployment/hardening) before opening the service to traffic.
## Engine-only mode
If you have your own admin stack, your own observability, and your own deployment platform, skip the standalone DB / admin REST entirely:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install idun-agent-engine
idun agent serve --source file --path config.yaml
```
No DB, no chat UI, no admin REST. The engine reads its YAML config at boot and serves `/agent/run`. Use this for CI/CD pipelines, headless integrations, or thin runtime workers that sit behind a control plane you already operate.
## Next steps
The shortest managed path with HTTPS, autoscaling, and Cloud SQL Postgres.
Lock down admin auth, TLS, secrets, and trace retention before exposing the service.
Every flag and env var for `idun serve`, `idun setup`, and engine-only mode.
# Audit logs
Source: https://docs.idun-group.com/enterprise/audit-logs
Tamper-evident record of every admin write, who performed it, and when, across the agent fleet.
The standalone records its own trace events. Enterprise adds a separate, append-only audit log of administrative actions across the fleet: who logged in, who edited which guardrail, who rotated which secret. The log is tamper-evident and exportable for compliance review.
## What gets logged
* **Authentication events**: sign-in, sign-out, failed attempts, IdP-side revocations.
* **Configuration writes**: every agent, guardrail, MCP server, observability provider, integration, and prompt mutation, with diff and actor.
* **Role and permission changes**: every grant, revoke, role definition edit.
* **Secret access**: API keys read or rotated through the admin surface.
* **Data export**: traces, logs, or analytics exported off the platform.
## Capabilities
* **Append-only storage** with cryptographic chaining so retroactive edits are detectable.
* **Retention policies** per record type, defaulting to seven years on the audit stream.
* **Export to SIEM**: stream to Splunk, Datadog, or any HTTP/Webhook consumer.
## Next steps
Authenticated identity is the actor field for every log entry.
See where audit events come from across the fleet.
# Multi-agent management
Source: https://docs.idun-group.com/enterprise/multi-agent
Register, monitor, and update multiple Idun Engine standalones from a single control plane.
Each Idun Engine standalone runs one agent in one process. Enterprise lets you manage a fleet of those standalones together: register them, see their health, push configuration, and route traffic without SSH-ing into each box.
## Capabilities
* **Fleet registry**: every standalone reports its identity, version, configured agent, and current health to the control plane on boot.
* **Centralized configuration**: push agent, guardrail, MCP, observability, and prompt updates to one or many standalones from a single UI.
* **Routing**: map an incoming request to the right standalone based on agent ID, tenant, or custom selectors.
* **Drift detection**: surface standalones whose live config no longer matches what the control plane expects.
## Next steps
Wire Okta, Entra ID, or any SAML provider into every agent.
Control who can see and edit which agents.
# Enterprise
Source: https://docs.idun-group.com/enterprise/overview
Run hundreds of agents on your own infrastructure with SSO, RBAC, audit logs, governance, and full data sovereignty.
Your teams are already building agents. Some on ChatGPT plus, some on Claude desktop, some on hand-rolled scripts. Idun Engine Enterprise pulls all of it into one governed, observable, on-prem control plane, then gives you the SSO, RBAC, audit, and policy machinery your security team is going to ask for anyway.
## Why Enterprise
Bring the agents your teams already built on ChatGPT, Claude, and consumer tools into one governed surface, before the audit asks where the data went.
On-prem, your VPC, or air-gapped. Your data never leaves your perimeter. No third-party LLM tenant, no shared inference logs.
Every admin action is captured with actor, timestamp, and diff. Append-only logs, configurable retention up to seven years, SIEM export.
Open standards under the hood: LangGraph, ADK, OpenTelemetry, MCP, OIDC. Swap providers any time. Your code stays yours.
## Governance, end to end
Register, monitor, and update every standalone agent from a single control plane. Drift detection across the fleet.
Okta, Microsoft Entra ID, Ping, SAML, and OIDC providers wired into every agent. Group-based allowlists with JIT provisioning.
Granular permissions across agents, configs, secrets, and the admin surface. Roles can be sourced from IdP groups.
Tamper-evident, append-only record of every admin write. Streams to your SIEM. Retention up to seven years.
Push org-wide guardrails, secret rotation policies, model allowlists, and approval workflows from one place to every agent.
Kubernetes, VMs, or air-gapped clusters. Your infrastructure, your rules. Helm charts and signed images.
## Built on the same open core
The Enterprise control plane wraps the same `idun-agent-engine` your developers already use. Same YAML, same APIs, same components. No fork, no migration. If you ever leave, your agents keep running.
## Who this is for
Teams running more than three agents in production. Teams with a CISO who has asked about agent governance. Teams whose data cannot leave the perimeter, or who need an audit trail for every administrative change.
## Talk to us
30 minutes. We walk through your governance requirements and show what the control plane looks like with your existing standalones plugged in.
# RBAC
Source: https://docs.idun-group.com/enterprise/rbac
Role-based access control across agents, configurations, and the admin surface.
The single-standalone admin panel uses a single password or open mode. Enterprise replaces that with role-based access control: distinct roles, granular permissions, and visibility scoped to the agents a user should see.
## Capabilities
* **Built-in roles**: Owner, Admin, Operator, Auditor, Viewer. Operators can edit live config; Auditors can read traces and logs but not change anything.
* **Custom roles**: compose permissions across agent edits, guardrail config, MCP registry, observability providers, integrations, prompts, traces, and audit logs.
* **Per-agent scope**: a role can be granted on the whole fleet, on a tenant, or on a specific agent.
* **IdP-driven**: roles can be derived from SSO group membership so HR systems drive access.
## Next steps
Source role assignments from your identity provider.
Verify what users with each role actually did.
# Enterprise SSO
Source: https://docs.idun-group.com/enterprise/sso
Wire Okta, Microsoft Entra ID, or any SAML / OIDC provider into every agent under one control plane.
Per-agent OIDC SSO (see [Authentication > SSO](/auth/sso)) is enough for a single standalone. Enterprise extends it across a fleet: one identity provider, one allowlist policy, applied to every agent the control plane manages.
## Capabilities
* **One IdP, many agents**: configure Okta, Microsoft Entra ID, or any compliant SAML / OIDC provider once in the control plane; every registered standalone enforces it.
* **Group-based allowlists**: scope agent access to IdP groups (not just emails or domains) so onboarding and offboarding flow through your existing identity workflow.
* **Just-in-time provisioning**: first sign-in creates the user record automatically; revocation in the IdP cascades to all agents on the next token refresh.
## Next steps
Once users authenticate, control what they can do.
Track who signed in and what they changed.
# FAQ
Source: https://docs.idun-group.com/faq
Frequently asked questions about Idun Engine, covering frameworks, authentication, data storage, guardrails, and licensing.
Idun Engine supports two agent frameworks:
* **LangGraph** (primary): Full support for graph definitions, checkpointing, memory, and streaming
* **Google ADK** (Agent Development Kit): Support for agent definitions, session services, and memory services
The framework is specified in the `agent.type` config field. Each framework has its own adapter in the engine that handles initialization, execution, and streaming. See [Frameworks](/frameworks/overview) for details.
There are two distinct auth surfaces.
**Standalone admin panel.** Two modes via `IDUN_ADMIN_AUTH_MODE`:
* `none`: open admin (laptop default).
* `password`: bcrypt password + signed session cookie with sliding renewal.
Use `idun hash-password` to generate the bcrypt hash, set it as `IDUN_ADMIN_PASSWORD_HASH` on first boot, and set `IDUN_SESSION_SECRET` (min 32 chars) when running in `password` mode.
**Agent runtime routes.** The engine can enforce OIDC JWT validation on `/agent/*` via a per-agent SSO config. Clients must present a valid bearer token; tokens are not stored. Some operational endpoints stay outside per-agent auth, so deploy inside a trusted network boundary and follow the [hardening guidance](/deployment/hardening).
See [Authentication](/auth/overview) for setup instructions.
The default path (standalone) uses a database. The engine-only mode does not.
**Standalone (default).** The standalone holds admin state and AG-UI trace events in a database. SQLite by default (a file on disk). Postgres optional via `DATABASE_URL`. Without a DB the standalone has nowhere to persist admin edits, traces, or session cookies.
**Engine-only.** No database required. The engine reads its YAML config at boot and serves the agent. Conversation state is whatever you configure for the LangGraph checkpointer (in-memory, SQLite file, or Postgres) or the ADK session service. With `in_memory`, nothing is persisted.
* **Agent code**: Lives in your repository. The engine wraps your code at runtime; it does not copy or store it.
* **Standalone**: Admin state (agent config, prompts, guardrails, MCP servers, observability, integrations, theme) and trace events live in the local database (SQLite by default, Postgres optional).
* **Engine-only**: Configuration comes from a YAML file. Conversation state goes to whichever checkpointer or session service you configure.
Yes. The engine ships an engine-only mode that skips the admin REST surface and the local DB entirely:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install idun-agent-engine
idun agent serve --source file --path config.yaml
```
No DB, no chat UI, no admin REST. Good for CI/CD pipelines or when you have your own admin stack and only need the runtime layer. The full standalone product is what `idun serve` (or `idun init`) gives you, and it does require a DB.
Guardrails validate agent inputs and outputs against configurable rules. You define guardrails as input guards, output guards, or both:
* **Input guardrails** check user messages before they reach the agent (e.g., jailbreak detection, PII detection)
* **Output guardrails** check agent responses before they are returned to the user (e.g., toxic language filtering, topic restriction)
Each guardrail has a type, a reject message, and type-specific configuration (threshold, word list, topic list). The engine evaluates guardrails in order and returns the reject message if a guard triggers.
The platform supports 15 guardrail types, including `BAN_LIST`, `DETECT_PII`, `TOXIC_LANGUAGE`, `DETECT_JAILBREAK`, `PROMPT_INJECTION`, `RESTRICT_TO_TOPIC`, `MODEL_ARMOR` (Google Cloud), and `CUSTOM_LLM`. See the [guardrails reference](/guardrails/reference) for the full list.
| | Standalone (default) | Engine-only |
| ----------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
| **Role** | Single-process product: engine plus chat UI, admin REST, traces | Runtime SDK that wraps your agent code into a production API |
| **Run command** | `idun serve` (or `idun init` first time) | `idun agent serve --source file --path config.yaml` |
| **Requires** | Python 3.12+, your agent code, a local file for SQLite | Python 3.12+, your agent code |
| **Has a UI?** | Yes (chat, admin, traces, bundled) | No |
| **Has a DB?** | Yes (SQLite by default; Postgres optional) | No |
| **Config source** | DB-backed (seeded from YAML on first boot) | YAML file |
| **Auth** | Admin: `none` or `password`. Agent routes: per-agent OIDC | Per-agent OIDC for `/agent/*`; rest open |
Both modes ship in the same `idun-agent-engine` wheel. Pick by which command you run. See the [glossary](/glossary) for the package layout.
Yes. Idun Engine is open source and available on [GitHub](https://github.com/Idun-Group/idun-agent-platform) under the **GNU General Public License v3.0** (GPLv3). The engine SDK (`idun-agent-engine`) and schema library (`idun-agent-schema`) are published to PyPI.
The short answer: **your agent code is generally treated as a separate work**, not a derivative of Idun. The longer answer depends on how the parts interact. This section is informational, not legal advice; review the [LICENSE](https://github.com/Idun-Group/idun-agent-platform/blob/main/LICENSE) and the [FSF's GPL FAQ](https://www.gnu.org/licenses/gpl-faq.html) before redistributing.
The engine loads your LangGraph or ADK agent from `agent.config.graph_definition` and runs it in the same Python process. The FSF considers in-process Python imports across a clean API boundary to be a closer relationship than mere aggregation, so a strictly conservative reading would treat your agent code as a derivative work subject to GPL.
In practice, most teams treat their agent code as a separate work because:
* The interaction is through a documented public extension point (`BaseAgent`, schema config fields), not internal engine APIs.
* Your agent code can be loaded into other runtimes without modification.
* You ship your agent as code that the user combines with the engine, not as a bundled binary.
The boundary is not crystal-clear. If you plan to **redistribute** your agent code bundled with `idun-agent-engine` (a single wheel, container image, or installer) and your agent code is not GPL-compatible, get legal advice before shipping.
Running the engine privately (internally, on your own infrastructure, with your own users) imposes no GPL distribution obligations.
MCP servers run as **separate processes** and communicate with the engine over stdio, SSE, streamable HTTP, or WebSocket. The FSF's [mere-aggregation guidance](https://www.gnu.org/licenses/gpl-faq.html#MereAggregation) explicitly treats inter-process communication as a non-derivative boundary.
Your MCP servers, your tool implementations, and any external services the agent calls are not derivative works of Idun.
Code generated by an LLM does not inherit a license from the LLM provider. The license applicable to LLM-generated code is whatever your provider's terms of service say (most major providers grant you rights to the output, subject to acceptable-use policies).
If you paste LLM-generated code into your `graph_definition`, the GPL question is the same as for any other agent code (see the first tab). The LLM provenance does not change the analysis.
If you need a non-GPL license for commercial redistribution, contact us via [Discord](https://discord.gg/KCZ6nW2jQe) to discuss options.
* **Bugs and feature requests**: [GitHub Issues](https://github.com/Idun-Group/idun-agent-platform/issues)
* **Questions and discussions**: [Discord](https://discord.gg/KCZ6nW2jQe)
The engine uses OpenTelemetry-based auto-instrumentation to capture traces, logs, and metrics from your agents. You configure an observability provider (Langfuse, Arize Phoenix, LangSmith, GCP Trace, or GCP Logging) and the engine instruments the agent runtime automatically.
Observability configurations are saved at the workspace level and can be reused across multiple agents. See [Observability](/observability/overview) for provider setup.
Yes. You can attach any number of MCP servers to a single agent. The engine discovers tools from all attached MCP servers at startup and makes them available to the agent. Each server can use a different transport type (stdio, SSE, streamable HTTP, WebSocket). See [MCP Servers](/mcp-servers/overview) for details.
# Google ADK
Source: https://docs.idun-group.com/frameworks/adk
Connect a Google ADK agent to Idun Engine with Gemini-powered workflows, session services, and memory backends.
Google ADK (Agent Development Kit) is Google's framework for building Gemini-powered agents. Idun Engine wraps ADK agents as production services with AG-UI streaming, guardrails, and observability.
Want to start from working code? The [agent templates](/templates) include ADK examples for tool calling and structured I/O.
## Code
```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from google.adk.agents import Agent
root_agent = Agent(
model="gemini-2.5-flash",
name="weather_agent",
description="An agent that answers questions about the weather.",
instruction="You are a helpful weather assistant. Answer questions about weather conditions.",
)
```
If you use Vertex AI models, authenticate with `gcloud auth application-default login` before running the agent.
## Config
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
server:
api:
port: 8000
agent:
type: ADK
config:
name: "my-adk-agent"
agent: "./agent.py:root_agent"
app_name: "myagent"
```
## How it works
The ADK adapter wraps your raw `Agent` instance for production serving. Five stages, driven by `agent.config` in `config.yaml`:
1. **Load.** `agent` is parsed as `:`. The adapter resolves it via `importlib.util.spec_from_file_location`, so the value must point at a real `.py` file (relative or absolute). Unlike LangGraph, module-dotted notation (`my_pkg.agent:root_agent`) is rejected here.
2. **Wrap.** The loaded agent goes into `google.adk.apps.App(root_agent=agent, name=app_name)`, then into `ADKAGUIAgent` from `ag_ui_adk` for AG-UI event streaming.
3. **Sessions.** A `session_service` is initialized from `config.yaml` (`in_memory` by default, or `database`, or `vertex_ai`) and passed into the runner. Session state, including the AG-UI `thread_id` mapping, is kept in this service.
4. **Memory.** A `memory_service` is initialized in parallel (`in_memory` by default, or `vertex_ai`) for long-term recall.
5. **Observability.** When a Langfuse provider is enabled in `observability:`, the adapter installs `GoogleADKInstrumentor` from `openinference.instrumentation.google_adk` automatically. When LangSmith is enabled, it calls `langsmith.integrations.google_adk.configure_google_adk(name=...)`. No extra wiring needed in your agent code.
6. **Serve.** Chat requests POST to `/agent/run` with the AG-UI streaming protocol, same as LangGraph.
Source: [`libs/idun_agent_engine/src/idun_agent_engine/agent/adk/adk.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/agent/adk/adk.py).
## Adding MCP tools
To wire MCP servers registered in `config.yaml` (or the admin panel) into the agent, pull them in with `get_adk_tools()` and pass them to the `Agent` constructor:
```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from google.adk.agents import Agent
from idun_agent_engine.mcp import get_adk_tools
root_agent = Agent(
model="gemini-2.5-flash",
name="weather_agent",
description="An agent that answers questions about the weather.",
instruction="You are a helpful weather assistant. Answer questions about weather conditions.",
tools=get_adk_tools(),
)
```
`get_adk_tools()` runs at engine boot, after the MCP registry has connected to every server in `mcp_servers`, so every advertised tool is available to the agent without per-tool wiring. See [MCP Servers](/mcp-servers/overview) for the full transport reference.
## Session and memory services
ADK agents have two persistence layers: session services (conversation state) and memory services (long-term recall). The scaffolded `config.yaml` uses in-memory for both by default. See [Memory and sessions for ADK](/memory/adk) for backend options including Database (PostgreSQL) and Vertex AI.
ADK does not support folder paths that contain spaces. Make sure your project directory path has no spaces.
## Next steps
Configure session and memory services across backends.
Add safety guards to your agent inputs and outputs.
Trace runs, monitor latency, and inspect token usage.
Connect external tools through the Model Context Protocol.
# Writing a custom framework adapter
Source: https://docs.idun-group.com/frameworks/custom-adapter
Extend the engine to support a new agent framework by subclassing BaseAgent. Covers the abstract surface, lifecycle, capability discovery, history hooks, and the (currently hardcoded) factory registry.
The engine ships adapters for LangGraph and Google ADK. If you run a different framework (CrewAI, LlamaIndex, AutoGen, or your own runtime), you can wrap it as an Idun adapter by subclassing `BaseAgent`. This page covers the abstract surface, the lifecycle the engine expects, and the limits of the extension point today.
The adapter factory in `idun_agent_engine.core.config_builder.ConfigBuilder.initialize_agent_from_config` is currently a hardcoded `if/elif` over the `AgentFramework` enum. Wiring a new adapter therefore requires a small upstream change: either a fork or a PR. There is no Python entry-point hook today. This is on the roadmap; please open an issue if you need a stable plugin API.
## The base class
`BaseAgent` lives in [`libs/idun_agent_engine/src/idun_agent_engine/agent/base.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/agent/base.py). It is an `ABC` with the following surface:
### Required overrides
| Member | Kind | What you return |
| ---------------------------------------- | --------------- | ---------------------------------------------------------------------------- |
| `id` | property | A stable string identifier for this adapter instance |
| `agent_type` | property | A short label (e.g. `"LangGraph"`, `"ADK"`, `"CrewAI"`) |
| `agent_instance` | property | The underlying framework's compiled agent object |
| `copilotkit_agent_instance` | property | A CopilotKit-compatible wrapper, or the same instance if not applicable |
| `infos` | property | A diagnostic dict surfaced via `/_engine/info` |
| `initialize(config, observability=None)` | async method | Parse config, instantiate the framework agent, wire observability |
| `invoke(message)` | async method | One-shot non-streaming run (used by the deprecated `/agent/invoke` route) |
| `stream(message)` | async generator | Pre-AG-UI stream (used by the deprecated `/agent/stream` route) |
| `run(input_data)` | async generator | **Canonical AG-UI entry point.** Accepts `RunAgentInput`, yields `BaseEvent` |
| `discover_capabilities()` | method | Return `AgentCapabilities` describing input/output schemas |
`run` is the only path the modern `POST /agent/run` route uses. `invoke` and `stream` exist for the deprecated `/agent/invoke` and `/agent/stream` compatibility shims. New adapters should focus on `run`; `invoke` and `stream` can raise `NotImplementedError` if you do not need the deprecated routes.
### Optional overrides
| Member | Default behaviour |
| --------------------------------------- | -------------------------------------------------------------------------------------------- |
| `history_capabilities()` | Returns `HistoryCapabilities(can_list=False, can_get=False)` |
| `list_sessions(user_id=None)` | Raises `NotImplementedError`. Override to expose session history. |
| `get_session(session_id, user_id=None)` | Raises `NotImplementedError`. Override to fetch a single session. |
| `get_graph_ir()` | Raises `NotImplementedError`. Override to enable the admin panel's live graph visualisation. |
| `draw_mermaid()` | Renders `get_graph_ir()` as Mermaid. Override only if you have a faster path. |
| `draw_ascii()` | Renders `get_graph_ir()` as ASCII. Same. |
### Constructor
Your `__init__` must call `super().__init__()` to populate `self.run_event_observers: RunEventObserverRegistry`. Run-event observers are how the standalone trace pipeline subscribes to AG-UI events for the local trace store.
## Minimal adapter sketch
```python my_adapter.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from __future__ import annotations
import uuid
from typing import Any, AsyncGenerator
from ag_ui.core import BaseEvent
from ag_ui.core.types import RunAgentInput
from idun_agent_engine.agent.base import BaseAgent
from idun_agent_schema.engine.agent_framework import AgentFramework
from idun_agent_schema.engine.capabilities import (
AgentCapabilities,
CapabilityFlags,
InputDescriptor,
OutputDescriptor,
)
from idun_agent_schema.engine.sessions import HistoryCapabilities
class MyAdapter(BaseAgent):
def __init__(self) -> None:
super().__init__() # critical: wires run_event_observers
self._id = str(uuid.uuid4())
self._instance: Any = None
@property
def id(self) -> str:
return self._id
@property
def agent_type(self) -> str:
return "MyFramework"
@property
def agent_instance(self) -> Any:
if self._instance is None:
raise RuntimeError("Agent not initialized")
return self._instance
@property
def copilotkit_agent_instance(self) -> Any:
# If your framework has no CopilotKit wrapper, return the same object.
# Browser clients calling /agent/run still work via the engine's
# AG-UI translation layer.
return self.agent_instance
@property
def infos(self) -> dict[str, Any]:
return {"framework": "MyFramework", "version": "0.1.0"}
async def initialize(
self,
config: dict,
observability: list | None = None,
) -> None:
# 1. Validate config against your Pydantic model
# 2. Instantiate your framework's agent
# 3. Wire observability callbacks if your framework supports them
from my_framework import build_agent
self._instance = build_agent(config)
async def invoke(self, message: Any) -> Any:
# Deprecated /agent/invoke path. Optional if you do not need
# the legacy shim; raise NotImplementedError to skip.
return await self._instance.run(message)
async def stream(self, message: Any) -> AsyncGenerator[BaseEvent, None]:
# Deprecated /agent/stream path. Optional; raise NotImplementedError
# to skip the legacy shim.
if False: # pragma: no cover
yield # type: ignore[misc]
raise NotImplementedError("stream() not implemented; use run()")
async def run(
self, input_data: RunAgentInput
) -> AsyncGenerator[BaseEvent, None]:
# Canonical AG-UI entry point. The /agent/run route iterates this
# generator and forwards each event to the SSE response. Translate
# your framework's native events into ag_ui.core event types here.
async for event in self._instance.run_ag_ui(input_data):
yield event
def discover_capabilities(self) -> AgentCapabilities:
return AgentCapabilities(
framework=AgentFramework.CUSTOM,
capabilities=CapabilityFlags(streaming=True, history=False, thread_id=True),
input=InputDescriptor(mode="chat"),
output=OutputDescriptor(mode="text"),
)
def history_capabilities(self) -> HistoryCapabilities:
return HistoryCapabilities(can_list=False, can_get=False)
```
## Lifecycle
1. **Boot.** The engine reads `config.yaml`, validates the `agent.type` field against the `AgentFramework` enum, and instantiates your adapter class.
2. **`initialize(config, observability)`.** Called once. Use this to parse `agent.config`, instantiate your framework, and register callbacks. Raise an exception to abort boot.
3. **`discover_capabilities()`.** Called by the engine to populate `/agent/capabilities`. The standalone admin UI uses the result to choose between the chat input and the structured-JSON input.
4. **`run_event_observers` registration.** The engine subscribes its trace pipeline before serving the first request. Your `run()` method does not need to know about observers: `BaseAgent` handles dispatch when AG-UI events are yielded.
5. **`run(input_data)`.** Called per request to `POST /agent/run`. The HTTP layer iterates the generator and forwards each event over Server-Sent Events. The deprecated `/agent/invoke` and `/agent/stream` routes delegate to `invoke()` and `stream()` respectively.
## Capability discovery
`AgentCapabilities` describes what your adapter accepts and returns. The standalone UI uses this to render the right input control.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_schema.engine.agent_framework import AgentFramework
from idun_agent_schema.engine.capabilities import (
AgentCapabilities,
CapabilityFlags,
InputDescriptor,
OutputDescriptor,
)
# Chat-mode agent (LangGraph default)
caps = AgentCapabilities(
framework=AgentFramework.LANGGRAPH,
capabilities=CapabilityFlags(streaming=True, history=True, thread_id=True),
input=InputDescriptor(mode="chat"),
output=OutputDescriptor(mode="text"),
)
# Structured-input agent
caps = AgentCapabilities(
framework=AgentFramework.LANGGRAPH,
capabilities=CapabilityFlags(streaming=True, history=False, thread_id=False),
input=InputDescriptor(mode="structured", schema={"type": "object", "properties": {...}}),
output=OutputDescriptor(mode="structured", schema={"type": "object", "properties": {...}}),
)
```
`InputDescriptor.mode` is `Literal["chat", "structured"]`. `OutputDescriptor.mode` is `Literal["text", "structured", "unknown"]`. When `input.mode == "structured"`, the standalone chat surface validates that `messages[-1].content` is JSON matching the schema before forwarding to your adapter.
## Wiring the adapter into the engine
Until a plugin API ships, you need to register your adapter in two places.
### 1. Use or add an enum value
The `AgentFramework` enum in `idun_agent_schema/engine/agent_framework.py` already ships a `CUSTOM` slot that you can claim without forking the schema. If you need a named slot for upstream contribution, add one:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class AgentFramework(str, Enum):
LANGGRAPH = "langgraph"
ADK = "adk"
CUSTOM = "custom"
MY_FRAMEWORK = "my_framework" # if upstreaming
```
### 2. Add a branch in `ConfigBuilder.initialize_agent_from_config`
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
elif agent_type == AgentFramework.MY_FRAMEWORK:
agent = MyAdapter()
await agent.initialize(
engine_config.agent.config.model_dump(mode="json"),
observability=engine_config.observability or [],
)
return agent
```
Then in your `config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "MY_FRAMEWORK"
config:
# your framework-specific fields
```
## Reference adapter: LangGraph
The LangGraph adapter at [`libs/idun_agent_engine/src/idun_agent_engine/agent/langgraph/langgraph.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/agent/langgraph/langgraph.py) is the most complete worked example. Reading it end-to-end is the fastest way to see how:
* `_load_graph_builder` resolves the `graph_definition` string (file path first, module fallback)
* `_setup_persistence` configures `InMemorySaver`, `AsyncSqliteSaver`, or `AsyncPostgresSaver` based on the checkpointer config
* `discover_capabilities()` reads `graph.input_schema` and `graph.output_schema` to detect chat vs structured mode
* `run()` delegates to LangGraph's AG-UI wrapper (`LangGraphAGUIAgent`) which emits `RunStarted`, `StepStarted`, `TextMessageStart` / `Content` / `End`, `ToolCallStart` / `Args` / `End`, `ThinkingStart` / `End`, and `RunFinished` events
* `get_graph_ir()` introspects the compiled graph and emits a framework-agnostic `AgentGraph` for the admin panel
## What's next
The same extension pattern for tracing providers.
JWT validation works for any adapter.
Common boot failures.
# LangGraph
Source: https://docs.idun-group.com/frameworks/langgraph
Connect a LangGraph agent to Idun Engine with graph-based workflows, AG-UI streaming, and persistent checkpointing.
LangGraph is the primary framework integration in Idun Engine. It supports full AG-UI streaming, CopilotKit, and persistent checkpointing through in-memory, SQLite, or PostgreSQL backends.
Want to start from working code? The [agent templates](/templates) include 7 LangGraph examples covering tool calling, structured I/O, and multi-step workflows.
## Code
```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from typing import Annotated, TypedDict
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
def chatbot(state: State):
return {"messages": [llm.invoke(state["messages"])]}
graph = StateGraph(State)
graph.add_node("chatbot", chatbot)
graph.add_edge(START, "chatbot")
graph.add_edge("chatbot", END)
```
## Config
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
server:
api:
port: 8000
agent:
type: LANGGRAPH
config:
name: "my-langgraph-agent"
graph_definition: "./agent.py:graph"
checkpointer:
type: sqlite
db_url: "sqlite:///conversations.db"
```
## How it works
The LangGraph adapter wraps your `StateGraph` for production serving. Four stages, all driven by `agent.config` in `config.yaml`:
1. **Load.** `graph_definition` is parsed as `:`. The adapter resolves it as a file path first (relative or absolute), then falls back to a Python module import path (`my_pkg.agent:graph`). Both work.
2. **Validate.** The exported variable must be a `StateGraph`. A `CompiledStateGraph` is accepted: the engine extracts `.builder` and recompiles with the engine-managed checkpointer and store, preserving any `interrupt_before` and `interrupt_after` you supplied to `.compile()`. A warning is logged when this path is taken.
3. **Compile.** The engine compiles the `StateGraph` with the checkpointer configured in `config.yaml` (`memory`, `sqlite`, or `postgres`) and wraps the result in `LangGraphAGUIAgent` from CopilotKit for AG-UI event streaming.
4. **Serve.** Chat requests POST to `/agent/run` with the AG-UI streaming protocol. The engine emits typed events (`RUN_STARTED`, `TEXT_MESSAGE_CONTENT`, `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `STEP_STARTED`, `STEP_FINISHED`, `RUN_FINISHED`) as SSE `data:` lines.
The capabilities endpoint introspects the compiled graph to detect chat vs structured input/output modes. A `StateGraph(state_schema)` with a `messages` field is treated as chat; an explicit `StateGraph(state, input_schema=..., output_schema=...)` declares a structured contract.
Source: [`libs/idun_agent_engine/src/idun_agent_engine/agent/langgraph/langgraph.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/agent/langgraph/langgraph.py).
## Notes
**Export the uncompiled `StateGraph`.** If you export a `CompiledStateGraph` (the result of `.compile()`), the engine extracts the original `StateGraph` via `.builder` and recompiles with the engine-managed checkpointer and store. Compile options like `interrupt_before` and `interrupt_after` are preserved. A warning is logged when this happens.
**Import type annotations directly.** The engine introspects annotations when loading the graph. `from __future__ import annotations` produces deferred string annotations (PEP 563), and the resolver raises `NameError` on names like `Annotated` at graph-build time.
**Always include a `checkpointer:` block.** LangGraph requires one at request time; requests fail with `No checkpointer set` when it is missing. Use `type: memory` for ephemeral state, `type: sqlite` or `type: postgres` for persistence. See [Memory and checkpointing for LangGraph](/memory/langgraph) for backend options.
## Next steps
Backend options and configuration for persistent state.
Add safety guards to your agent inputs and outputs.
Trace runs, monitor latency, and inspect token usage.
Connect external tools through the Model Context Protocol.
# Agent frameworks
Source: https://docs.idun-group.com/frameworks/overview
Connect LangGraph or Google ADK agents to Idun Engine and run them as production-ready services.
Idun Engine wraps your agent code into a FastAPI service through framework-specific adapters. You write your agent in the framework you prefer, and the engine handles streaming, checkpointing, guardrails, and observability through a unified API.
Each adapter implements a common `BaseAgent` protocol. Your clients talk to the same HTTP API regardless of the underlying framework.
## Supported frameworks
Graph-based agents with full AG-UI streaming, checkpointing (in-memory, SQLite, PostgreSQL), and CopilotKit support. Has the most complete feature coverage of the supported frameworks.
Google's Agent Development Kit for Gemini-powered agents. Supports session and memory services through in-memory, Vertex AI, or database backends.
LangChain's framework for agents that plan, write to a virtual filesystem, and spawn subagents. Runs as a regular LangGraph agent in Idun (no separate adapter, no glue code).
### Documentation and source code
| Framework | Docs | Repo |
| ----------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| LangGraph | [docs.langchain.com/oss/python/langgraph](https://docs.langchain.com/oss/python/langgraph) | [github.com/langchain-ai/langgraph](https://github.com/langchain-ai/langgraph) |
| Google ADK | [google.github.io/adk-docs](https://google.github.io/adk-docs/) | [github.com/google/adk-python](https://github.com/google/adk-python) |
| Deep Agents | [docs.langchain.com/oss/python/deepagents](https://docs.langchain.com/oss/python/deepagents/overview) | [github.com/langchain-ai/deepagents](https://github.com/langchain-ai/deepagents) |
Haystack support was removed in 0.6.0. Existing Haystack agent configs need to be ported to LangGraph or ADK before upgrading.
## Feature comparison
| Feature | LangGraph | Google ADK |
| ---------------------- | ----------------------------- | ------------------------------ |
| AG-UI streaming | Yes | Yes |
| Checkpointing / memory | In-memory, SQLite, PostgreSQL | In-memory, Vertex AI, Database |
| CopilotKit support | Yes | No |
| Guardrails | Yes | Yes |
| Observability | Yes | Yes |
| MCP servers | Yes | Yes |
## How adapters work
Under the hood, the engine loads your agent through the adapter specified in your configuration. This gives you:
* **Unified API**: All agents expose the same HTTP endpoints for invoke, streaming, and chat.
* **Shared platform features**: Observability, guardrails, MCP tools, and memory work the same way across frameworks.
* **Consistent deployment**: The same `idun serve` command runs any supported framework.
Select the framework by setting the `type` field in the `agent` section of your `config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: LANGGRAPH # or ADK
config:
# framework-specific fields
```
The engine validates that the `config` shape matches the selected `type` at startup. The standalone admin panel at `/admin/agent/` exposes the same field with a dropdown when you want to switch frameworks without editing YAML.
## Next steps
Graph-based agents with full AG-UI streaming, checkpointing, and CopilotKit support.
Google's Agent Development Kit for Gemini-powered agents with session and memory services.
Build your own adapter to bring a different agent framework into Idun Engine.
Full schema for `config.yaml` including agent, guardrails, MCP, observability, and more.
# Glossary
Source: https://docs.idun-group.com/glossary
Definitions of the Idun packages and where each one fits.
Idun Engine ships as a small set of packages with a clear relationship. This page defines each one and explains which install path you actually want to follow.
## Packages
### Idun Agent Engine
Package: `idun-agent-engine`. Console script: `idun`.
The Python SDK and FastAPI runtime that wraps a LangGraph or Google ADK agent into a production endpoint. You point it at a YAML config and a Python module, and it serves your agent over the AG-UI streaming protocol with optional guardrails, observability providers, MCP tool registries, and prompt loading.
The wheel bundles the standalone admin/chat/traces app and the `idun-agent-schema` runtime, so a single `pip install idun-agent-engine` is everything you need on a fresh machine. Engine-only mode (no DB, no UI, no admin REST) is available through `idun agent serve --source file --path config.yaml` if you want a thin runtime layer for your own platform.
### Idun Agent Standalone
Source: `libs/idun_agent_standalone` inside the engine wheel.
The single-process, single-tenant product that wraps the engine with an embedded chat UI, an admin REST surface, traces capture, and a local DB. One binary you can deploy on a laptop, VM, or Cloud Run.
What it adds on top of the engine:
* A bundled Next.js UI at `/` (chat), `/admin/` (config editing), and `/admin/traces/` (recent run events).
* Admin REST endpoints under `/admin/api/v1/*` for editing agent, guardrails, MCP servers, prompts, observability, integrations, and theme.
* A SQLite-backed database by default (Postgres optional) holding admin state and AG-UI trace events.
* Password authentication for the admin panel (`none` for laptops, `password` for containers), and per-agent OIDC SSO on the `/agent/*` runtime routes with email/domain allowlists. See [Authentication](/auth/overview) and [SSO](/auth/sso).
* A reload orchestrator that rebuilds the engine on every admin write and swaps it atomically.
Use this when you ship one agent end-to-end and want chat, admin, and traces in one process.
### Idun Agent Schema
Package: `idun-agent-schema`.
The Pydantic models that define the config structure consumed by the engine and standalone. Schema changes start here and propagate. Installed automatically as a transitive dependency of `idun-agent-engine`; you rarely import it directly.
## Install path
One command on any host with Python 3.12+:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install idun-agent-engine
mkdir my-agent && cd my-agent
idun init
```
`idun init` runs DB migrations and opens your browser at `http://localhost:8000/`. The onboarding wizard scaffolds `agent.py`, `config.yaml`, and `.env.example` and seeds the local DB. After that, the DB is the source of truth and admin edits persist across restarts. Visit `/` for the chat UI, `/admin/` to edit config, `/admin/traces/` to inspect AG-UI events.
If you have your own admin stack and only want the runtime layer, skip the standalone surface and use engine-only mode:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun agent serve --source file --path config.yaml
```
No DB, no UI, no admin REST. You bring your own deployment story.
## Next steps
Deploy your first agent in under 30 minutes
How the engine, standalone, and schema connect
# Guardrails
Source: https://docs.idun-group.com/guardrails/overview
Protect your agents with 15 built-in guardrails for PII detection, jailbreak prevention, toxic language filtering, topic restriction, and more.
Guardrails scan agent inputs and outputs to enforce safety and policy boundaries. Idun Engine provides 15 built-in guardrail types powered by [Guardrails AI](https://guardrailsai.com), applied at the input position, output position, or both.
## How guardrails work
Guardrails run at two positions in the agent request lifecycle:
* **Input guardrails** validate user messages before the agent processes them. If any input guardrail fails, the request is blocked immediately and the agent never sees the message.
* **Output guardrails** validate agent responses before returning them to the user. They run after agent processing completes. Output guardrails add latency to the response time.
You can configure multiple guardrails at each position. All guardrails at a given position are checked, and any single failure blocks the request or response.
## Configuration
Add guardrails in the `guardrails` section of your `config.yaml`. Each guardrail has a `config_id` that identifies the type and parameters specific to that type.
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
input:
- config_id: "ban_list"
banned_words: ["spam", "scam", "phishing"]
- config_id: "detect_pii"
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD_NUMBER"]
- config_id: "detect_jailbreak"
threshold: 0.8
output:
- config_id: "toxic_language"
threshold: 0.7
- config_id: "gibberish_text"
threshold: 0.8
```
Infrastructure fields (`api_key`, `guard_url`, `reject_message`) are populated automatically. For YAML-based configs the `api_key` is read from the `GUARDRAILS_API_KEY` environment variable. You only need to specify the `config_id` and guard-specific parameters.
Navigate to `/admin/guardrails/` in the running standalone. The catalog at the top groups guards by category; configured guards are listed below.
Click the guard type you want (e.g., Ban List, Detect PII, Toxic Language). Fill in the configuration form, including the **Guardrails AI API key** field on the first guard you create. Get a key from [hub.guardrailsai.com](https://hub.guardrailsai.com). The key is persisted in the guardrail row and re-hydrated into the process environment on every boot, so you only enter it once.
Save the form. The reload pipeline validates the new config, re-instantiates the engine, and the guard is live. A bad save rolls back without disturbing the running agent.
Some guards are marked "Soon" and not yet available: Code Scanner, Jailbreak, Prompt Injection, Model Armor, Custom LLM, and RAG Hallucination.
Guardrails need a Guardrails AI API key. Either set it once in the admin form on your first guardrail (the standalone persists and re-hydrates it on boot), or export it as `GUARDRAILS_API_KEY` in your environment. Get a key from [Guardrails AI](https://guardrailsai.com).
## Available guardrail types
All 15 guardrail types and their key parameters:
| `config_id` | Description | Key parameters |
| ------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- |
| `ban_list` | Block specific words or phrases | `banned_words` (list of strings) |
| `detect_pii` | Detect personally identifiable information (emails, phone numbers, addresses) | `pii_entities` (list of PII types) |
| `nsfw_text` | Block sexually explicit or violent content | `threshold` (0.0 to 1.0) |
| `toxic_language` | Detect toxic or offensive language | `threshold` (0.0 to 1.0) |
| `detect_jailbreak` | Identify attempts to bypass safety guidelines | `threshold` (0.0 to 1.0) |
| `prompt_injection` | Detect prompt injection attacks | `threshold` (0.0 to 1.0) |
| `competition_check` | Block mentions of competitor names or products | `competitors` (list of strings) |
| `bias_check` | Detect biased language | `threshold` (0.0 to 1.0) |
| `correct_language` | Verify text is written in expected languages | `expected_languages` (ISO codes, e.g. `["en", "fr"]`) |
| `restrict_to_topic` | Keep conversation within defined subject areas | `topics` (list of allowed topics) |
| `gibberish_text` | Filter nonsensical or incoherent output | `threshold` (0.0 to 1.0) |
| `rag_hallucination` | Detect hallucinated content in RAG responses | `threshold` (0.0 to 1.0) |
| `code_scanner` | Validate code blocks for allowed programming languages | `allowed_languages` (list of language names) |
| `model_armor` | Google Cloud Model Armor integration | `project_id`, `location`, `template_id` |
| `custom_llm` | Define custom validation rules using an LLM | `model`, `prompt` |
## Adding guardrails through config file
For first-boot seeding (or engine-only mode), add guardrails directly to your `config.yaml`:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
input:
- config_id: "ban_list"
banned_words: ["competitor-product", "internal-codename"]
- config_id: "detect_pii"
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER"]
```
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
output:
- config_id: "toxic_language"
threshold: 0.7
- config_id: "gibberish_text"
threshold: 0.8
```
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
input:
- config_id: "detect_jailbreak"
threshold: 0.8
- config_id: "prompt_injection"
threshold: 0.8
output:
- config_id: "rag_hallucination"
threshold: 0.7
```
Each guardrail entry supports an optional `reject_message` field to customize the error message returned when the guardrail triggers:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
input:
- config_id: "ban_list"
banned_words: ["blocked-term"]
reject_message: "Your message contains a restricted term."
```
## Testing guardrails
After configuring guardrails, verify they work as expected by sending test requests through the API.
```bash Test input guardrail (PII) theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST http://localhost:8008/v1/agents/{agent_id}/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {api_key}" \
-d '{"message": "My email is john.doe@example.com and phone is 555-0123"}'
```
```bash Test with safe input theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST http://localhost:8008/v1/agents/{agent_id}/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {api_key}" \
-d '{"message": "What is the weather like today?"}'
```
When a guardrail blocks a request, the response includes the `guardrail` field identifying which guard triggered and a `detail` message explaining why.
## Best practices
* **Layer multiple guardrails** at the input position for defense in depth. Combine ban lists with PII detection and jailbreak prevention.
* **Use output guardrails sparingly** since they add latency. Reserve them for critical checks like hallucination detection or gibberish filtering.
* **Set thresholds conservatively** at first (higher values = stricter), then lower them if you see too many false positives.
* **Test with realistic inputs** before production. Send messages that should trigger each guardrail and verify legitimate content passes through.
## Next steps
All 15 guardrail types and their configuration fields.
Monitor guardrail activity in traces.
Deploy your agent to Cloud Run, a VM, or your laptop.
# Guardrails reference
Source: https://docs.idun-group.com/guardrails/reference
Reference for all 15 guardrail types available in Idun Engine, including configuration fields and usage positions.
Idun Engine supports 15 guardrail types that validate agent inputs, outputs, or both. Each guardrail has a `config_id`, a reject message returned when the guard triggers, and type-specific configuration fields.
## Guardrail positions
Guardrails are placed in one of two positions:
* **Input**: Applied to user messages before they reach the agent
* **Output**: Applied to agent responses before they are returned to the user
You can place the same guardrail type in both positions.
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
guardrails:
input:
- config_id: detect_pii
reject_message: "PII detected in your input"
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD"]
on_fail: exception
output:
- config_id: toxic_language
reject_message: "Response contains inappropriate language"
threshold: 0.7
```
## Guardrail types
### BAN\_LIST
Blocks messages containing specific words or phrases.
| Field | Type | Description |
| ---------------- | -------------- | ------------------------------- |
| `config_id` | `ban_list` | |
| `reject_message` | `string` | Message returned when triggered |
| `banned_words` | `list[string]` | Words or phrases to block |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: ban_list
reject_message: "Message contains banned content"
banned_words: ["banned-word", "another phrase"]
```
### DETECT\_PII
Detects personally identifiable information in text.
| Field | Type | Description |
| ---------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `config_id` | `detect_pii` | |
| `reject_message` | `string` | Message returned when triggered |
| `pii_entities` | `list[string]` | PII entity types to detect (e.g., `EMAIL_ADDRESS`, `PHONE_NUMBER`, `CREDIT_CARD`, `SSN`) |
| `on_fail` | `string` | Action on detection. Default: `exception` |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: detect_pii
reject_message: "Personal information detected"
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD"]
on_fail: exception
```
### NSFW\_TEXT
Detects not-safe-for-work content.
| Field | Type | Description |
| ---------------- | ----------- | --------------------------------------------------------------- |
| `config_id` | `nsfw_text` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0). Lower values are more sensitive |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: nsfw_text
reject_message: "Inappropriate content detected"
threshold: 0.5
```
### COMPETITION\_CHECK
Flags mentions of competitor companies or products.
| Field | Type | Description |
| ---------------- | ------------------- | ----------------------------------------- |
| `config_id` | `competition_check` | |
| `reject_message` | `string` | Message returned when triggered |
| `competitors` | `list[string]` | Names of competitor companies or products |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: competition_check
reject_message: "Competitor reference detected"
competitors: ["CompetitorA", "CompetitorB"]
```
### BIAS\_CHECK
Detects biased language in text.
| Field | Type | Description |
| ---------------- | ------------ | ------------------------------- |
| `config_id` | `bias_check` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: bias_check
reject_message: "Biased language detected"
threshold: 0.7
```
### CORRECT\_LANGUAGE
Validates that text is in one of the expected languages.
| Field | Type | Description |
| -------------------- | ------------------ | ------------------------------------------------- |
| `config_id` | `correct_language` | |
| `reject_message` | `string` | Message returned when triggered |
| `expected_languages` | `list[string]` | Valid ISO language codes (e.g., `en`, `fr`, `es`) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: correct_language
reject_message: "Please use English or French"
expected_languages: ["en", "fr"]
```
### GIBBERISH\_TEXT
Filters nonsensical or garbled input.
| Field | Type | Description |
| ---------------- | ---------------- | ------------------------------- |
| `config_id` | `gibberish_text` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: gibberish_text
reject_message: "Input appears to be nonsensical"
threshold: 0.8
```
### TOXIC\_LANGUAGE
Detects toxic or harmful language.
| Field | Type | Description |
| ---------------- | ---------------- | ------------------------------- |
| `config_id` | `toxic_language` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: toxic_language
reject_message: "Toxic language detected"
threshold: 0.7
```
### RESTRICT\_TO\_TOPIC
Keeps conversations within a defined set of allowed topics.
| Field | Type | Description |
| ---------------- | ------------------- | ------------------------------- |
| `config_id` | `restrict_to_topic` | |
| `reject_message` | `string` | Message returned when triggered |
| `topics` | `list[string]` | List of allowed topics |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: restrict_to_topic
reject_message: "That topic is outside the scope of this agent"
topics: ["customer support", "product information", "billing"]
```
### DETECT\_JAILBREAK
Detects jailbreak attempts in user input.
| Field | Type | Description |
| ---------------- | ------------------ | ------------------------------- |
| `config_id` | `detect_jailbreak` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: detect_jailbreak
reject_message: "Jailbreak attempt detected"
threshold: 0.5
```
### PROMPT\_INJECTION
Detects prompt injection attacks.
| Field | Type | Description |
| ---------------- | ------------------ | ------------------------------- |
| `config_id` | `prompt_injection` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: prompt_injection
reject_message: "Prompt injection detected"
threshold: 0.5
```
### RAG\_HALLUCINATION
Detects hallucinations in RAG (Retrieval-Augmented Generation) responses by comparing the response against the retrieved context.
| Field | Type | Description |
| ---------------- | ------------------- | ------------------------------- |
| `config_id` | `rag_hallucination` | |
| `reject_message` | `string` | Message returned when triggered |
| `threshold` | `float` | Sensitivity level (0.0 to 1.0) |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: rag_hallucination
reject_message: "Response may contain unsupported claims"
threshold: 0.7
```
### CODE\_SCANNER
Scans and validates code in messages, restricting to allowed programming languages.
| Field | Type | Description |
| ------------------- | -------------- | ------------------------------------- |
| `config_id` | `code_scanner` | |
| `reject_message` | `string` | Message returned when triggered |
| `allowed_languages` | `list[string]` | List of allowed programming languages |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: code_scanner
reject_message: "Code in this language is not allowed"
allowed_languages: ["python", "javascript", "sql"]
```
### MODEL\_ARMOR (Google Cloud)
Uses Google Cloud's Model Armor service for content safety evaluation.
| Field | Type | Description |
| ------------- | ------------- | ----------------------------------------- |
| `config_id` | `model_armor` | |
| `name` | `string` | Name of the armor configuration |
| `project_id` | `string` | Google Cloud project ID |
| `location` | `string` | Google Cloud region (e.g., `us-central1`) |
| `template_id` | `string` | Model Armor template ID |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: model_armor
name: production-armor
project_id: my-gcp-project
location: us-central1
template_id: my-armor-template
```
Model Armor requires a Google Cloud project with the Model Armor API enabled. This guardrail type does not use the Guardrails AI hub.
### CUSTOM\_LLM
Uses a large language model as a custom guardrail with a prompt you define.
| Field | Type | Description |
| ----------- | ------------ | ---------------------------------------------------------- |
| `config_id` | `custom_llm` | |
| `name` | `string` | Name of the custom guardrail |
| `model` | `string` | LLM model to use for evaluation |
| `prompt` | `string` | System instruction prompt that defines the guardrail logic |
Supported models:
| Model ID | Name |
| ----------------------- | --------------------- |
| `Gemini 2.5 flash lite` | Gemini 2.5 Flash Lite |
| `Gemini 2.5 flash` | Gemini 2.5 Flash |
| `Gemini 2.5 pro` | Gemini 2.5 Pro |
| `Gemini 3 pro` | Gemini 3 Pro |
| `OpenAi GPT-5.1` | OpenAI GPT-5.1 |
| `OpenAi GPT-5 mini` | OpenAI GPT-5 Mini |
| `OpenAi GPT-5 nano` | OpenAI GPT-5 Nano |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- config_id: custom_llm
name: compliance-check
model: "Gemini 2.5 flash"
prompt: "Evaluate the following text for regulatory compliance violations. Return PASS if compliant, FAIL if not."
```
Custom LLM guardrails use a separate LLM call for evaluation. This adds latency and cost to each guarded request.
## Summary table
| Type | Config ID | Position | Key config fields |
| ----------------- | ------------------- | ------------ | --------------------------------------- |
| Ban list | `ban_list` | Input/Output | `banned_words` |
| Detect PII | `detect_pii` | Input/Output | `pii_entities`, `on_fail` |
| NSFW text | `nsfw_text` | Input/Output | `threshold` |
| Competition check | `competition_check` | Input/Output | `competitors` |
| Bias check | `bias_check` | Input/Output | `threshold` |
| Correct language | `correct_language` | Input/Output | `expected_languages` |
| Gibberish text | `gibberish_text` | Input/Output | `threshold` |
| Toxic language | `toxic_language` | Input/Output | `threshold` |
| Restrict to topic | `restrict_to_topic` | Input/Output | `topics` |
| Detect jailbreak | `detect_jailbreak` | Input | `threshold` |
| Prompt injection | `prompt_injection` | Input | `threshold` |
| RAG hallucination | `rag_hallucination` | Output | `threshold` |
| Code scanner | `code_scanner` | Input/Output | `allowed_languages` |
| Model Armor | `model_armor` | Input/Output | `project_id`, `location`, `template_id` |
| Custom LLM | `custom_llm` | Input/Output | `model`, `prompt` |
## Next steps
How guardrails fit into the agent request lifecycle.
Trace guardrail decisions alongside agent runs.
Diagnose configuration and provider errors.
# Connect your existing agent
Source: https://docs.idun-group.com/guides/connect-your-agent
Wrap an existing LangGraph or ADK agent in an Idun engine service. Write a minimal config.yaml that points at your StateGraph, install idun-agent-engine, and run.
You already have an agent. Maybe it's a LangGraph `StateGraph` in `my_agent.py`, or an ADK `Agent` in a Python package. This guide shows the smallest possible step from "I have an agent" to "I have an HTTP service with a chat UI, traces, and admin panel."
## Prerequisites
* Python 3.12 or newer
* Your agent code, with a top-level variable that holds the graph (LangGraph) or agent (ADK)
* `pip install idun-agent-engine` (this ships the `idun` CLI and the bundled standalone admin UI)
## Example: a LangGraph echo agent
Assume you have `my_agent.py` in the current directory:
```python my_agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
messages: list
def echo(state: State) -> dict:
last = state["messages"][-1]
text = last.content if hasattr(last, "content") else str(last)
return {"messages": state["messages"] + [{"role": "assistant", "content": text}]}
builder = StateGraph(State)
builder.add_node("echo", echo)
builder.add_edge(START, "echo")
builder.add_edge("echo", END)
graph = builder # <-- this is what Idun will load
```
Assign an uncompiled `StateGraph` to the top-level variable. The engine compiles it with its own checkpointer and store. A `CompiledStateGraph` is also accepted (the engine extracts its `.builder` and recompiles), but you'll see a deprecation warning.
## Step 1: Write a minimal config.yaml
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
server:
api:
port: 8000
agent:
type: "LANGGRAPH"
config:
name: "My agent"
graph_definition: "./my_agent.py:graph"
```
Three fields do all the work:
* `agent.type`: `"LANGGRAPH"` or `"ADK"` (case-insensitive). Determines which adapter wraps your code.
* `agent.config.name`: display name shown in the admin panel.
* `agent.config.graph_definition`: `":"` for LangGraph. For ADK, the equivalent field is `agent.config.agent`.
### `graph_definition` format
The string is parsed as `:`. The engine tries file-path resolution first, then falls back to Python module import:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# File path (relative or absolute)
graph_definition: "./my_agent.py:graph"
graph_definition: "src/agents/router.py:app"
# Python module path (when your package is importable)
graph_definition: "my_package.agents.router:graph"
```
Common variable names: `graph`, `app`, `agent`. Anything that resolves to a `StateGraph` works.
## Step 2: Run
For evaluation and local development, use the standalone launcher:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun init
```
`idun init` runs Alembic migrations, seeds the DB from your `config.yaml`, opens your browser, and serves on `http://localhost:8000`.
### Skipping Step 1: let the wizard find your agent
If you'd rather not hand-write `config.yaml`, `idun init` ships with an onboarding scanner that walks the current folder and offers any LangGraph or Google ADK agents it finds. Drop into an existing repo, run `idun init`, and the wizard surfaces the detected agents so you can pick one without touching YAML.
The scanner (`libs/idun_agent_standalone/src/idun_agent_standalone/services/scanner.py`) looks at three sources, in order of confidence:
| Source | Confidence | What it parses |
| --------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------- |
| `config.yaml` or `config.yml` at depth 0 to 2 | HIGH | Reads `agent.type` and either `graph_definition` (LangGraph) or `agent` (ADK). |
| `langgraph.json` at the root | HIGH | One detection per entry in the `graphs` dict. |
| `.py` source files, depth up to 4 | MEDIUM | Regex pre-filter for `langgraph`, `deepagents`, or `google.adk` imports, then AST analysis. |
From Python source, the AST pass recognizes:
* `StateGraph(...)` and `.compile()` where `` is a known `StateGraph` binding
* Factory calls: `create_react_agent` (from `langgraph.prebuilt`) and `create_deep_agent` (from `deepagents`)
* Same-module builder functions that return a LangGraph (the common `graph = _build()` idiom)
* ADK agent classes: `Agent`, `LlmAgent`, `SequentialAgent`, `ParallelAgent`, `LoopAgent`
A few limits worth knowing:
* The walker stops at depth 4 and skips standard junk directories (`.git`, `__pycache__`, `node_modules`, `.venv`, `venv`, `env`, `dist`, `build`, `target`, plus any dot-folder).
* Files over 1 MB are skipped to keep the scan fast.
* Symlinks are not followed.
* Parse errors (broken YAML, syntax errors in a `.py` file) are silently skipped; the scan never fails on malformed input.
When multiple sources point at the same `(file_path, variable_name)`, the highest-confidence detection wins, so a YAML or `langgraph.json` entry always beats a source-only match for the same target.
The wizard then runs a 6-rule cascade to pick a display name: `config.name`, then the `langgraph.json` graph key, then `pyproject.toml`'s `project.name`, then the parent directory (skipping `src`), then the filename with a trailing `_agent` stripped, and finally `My Agent` as a fallback.
Subsequent boots only need:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun serve
```
By default, `IDUN_CONFIG_PATH` points at `./config.yaml`. Override with `--config` or the env var if your config lives elsewhere:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun setup --config ./prod/config.yaml
idun serve
```
### Or run programmatically (engine-only)
If you don't want the admin panel and DB-backed config, use the engine SDK directly. No CLI, no DB, no UI:
```python serve.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine import run_server_from_config
run_server_from_config("config.yaml")
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
python serve.py
```
The engine reads the YAML, instantiates your graph, and serves the AG-UI streaming endpoint at `POST /agent/run`. You bring your own admin tooling.
## Step 3: Test the agent
Open `http://localhost:8000/` in a browser. The chat UI streams your agent's responses.
Or call the API directly:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST http://localhost:8000/agent/run \
-H "Content-Type: application/json" \
-d '{
"thread_id": "test-1",
"messages": [{"role": "user", "content": "Hello"}]
}'
```
You'll receive a server-sent event stream of AG-UI events.
## What you got for free
By writing eight lines of YAML, you now have:
* A FastAPI service at `/agent/run` with AG-UI streaming
* A chat UI at `/`
* An admin panel at `/admin/` for editing guardrails, memory, MCP servers, observability, and integrations
* A trace viewer at `/admin/traces/` with a waterfall span tree
* An OpenAPI schema at `/docs`
To add guardrails, observability, MCP servers, or per-agent SSO, edit the YAML or use the admin panel: no agent code change required.
## ADK agents
The flow is identical, with `agent.type: "ADK"` and the `agent` field instead of `graph_definition`:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
server:
api:
port: 8000
agent:
type: "ADK"
config:
name: "My ADK agent"
agent: "./my_adk_agent.py:root_agent"
session_service:
type: "in_memory"
memory_service:
type: "in_memory"
```
See [ADK frameworks reference](/frameworks/adk) for full configuration options.
## What's next
before exposing this beyond localhost
add input and output guards
wire Langfuse, Phoenix, or LangSmith
attach MCP servers
graph load errors, reload failures
# How easy it is to use Idun Engine with any Deep Agent
Source: https://docs.idun-group.com/guides/deepagents-with-idun
Wrap LangChain's Deep Agents framework in a production FastAPI service in minutes. Discover your agent through the Idun wizard, chat with it, get built-in dashboards and traces, plug in Langfuse on top, then add a Google Workspace MCP to send email — all without touching the engine code.
[Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview) is LangChain's framework for agents that plan, write to a virtual filesystem, and spawn subagents. Under the hood, it produces a compiled LangGraph `StateGraph` — which means **the Idun Engine treats it exactly like any other LangGraph agent**. No adapter, no glue code: point Idun at the module, get a streaming API, a chat UI, observability, and MCP tools for free.
This guide takes the official [text-to-SQL Deep Agent example](https://github.com/langchain-ai/deepagents/tree/main/examples/text-to-sql-agent), drops it behind Idun, and walks all the way to a Gemini-powered agent that queries SQLite, traces every step in the built-in dashboard and Langfuse, and emails its results via a Google Workspace MCP server.
## What you will build
Clone the Deep Agents example, run `idun init`, point the wizard at `agent.py`.
Ask natural-language questions, watch the planning + SQL tool calls stream live.
Switch the LangGraph checkpointer to SQLite — full chat history with one click.
Use the built-in dashboard and trace viewer, then plug Langfuse on top.
Connect Gmail / Drive / Calendar through MCP and have the agent email the results.
## Prerequisites
* Python 3.12+
* A Gemini API key — [get one](https://aistudio.google.com/apikey)
* (Step 7) A free [Langfuse Cloud](https://cloud.langfuse.com) account
* (Step 8) A Google Cloud project with a Desktop OAuth client and Gmail API enabled
We'll work from the official `text-to-sql-agent` example. It ships with the Chinook SQLite database and a few skills (`query-writing`, `schema-exploration`):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
git clone https://github.com/langchain-ai/deepagents.git
cd deepagents/examples/text-to-sql-agent
# Download the demo database
curl -L -o chinook.db \
https://github.com/lerocha/chinook-database/raw/master/ChinookDatabase/DataSources/Chinook_Sqlite.sqlite
```
The interesting bit of `agent.py` is the very last line:
```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent = create_sql_deep_agent()
```
`create_deep_agent(...)` returns a compiled `CompiledStateGraph`. Idun's LangGraph adapter accepts both compiled graphs and raw `StateGraph` builders, so this Just Works — no rewrite needed.
Install the engine and the standalone runtime alongside the example's dependencies:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv venv --python 3.12
source .venv/bin/activate
uv pip install -e .
uv pip install idun-agent-engine idun-agent-standalone langchain-google-genai
```
Create `.env` from the template the repo ships:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cp .env.example .env
```
```bash .env theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Idun standalone
IDUN_CONFIG_PATH=./config.yaml
IDUN_AGENT_CONFIG_PATH=./config.yaml
IDUN_ALLOW_OPEN_ADMIN=1
IDUN_ADMIN_AUTH_MODE=password
IDUN_ADMIN_PASSWORD_HASH=${IDUN_ADMIN_PASSWORD_HASH} # paste the output of `idun hash-password`
IDUN_SESSION_SECRET=${IDUN_SESSION_SECRET} # paste the output of `openssl rand -hex 32`
# LLM
GEMINI_API_KEY=${GEMINI_API_KEY} # export your real Gemini API key in your shell
```
Generate the two secrets:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun hash-password # prompts, prints a bcrypt hash → IDUN_ADMIN_PASSWORD_HASH
openssl rand -hex 32 # → IDUN_SESSION_SECRET
```
`IDUN_ALLOW_OPEN_ADMIN=1` lets you reach `/admin/*` without an auth gate while you explore locally. Turn it off (or remove it) before exposing the service.
You don't need to write `config.yaml` yourself. `idun init` boots the standalone server, opens your browser, and — since no agent is configured yet — routes you to the onboarding wizard.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun init
```
What happens on first run:
1. Alembic migrations create the standalone DB.
2. The server starts on `http://localhost:8000`.
3. Your browser opens the **discover** wizard.
The wizard asks two things:
* **Framework** — pick `LangGraph`. Deep Agents compile to a `StateGraph`, so the LangGraph adapter handles them.
* **Graph definition** — point Idun at the compiled graph: `agent.py:agent`. Format is `path/to/file.py:variable`.
That's the entire setup. No FastAPI route, no streaming protocol, no thread-id wiring. Click through and the engine boots with your agent attached.
Once the wizard finishes you land in the admin **Agent** page. The connection probe confirms the engine reports your agent as `text2sql_deepagent`; the configuration card shows the framework (LangGraph), the agent name, and the `agent.py:agent` graph reference.
Scroll down for the **Agent graph** card. Idun introspects the compiled graph and renders the Deep Agent's actual node structure — the `MemoryMiddleware`, `SkillsMiddleware`, and `PatchToolCallsMiddleware` `before_agent` hooks, the `model` node, the `TodoListMiddleware.after_model` post-hook, and the `tools` node. This is the Deep Agents runtime, made visible.
Open `http://localhost:8000/` and ask something the agent has to plan for:
> Hello, show me the schema for our users and transactions tables and find any customers who haven't made a purchase in 6 months.
The chat collapses the agent's planning into a **Reasoning** card. Expand it and you see exactly what the Deep Agent did: it loaded its skills via `read_file`, listed the SQL tables, fetched the `Customer` and `Invoice` schemas, sanity-checked the date, ran the query through `sql_db_query_checker`, and finally executed the result.
Below the reasoning card, the agent renders the final answer: a clean schema breakdown and a table of the 10 customers who haven't bought anything in six months.
Try a few more:
* "Top 5 best-selling artists?"
* "Which employee generated the most revenue, broken down by country?"
* "Plot revenue by genre and write a one-paragraph summary."
The same `POST /agent/run` endpoint is exposed at the API level — point CopilotKit, Vercel AI SDK, or any AG-UI client at it.
By default the LangGraph checkpointer is in-memory: restart the server and every conversation is gone. The **Memory** page in the admin lets you swap the backend without touching code.
Pick **SQLite**, set a file-backed URL (`sqlite:///deep.db`), **Save**. Idun reloads the checkpointer in place.
Back in the chat, every previous turn now lives in the **History** sidebar. New threads, named after their first message, persist across restarts.
For multi-replica production deployments, swap SQLite for PostgreSQL the same way — same UI, same one-click reload.
Before you wire up an external trace backend, Idun already gives you a built-in dashboard and a trace viewer powered by the same OpenTelemetry stream.
The **Dashboard** summarizes the last 24 hours: request count, p50/p95 latency, error rate, total cost, requests-per-minute and latency time series, and top error spans.
The **Traces** page lists every run with model, tokens, cost, and status — filter by model, status, user, or session.
Click any trace to inspect the span tree. You see the same node structure as the agent graph, plus the `ChatGoogleGenerativeAI` calls with their token counts, and the `sql_db_query` tool span.
Switch to **Waterfall** for the time-ordered view — which span ran in parallel, which blocked, where the latency lives.
This works out of the box. Nothing to configure.
Already-rich built-in observability is fine for development. For long-term storage, evaluations, prompt management, and team-wide sharing, plug in [Langfuse](https://langfuse.com).
1. Sign up for [Langfuse Cloud](https://cloud.langfuse.com) (free) and create a project. Copy the public + secret keys.
2. In the Idun admin, open **Observability** → pick **Langfuse**, fill in host, public key, secret key, and a run name. Toggle **Enabled** and **Save**.
3. Ask the agent another question. Refresh the Langfuse dashboard.
Every Deep Agent step is captured: the middleware hooks, each `ChatGoogleGenerativeAI` call with tokens and cost, the tool spans, the full prompt/completion pair. Langfuse also renders the graph topology — same nodes you saw inside Idun, now persisted for evaluation runs and team review.
Idun supports Langfuse, Arize Phoenix, LangSmith, GCP Trace, and GCP Logging side-by-side. They don't conflict; add more on the same Observability page.
Now the fun part: give the agent the ability to email its findings. We'll use the open-source [Google Workspace MCP server](https://github.com/taylorwilsdon/google_workspace_mcp), which exposes Gmail, Calendar, Drive, Docs, Sheets, Slides, and Forms as MCP tools.
### 9a. Get a Desktop OAuth client
In Google Cloud Console:
1. Create or pick a project, enable the **Gmail API** and **Google Docs API** (and any other Workspace APIs you want).
2. **APIs & Services → Credentials → Create credentials → OAuth client ID → Desktop app**.
3. Copy the client ID and secret into `.env`:
```bash .env theme={"theme":{"light":"github-light","dark":"github-dark"}}
GOOGLE_OAUTH_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-...
USER_GOOGLE_EMAIL=you@yourdomain.com
OAUTHLIB_INSECURE_TRANSPORT=1 # localhost OAuth callback
```
### 9b. Start the MCP server
Run the Google Workspace MCP locally over streamable HTTP:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
uvx workspace-mcp --transport streamable-http --port 8000
```
The first time you connect, the server prints an OAuth URL — complete the consent flow once and the token is cached for future runs.
### 9c. Register the server in Idun
In the admin: **MCP** → pick **Streamable HTTP** → fill in the endpoint (`http://127.0.0.1:8000/mcp`), name it `google-workspace`, save.
Click the wrench (🔧) to probe the server. Idun lists every tool it discovered — `send_gmail_message`, `create_doc`, `create_calendar`, `append_table_rows`, and 116 others.
### 9d. Wire the tools into the Deep Agent
Deep Agents bind tools at construction time, so we expose Idun's configured MCP tools to `create_deep_agent`. The engine ships an async helper for this — `idun_agent_engine.mcp.get_langchain_tools` — that reads the MCP servers from your Idun config and returns ready-to-use LangChain tools.
Because `agent.py` is imported at module load (inside the engine's lifespan, where an event loop is already running), we wrap the async helper in a one-shot thread so it can be called synchronously:
```python agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
from idun_agent_engine.mcp import get_langchain_tools
def get_langchain_tools_sync(config_path: str | Path | None = None) -> list[Any]:
"""Run the async ``get_langchain_tools`` from sync code at module load."""
with ThreadPoolExecutor(max_workers=1) as ex:
return ex.submit(asyncio.run, get_langchain_tools(config_path)).result()
idun_tools = get_langchain_tools_sync()
# ... inside create_sql_deep_agent():
return create_deep_agent(
model=model,
memory=["./AGENTS.md"],
skills=["./skills/"],
tools=sql_tools + idun_tools,
backend=FilesystemBackend(root_dir=base_dir),
)
```
Restart the engine (admin → Agent → **Restart**) and the Deep Agent now sees the 120 MCP tools alongside its SQL toolkit.
### 9e. Ask the agent to ship the report
Back in chat:
> Find the 10 customers who haven't bought anything in 6 months. Write up a detailed report as a Google Doc and email it to me at `you@example.com`.
The Deep Agent plans, runs the SQL, edits its working file with the report content, saves your address to memory via `write_todos`, then calls the MCP tools to create a Google Doc and send the email with the doc linked.
Check your inbox: the agent sent a clean message with the Doc link, signed as "Deep Agent", with the Google Doc auto-attached by Gmail.
And because Langfuse is still attached, the `create_doc` and `send_gmail_message` calls show up in the trace alongside the SQL steps — full causal chain in one place.
## What this gave you
Starting from an `agent.py` you didn't touch, you got:
| | Without Idun | With Idun |
| ------------------- | --------------------------------- | ------------------------------- |
| API serving | Write FastAPI yourself | `idun init` |
| Streaming protocol | Map `astream_events` by hand | AG-UI out of the box |
| Chat UI | Build a frontend | Bundled |
| Conversation memory | Wire up checkpointer + thread IDs | One-click backend swap |
| Dashboard | Hand-roll Prometheus + Grafana | Built in |
| Distributed tracing | Instrument OTLP manually | Built in + plug Langfuse on top |
| MCP tools | Write LangChain adapters | Register, discover, attach |
Total engine code written: **zero lines**.
## Next steps
turn off `IDUN_ALLOW_OPEN_ADMIN`, add SSO/OIDC, generate API keys for `/agent/run`.
when you go multi-replica — same Memory page, same one-click reload.
(PII, prompt injection) before exposing the chat publicly.
with the provided Dockerfile.
The Deep Agents framework gives you planning, filesystem, and subagents. Idun gives you the production wrapper. Together: an agent you can actually ship.
# Build and run the idun-assistant copilot
Source: https://docs.idun-group.com/guides/idun-assistant-copilot
Wire a LangGraph agent into Idun Engine end to end: MCP tools, system prompt, SQLite memory, Langfuse observability, and Google OIDC SSO, then run a multi-tool task in chat.
idun-assistant is Idun's in-house dev copilot. A LangGraph agent that reads the Idun docs, queries Jira and Confluence, opens GitHub issues, drafts Google Docs, and sends mail. End-to-end, the agent code is \~180 lines of Python; everything else (tools, prompts, memory, observability, auth) is wired through the standalone admin panel.
This guide takes you from "you have an agent" to "you have a protected, observable copilot," then runs a real multi-tool task in chat.
## The demo
After setup, you'll send this to the bot:
> Can you fetch the latest GitHub issues, make a summary in a Google Doc, and mail that to me?
The agent has to chain three MCP calls:
1. List recent issues through the GitHub MCP.
2. Create a new Google Doc through the `google-workspace` MCP.
3. Send the doc link via Gmail through the same MCP.
You'll see each tool call inline in the chat UI and, after the fact, in the trace at `/admin/traces/`.
## The agent code
`agent/agent.py` is a normal LangGraph workflow. A single ReAct loop. Two nodes, one conditional edge, no Idun-specific scaffolding. Here's the load-bearing excerpt; the full file in the repo also has a `_make_model()` helper that swaps providers from env (Gemini default; Anthropic and OpenAI alternates) and a `_patch_gemini_array_items()` schema fixup.
```python agent/agent.py (excerpt — see the repo for imports + _make_model) theme={"theme":{"light":"github-light","dark":"github-dark"}}
from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import tools_condition
from idun_agent_engine import get_prompt
from idun_agent_engine.mcp.helpers import get_langchain_tools
_model = _make_model() # env-driven Gemini/Anthropic/OpenAI selector
_prompt = get_prompt("system_prompt")
SYSTEM_PROMPT = _prompt.content if _prompt else ""
async def call_model(state: MessagesState) -> dict:
tools = await get_langchain_tools()
bound = _model.bind_tools(tools) if tools else _model
messages = [SystemMessage(content=SYSTEM_PROMPT), *state["messages"]]
response = await bound.ainvoke(messages)
return {"messages": [response]}
async def call_tools(state: MessagesState) -> dict:
tools = list(await get_langchain_tools() or [])
by_name = {t.name: t for t in tools}
out: list[ToolMessage] = []
for call in getattr(state["messages"][-1], "tool_calls", []) or []:
tool = by_name.get(call["name"])
content = (
str(await tool.ainvoke(call["args"]))
if tool else f"Tool {call['name']!r} not available."
)
out.append(ToolMessage(content=content, tool_call_id=call["id"], name=call["name"]))
return {"messages": out}
workflow = StateGraph(MessagesState)
workflow.add_node("call_model", call_model)
workflow.add_node("tools", call_tools)
workflow.add_edge(START, "call_model")
workflow.add_conditional_edges("call_model", tools_condition)
workflow.add_edge("tools", "call_model")
```
Three things to notice:
1. `get_prompt("system_prompt")` resolves a versioned prompt from the standalone DB. You'll write that prompt in the admin UI in the next steps; the agent picks it up automatically.
2. `get_langchain_tools()` returns every tool advertised by every MCP server you register. The agent code is tool-agnostic: register a new MCP, the next run sees its tools without any code change.
3. `workflow` is an uncompiled `StateGraph`. Idun compiles it with the configured checkpointer.
`_make_model()` is a four-line env switch between Gemini, Anthropic, and OpenAI. Skip it for now.
## Spin up the standalone
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install idun-agent-engine
cd idun-assistant
cp .env.example .env # fill in GEMINI_API_KEY (or your provider's key)
idun init
```
`idun init` migrates the DB, opens your browser at `http://localhost:8000/`, and serves the standalone. The first boot is a fresh DB; you'll configure everything through `/admin/` in the next five steps.
Open `/admin/mcp/`. Add four MCP servers. The agent will discover every tool they advertise on the next reload.
| Server | Transport | What it gives the agent |
| ------------------ | -------------------------------------------------------------------------- | -------------------------------------------------- |
| `idun-docs` | Streamable HTTP at `https://docs.idun-group.com/mcp` | Search Idun's own docs. |
| `atlassian` | stdio: `uvx mcp-atlassian`, with Jira + Confluence env vars | Search and edit Jira tickets and Confluence pages. |
| `github` | stdio: `docker run -i --rm ghcr.io/github/github-mcp-server` with a GH PAT | Repos, issues, PRs, commits, releases. |
| `google-workspace` | Streamable HTTP at `http://127.0.0.1:8000/mcp` | Gmail, Docs, Drive, Calendar. |
Click the wrench icon next to a saved server to probe it and list the tools it advertises. Atlassian alone gives the agent 72 tools spanning Confluence and Jira:
Open `/admin/prompts/` and create a new prompt. Name it `system_prompt`. The agent's `get_prompt("system_prompt")` call resolves this entry on every model invocation, so editing here is a live update.
Body:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are Idun's in-house dev assistant. You hang out in Discord and Google
Chat, helping the team get things done, looking up Jira tickets, reading
docs (idun-docs MCP), drafting messages, finding files in Drive, opening
PRs, whatever fits the tools bound to your toolset right now.
You sound like a sharp colleague, not a corporate chatbot. Direct, dry,
a little wry. No "I hope this helps!" sign-offs, no apologetic preambles,
no flattery. If something is a bad idea, say so. If a question is genuinely
ambiguous, ask once, briefly, then move on.
Behavioral rules:
- Use the bound tools when they fit. Only call tools that appear in your
tool set. Never invent tool names or arguments.
- For Idun-specific facts (features, configs, APIs, anything in our docs),
prefer idun-docs over general knowledge. For repo facts, use github.
For tickets and Confluence pages, use atlassian.
- Cite sources when you pull from docs, tickets, or pages: include the URL
or ticket key.
- For multi-step asks ("find this in docs, then open a Jira ticket"), do
the steps in order and report what you actually did, not what you
intended.
- When you don't know, say "I don't know" instead of guessing.
```
Tag it `latest`. Save. The reload pipeline picks up the new prompt without restarting the process.
The default scaffold uses in-memory checkpointing, which loses every conversation when the process restarts. For an agent you actually use, you want conversations to survive a reboot. Open `/admin/memory/` and pick SQLite.
Field:
* **db\_url**: `sqlite:///./conversations.db` (a file next to the running process).
Save. The reload pipeline rebuilds the LangGraph checkpointer against SQLite. Every thread is now durable; restart `idun serve` and the next user message resumes the same conversation.
For production with multiple replicas, swap to `postgresql://...` in the same form. The agent code does not change.
Open `/admin/observability/` and click Langfuse.
Fields:
* **Host**: `https://cloud.langfuse.com` (or your self-hosted URL)
* **Public key**: from your Langfuse project
* **Secret key**: from your Langfuse project
* **Run name**: `idun-assistant`
Save. The engine fans every span out to Langfuse alongside the local trace store at `/admin/traces/`. Same data, two surfaces.
idun-assistant is an internal tool. Anyone with an `@idun-group.com` Google account should be able to use it; no one else. Open `/admin/sso/` and configure Google OIDC with a domain allowlist.
Fields:
* **Provider**: Google
* **Issuer**: `https://accounts.google.com`
* **Client ID**: the OAuth client ID from your Google Cloud project
* **Audience**: same value as Client ID (default)
* **Allowed domains**: `idun-group.com`
Save. From now on, every request to `/agent/run` (and the deprecated invoke/stream shims) requires a valid Google JWT whose `email` claim ends in `@idun-group.com`. The chat UI at `/` runs the OAuth handshake automatically and forwards the access token. See [SSO](/auth/sso) for the full validation flow and how to add specific outside emails to `allowed_emails`.
## Run the demo
Open `http://localhost:8000/`. Sign in with your Google account (the OAuth screen appears once and the cookie sticks for 24h). Send:
> Can you fetch the latest GitHub issues, make a summary in a Google Doc, and mail that to me?
The bot streams partial output as the ReAct loop runs. Inline action cards show each call the model emits, the arguments it picks, and the result the tool returns. You also get a collapsed **Reasoning** card at the top with the agent's step-by-step plan. By the end of the chain, the agent has:
1. Called `list_issues` and `search_repositories` to find the right repo (`Idun-Group/idun-agent-platform`) and pull the top open issues.
2. Created a Google Doc with `create_doc`, summarizing the issues.
3. Sent the doc link via `send_gmail_message`.
And in your inbox:
Back in the admin panel, `/admin/` now reflects the run. Request count, p50/p95 latency, error rate, and total cost all roll up from the same `standalone_trace` rows the trace viewer reads.
For deeper debugging open `/admin/traces/` and click the run. You get the full span tree (every LLM call and every tool invocation) plus the exact JSON in/out of each tool in the right rail. Useful when the agent picks the wrong tool or passes weird arguments.
Toggle to the **Waterfall** view to see where the time actually went. The critical path is highlighted, so you can spot the slow tool call at a glance.
See [Local trace store](/observability/traces) for the trace UI reference.
## What's next
the full transport reference and how to register your own MCP server.
versioning, `get_prompt()` resolution, and the admin REST API.
when to swap SQLite for Postgres.
the bundled span-tree viewer at `/admin/traces/`.
provider presets, allowed\_domains + allowed\_emails, and how validation works.
# How to deploy a LangGraph agent to production in 5 minutes
Source: https://docs.idun-group.com/guides/langgraph-production-deployment
Step-by-step tutorial to take a LangGraph agent from a local script to a production API with streaming, guardrails, memory, and observability using Idun Engine.
You built a LangGraph agent that works in a notebook. Now you need it behind an API with authentication, conversation memory, guardrails, and tracing. This guide takes you from a working `StateGraph` to a production endpoint in under 5 minutes.
## The production gap
LangGraph gives you a graph-based runtime for building agents. It does not give you the infrastructure to serve them. When you move from `graph.invoke()` in a script to handling real traffic, you run into five missing pieces:
No built-in HTTP server. You write FastAPI routes, CORS, request parsing, and streaming yourself. LangServe is deprecated. LangGraph Platform requires a LangSmith account.
LangGraph supports checkpointers, but you wire up database connections, async lifecycle, and thread ID routing from HTTP requests yourself.
Your agent will process PII, jailbreak attempts, and toxic content unless you build input/output validation from scratch.
When a production agent returns garbage at 3am, you need traces. LangGraph has no built-in tracing. You instrument it yourself.
And one more: **no streaming protocol.** Modern chat UIs expect Server-Sent Events with structured events (text deltas, tool calls, thinking indicators). LangGraph emits raw `astream_events` that you need to map to a protocol your frontend understands.
Idun Engine fills all five gaps. Your `StateGraph` stays unchanged. Idun wraps it into a FastAPI service with AG-UI streaming, configurable memory, guardrails, and multi-provider observability, all configured through a single YAML file.
This guide drives configuration from `config.yaml`. The standalone runtime exposes the same fields through the admin panel at `/admin/`, so any step that edits the YAML can also be done from a browser; the DB is the source of truth in steady state.
## What you will build
A LangGraph agent served as a REST + AG-UI streaming endpoint.
In-memory persistence, upgradeable to PostgreSQL or SQLite.
PII detection that blocks requests before they reach the agent.
Langfuse tracing on every invocation with full LLM call details.
## Prerequisites
* Python 3.12+
* A Gemini API key (or any LangChain-compatible LLM)
* 5 minutes
Create a project directory with an agent that has tool calling built in:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir langgraph-prod && cd langgraph-prod
mkdir agent && touch agent/__init__.py
```
```python agent/agent.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from langchain_core.tools import tool
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.graph import MessagesState, START, StateGraph
from langgraph.prebuilt import tools_condition
@tool
def add(a: float, b: float) -> float:
"""Add two numbers together."""
return a + b
@tool
def multiply(a: float, b: float) -> float:
"""Multiply two numbers together."""
return a * b
model = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
google_api_key=os.getenv("GEMINI_API_KEY"),
)
tools = [add, multiply]
async def call_model(state: MessagesState):
model_with_tools = model.bind_tools(tools)
response = await model_with_tools.ainvoke(state["messages"])
return {"messages": [response]}
async def call_tools(state: MessagesState):
from langchain_core.messages import ToolMessage
tools_by_name = {t.name: t for t in tools}
last_message = state["messages"][-1]
results = []
for tool_call in last_message.tool_calls:
t = tools_by_name[tool_call["name"]]
result = await t.ainvoke(tool_call["args"])
results.append(
ToolMessage(
content=str(result),
tool_call_id=tool_call["id"],
name=tool_call["name"],
)
)
return {"messages": results}
workflow = StateGraph(MessagesState)
workflow.add_node("call_model", call_model)
workflow.add_node("tools", call_tools)
workflow.add_edge(START, "call_model")
workflow.add_conditional_edges("call_model", tools_condition)
workflow.add_edge("tools", "call_model")
```
Two things matter here:
1. The variable `workflow` is an **uncompiled** `StateGraph`. Do not call `.compile()`. Idun compiles it for you with the configured checkpointer and store.
2. The state uses `MessagesState` (a `TypedDict` with a single `messages` field). Idun auto-detects this as a chat-mode agent. If your state has additional fields, Idun treats it as a structured-input agent and exposes the full JSON Schema through the capabilities endpoint.
Install the engine wheel (it bundles the standalone admin/chat/traces app and the `idun` console script):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install idun-agent-engine langchain-google-genai
```
Create `config.yaml` next to your agent directory:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: LANGGRAPH
config:
name: "Production Chatbot"
graph_definition: "./agent/agent.py:workflow"
checkpointer:
type: memory
```
That is a complete, valid config. Three fields define your entire setup:
* `type: LANGGRAPH` tells Idun which adapter to use.
* `graph_definition: "./agent/agent.py:workflow"` points to your file and variable. Format: `path/to/file.py:variable_name`. Idun dynamically imports it.
* `checkpointer.type: memory` enables in-memory conversation persistence. Every request with the same `thread_id` continues the conversation.
Start the server:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GEMINI_API_KEY="AIzaSy-replace-with-your-gemini-key"
idun serve
```
You should see:
```
Agent 'Production Chatbot' initialized and ready to serve!
Starting Idun Agent Engine server on http://localhost:8000...
```
Your agent is now live. Open `http://localhost:8000/docs` to see the full OpenAPI spec, `http://localhost:8000/` for the chat UI, and `http://localhost:8000/admin/` for the admin panel. To use a different port, export `IDUN_PORT` before running `idun serve`.
The canonical endpoint is `POST /agent/run`, which implements the AG-UI streaming protocol used by CopilotKit, Vercel AI SDK, and other modern chat frontends.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -N -X POST http://localhost:8000/agent/run \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"threadId": "session-1",
"runId": "run-1",
"state": {},
"messages": [
{"id": "msg-1", "role": "user", "content": "What is 25 multiplied by 17?"}
],
"tools": [],
"context": [],
"forwardedProps": {}
}'
```
You get back a stream of AG-UI events: `RunStarted`, `TextMessageStart`, `ToolCallStart`, `ToolCallEnd`, `TextMessageContent` (with deltas), `TextMessageEnd`, `RunFinished`. Your frontend renders these as they arrive.
You can also check capabilities and health:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl http://localhost:8000/agent/capabilities
curl http://localhost:8000/health
```
Or open `/` in a browser and chat with the agent directly through the bundled chat UI.
Guardrails validate input and output at the API boundary. They run before and after your agent, blocking requests that violate your policies. Idun uses [Guardrails AI](https://guardrailsai.com) validators, downloaded and run locally.
Add a `guardrails` section to your `config.yaml`:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: LANGGRAPH
config:
name: "Production Chatbot"
graph_definition: "./agent/agent.py:workflow"
checkpointer:
type: memory
guardrails:
input:
- config_id: detect_pii
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD"]
- config_id: ban_list
banned_words: ["ignore previous instructions", "system prompt"]
```
Set your Guardrails AI API key and restart:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export GUARDRAILS_API_KEY="grdrls_replace-with-your-guardrails-key"
idun serve
```
On startup, Idun downloads the guardrail validators from the Guardrails AI Hub and initializes them locally. The first start takes 30-60 seconds while models download. Subsequent starts are instant.
Test it by sending a message that contains PII:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -N -X POST http://localhost:8000/agent/run \
-H "Content-Type: application/json" \
-d '{
"threadId": "session-2",
"runId": "run-1",
"state": {},
"messages": [
{"id": "msg-1", "role": "user", "content": "My email is john@company.com and my phone is 555-0123"}
],
"tools": [],
"context": [],
"forwardedProps": {}
}'
```
The request is blocked before it ever reaches your agent. The PII guardrail detected an email address and phone number in the input.
Idun supports 15 guardrail types: PII detection, ban lists, toxic language, jailbreak detection, prompt injection, NSFW filtering, bias detection, topic restriction, gibberish detection, competition checks, language validation, code scanning, RAG hallucination detection, Google Model Armor, and custom LLM-based validation.
Add an `observability` section to your `config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: LANGFUSE
enabled: true
config:
host: "https://cloud.langfuse.com"
public_key: "pk-lf-replace-with-your-public-key"
secret_key: "sk-lf-replace-with-your-secret-key"
run_name: "production-chatbot"
```
Restart the server. Every invocation now shows up in your Langfuse dashboard with full traces: LLM calls, token counts, latencies, tool executions.
Idun supports five observability providers, and you can attach more than one to the same agent:
| Provider | Mechanism |
| ----------------- | ----------------------------- |
| Langfuse | LangChain callback handler |
| Arize Phoenix | OpenTelemetry + OpenInference |
| LangSmith | LangChain callback handler |
| GCP Cloud Trace | OpenTelemetry span exporter |
| GCP Cloud Logging | Python logging integration |
The engine fans spans out to every enabled provider; each is lazy-loaded. Independently, the local trace store at `/admin/traces/` always captures every run alongside any external providers you configure; see [Local trace store](/observability/traces).
In-memory checkpointing loses conversations when the server restarts. For production, switch to PostgreSQL:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: LANGGRAPH
config:
name: "Production Chatbot"
graph_definition: "./agent/agent.py:workflow"
checkpointer:
type: postgres
db_url: "postgresql://postgres:postgres@localhost:5432/agent_memory"
```
Idun handles the async connection pool, table creation, and lifecycle management. Your agent code does not change.
For a simpler persistence option, use SQLite:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkpointer:
type: sqlite
db_url: "sqlite:///conversations.db"
```
The `/agent/run` endpoint implements the AG-UI protocol, which is natively supported by CopilotKit and compatible with any SSE-consuming frontend. The standalone also ships its own chat UI at `/`, so you can demo and dogfood without writing one. Replace the bundled UI by pointing `IDUN_UI_DIR` at your own static export; see [Customizing the chat UI](/standalone/customizing-ui).
## Full config.yaml
Here is the complete config file with everything from this guide: agent, guardrails, observability, and PostgreSQL memory. Copy it and adjust the values for your setup.
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: LANGGRAPH
config:
name: "Production Chatbot"
graph_definition: "./agent/agent.py:workflow"
checkpointer:
type: postgres
db_url: "postgresql://postgres:postgres@localhost:5432/agent_memory"
guardrails:
input:
- config_id: detect_pii
pii_entities: ["EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD"]
- config_id: ban_list
banned_words: ["ignore previous instructions", "system prompt"]
observability:
- provider: LANGFUSE
enabled: true
config:
host: "https://cloud.langfuse.com"
public_key: "pk-lf-replace-with-your-public-key"
secret_key: "sk-lf-replace-with-your-secret-key"
run_name: "production-chatbot"
```
## How Idun compares to alternatives
| Capability | Manual FastAPI | LangServe (deprecated) | LangGraph Platform | Idun Engine |
| -------------------- | -------------- | ---------------------- | -------------------------- | --------------------------- |
| API serving | You build it | Provided | Provided (cloud) | Provided (self-hosted) |
| AG-UI streaming | You build it | Not supported | Not supported | Built in |
| Guardrails | You build it | Not supported | Not supported | 15 types, YAML config |
| Observability | You build it | Limited | LangSmith only | 5 providers, simultaneous |
| Memory/checkpointing | You wire it | Limited | Built in | YAML config, 3 backends |
| MCP tool servers | You build it | Not supported | Not supported | YAML config |
| SSO/OIDC | You build it | Not supported | Cloud-managed | YAML config |
| Vendor lock-in | None | LangChain ecosystem | LangSmith account required | None (open source, GPL-3.0) |
| Self-hosted | Yes | Yes | No (cloud only) | Yes |
Idun Engine is open source and self-hosted. Your agent code stays yours. The config file is the only coupling, and it is a plain YAML file you can generate from any tool.
## Next steps
You now have a production LangGraph agent with streaming, memory, guardrails, and observability. From here:
to give your agent access to external tools via the Model Context Protocol. Add an `mcp_servers` section to your config, or use the admin panel at `/admin/mcp/`.
to require JWT authentication on all agent endpoints. Add an `sso` section with your OIDC issuer and client ID.
(WhatsApp, Discord, Slack, Google Chat) to expose your agent on external channels. Each is a config section with provider credentials, or a card at `/admin/integrations/`.
with versioning and Jinja2 variables through the admin panel at `/admin/prompts/` or in your YAML.
The full configuration reference is at [/configuration](/configuration). The complete list of guardrail types is at [/guardrails/reference](/guardrails/reference).
# Guides
Source: https://docs.idun-group.com/guides/overview
Step-by-step tutorials for common workflows on Idun Engine.
Practical guides to help you build, deploy, and manage your AI agents.
Deploy your first agent in minutes
Take a LangGraph agent from notebook to production API
A real internal copilot in \~180 lines + a few clicks
Wrap an existing LangGraph or ADK agent in Idun
Wrap a LangChain Deep Agent (planner, virtual filesystem, sub-agents) end to end
Drive your hosted agent from a script via /agent/run
LangGraph or Google ADK adapter reference
Extend the agent with external tools over stdio, SSE, HTTP, or WebSocket
Protect your agent with safety guards
Trace and monitor agent runs
Persist conversation state across sessions
Single-container deploy with managed Postgres
# Programmatic chat: the /agent/run contract
Source: https://docs.idun-group.com/guides/programmatic-chat
Drive your Idun-hosted agent from a script using the AG-UI request shape, plus the structured-mode trap to watch out for.
The `/agent/run` endpoint streams [AG-UI](https://github.com/CopilotKit/ag-ui) events over Server-Sent Events. The bundled chat UI uses it. Your scripts and integrations call the same endpoint.
## Request shape
The body is an AG-UI `RunAgentInput`:
| Field | Type | Common value |
| ---------------- | ------ | --------------------------- |
| `threadId` | string | a unique conversation ID |
| `runId` | string | a unique-per-call ID |
| `state` | object | `{}` for chat-mode agents |
| `messages` | array | `[{id, role, content}]` |
| `tools` | array | `[]` if your agent has none |
| `context` | array | `[]` for the default case |
| `forwardedProps` | object | `{}` for the default case |
`parentRunId` is optional, used when resuming a run.
All seven required fields must be present (even when empty), or the endpoint returns `422 Unprocessable Entity`.
## Curl example
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -N -X POST http://127.0.0.1:8000/agent/run \
-H "Content-Type: application/json" \
-d '{
"threadId": "demo-1",
"runId": "run-1",
"state": {},
"messages": [
{"id":"msg-1","role":"user","content":"Hello"}
],
"tools": [],
"context": [],
"forwardedProps": {}
}'
```
The response is an SSE stream (`Content-Type: text/event-stream`) of AG-UI events: `RunStarted`, `TextMessageStart` / `Content` / `End`, `ToolCallStart` / `Args` / `End`, `ThinkingStart` / `End`, `RunFinished`.
## Structured-mode agents
If your LangGraph agent declares an explicit `input_schema` on the `StateGraph` that contains fields beyond `messages`, the engine auto-detects `input.mode = "structured"` and requires `messages[-1].content` to be valid JSON matching the input schema. The chat surface returns a `RUN_ERROR` SSE event with `code: VALIDATION_ERROR` if you send plain text.
Example explicit-schema agent:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class InputState(TypedDict):
user_input: str
class OutputState(TypedDict):
graph_output: str
class OverallState(TypedDict):
user_input: str
intermediate: str
graph_output: str
builder = StateGraph(
OverallState,
input_schema=InputState,
output_schema=OutputState,
)
```
For this agent, the chat content must be JSON:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"messages": [
{
"id": "msg-1",
"role": "user",
"content": "{\"user_input\": \"Hello\"}"
}
]
}
```
The bundled chat UI handles this auto-wrap. If you call `/agent/run` directly, build the JSON yourself.
## Implicit-state agents (the common case)
When your agent declares one `OverallState` TypedDict with `messages` plus internal carry-fields and does not supply `input_schema=`, the engine resolves to `input.mode = "chat"`. Plain-text content works as expected. The internal scalars (`intent`, `draft`, etc.) get populated by your nodes during the run.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class AgentState(TypedDict, total=False):
messages: list[BaseMessage]
intent: str
draft: str
response: str
builder = StateGraph(AgentState) # no input_schema, chat mode
```
For a strict typed public input contract, follow the explicit-schema idiom from [LangGraph's input/output schema how-to](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/). Otherwise the implicit default keeps chat plain-text-friendly.
## Reading the SSE stream
Every event line is `data: \n\n`. Decode and dispatch on `type`. A minimal Python reader:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
import requests
with requests.post(
"http://127.0.0.1:8000/agent/run",
json={
"threadId": "demo-1",
"runId": "run-1",
"state": {},
"messages": [{"id": "msg-1", "role": "user", "content": "Hello"}],
"tools": [],
"context": [],
"forwardedProps": {},
},
stream=True,
headers={"Accept": "text/event-stream"},
) as response:
for line in response.iter_lines():
if not line or not line.startswith(b"data: "):
continue
event = json.loads(line[len(b"data: "):])
if event.get("type") == "TextMessageContent":
print(event["content"], end="", flush=True)
```
For TypeScript and React, use the AG-UI client SDK or the bundled chat UI hook. Both handle reconnects and token-level streaming for you.
# Discord
Source: https://docs.idun-group.com/integrations/discord
Connect your Idun agent to Discord so users can interact with it through slash commands.
Connect your Idun agent to Discord so users can interact with it through slash commands in any server.
## Prerequisites
* A running Idun agent (engine)
* A Discord account
* Your engine must be publicly reachable (use [ngrok](https://ngrok.com) for local development)
## Setup
Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels.
Click **Discord** and fill in the form:
| Field | Value |
| ---------------- | ------------------------------------------------ |
| `bot_token` | Bot token from your Discord application |
| `application_id` | Application ID from the Discord Developer Portal |
| `public_key` | Public key from the Discord Developer Portal |
Save the form. The reload pipeline registers the Discord webhook handler on the running engine.
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
2. Click **New Application** and give it a name
3. On the **General Information** page, copy the **Application ID** and **Public Key**
1. Go to the **Bot** tab
2. Click **Reset Token** to generate a **Bot Token**
3. Copy the token immediately (it is only shown once)
1. Go to **OAuth2 > URL Generator**
2. Select scopes: `bot`, `applications.commands`
3. Select bot permissions: `Send Messages`
4. Copy the generated URL, open it in your browser, and select your server
5. To get your **Guild ID**: enable Developer Mode in Discord settings (User Settings > Advanced > Developer Mode), then right-click your server name and select **Copy Server ID**
Add the Discord integration to your engine config:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
integrations:
- provider: "DISCORD"
enabled: true
config:
bot_token: "MTI..."
application_id: "123456789012345678"
public_key: "abcdef1234567890..."
guild_id: "987654321098765432"
```
| Field | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| `bot_token` | Bot token from step 2 |
| `application_id` | Application ID from step 1 |
| `public_key` | Public key from step 1 (used for Ed25519 signature verification) |
| `guild_id` | (Optional) Your Discord server ID. If set, slash commands are scoped to this server and appear instantly |
1. Make sure your engine is running and publicly reachable
2. In the Discord Developer Portal, go to **General Information**
3. Set **Interactions Endpoint URL** to:
```
https:///integrations/discord/webhook
```
Discord sends a PING request to verify the endpoint. The engine handles this automatically.
Discord does not create commands automatically. Register them via the Discord API.
**Register a guild command** (appears instantly in your server):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
"https://discord.com/api/v10/applications/{APPLICATION_ID}/guilds/{GUILD_ID}/commands" \
-H "Authorization: Bot {BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "ask",
"description": "Ask the agent a question",
"options": [
{
"name": "query",
"description": "Your question",
"type": 3,
"required": true
}
]
}'
```
Replace `{APPLICATION_ID}`, `{GUILD_ID}`, and `{BOT_TOKEN}` with your values.
`"type": 3` means a STRING option. The engine extracts this as the query text sent to your agent.
To register a **global command** (available in all servers the bot is in), omit `/guilds/{GUILD_ID}` from the URL. Global commands can take up to 1 hour to appear.
1. Go to your Discord server
2. Type `/ask query: Hello`
3. The bot shows a "thinking..." indicator (deferred response), then replies with your agent's answer
## How it works
1. User sends `/ask query: ...` in Discord
2. Discord POSTs the interaction to your engine's webhook
3. Engine verifies the Ed25519 signature
4. Engine defers the response (Discord requires a reply within 3 seconds)
5. Engine invokes the agent asynchronously with the query text
6. Engine edits the deferred message with the agent's reply
**Session tracking**: The Discord user ID is used as the session ID, so conversation context is maintained per user.
**Message limit**: Discord messages are capped at 2,000 characters. Longer replies are truncated.
## Next steps
Connect your agent to Slack DMs and channels.
Reach the same agent through Bot Framework in Teams.
Secure the engine before exposing webhooks to the public internet.
# Google Chat
Source: https://docs.idun-group.com/integrations/google-chat
Connect your Idun agent to Google Chat so users can interact with it by @mentioning the bot in spaces and direct messages.
Connect your Idun agent to Google Chat so users can interact with it by @mentioning the bot in spaces and direct messages.
## Prerequisites
* A running Idun agent (engine)
* A [Google Workspace](https://workspace.google.com) account with access to Google Chat
* A [Google Cloud](https://console.cloud.google.com) project (free to create, no billing required for Chat API config)
* Your engine must be publicly reachable (use [ngrok](https://ngrok.com) for local development)
## Setup
Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows the available channels including Google Chat.
Click **Google Chat** and fill in the credentials.
| Field | Value |
| ---------------------------------- | --------------------------------------------------------- |
| `service_account_credentials_json` | Full JSON key file content from your GCP service account |
| `project_number` | GCP project number (found on the Cloud Console dashboard) |
Save the form. The reload pipeline registers the Google Chat webhook handler on the running engine.
1. Go to the [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project (or use an existing one)
3. Navigate to **APIs & Services** > **Library**
4. Search for **Google Chat API** and click **Enable**
1. Go to **IAM & Admin** > **Service Accounts**
2. Click **Create Service Account**
3. Give it a name (e.g. "idun-chat-bot")
4. Click **Done** (no additional roles needed)
5. Click on the service account > **Keys** tab > **Add Key** > **Create new key** > **JSON**
6. Save the downloaded JSON key file
1. Go to the [Google Chat API configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
2. Fill in the app details:
* **App name**: Your agent's name (this is what users will @mention)
* **Avatar URL**: Optional icon URL
* **Description**: Brief description of what the bot does
3. Under **Connection settings**, select **HTTP endpoint URL**
4. Set the URL to:
```
https:///integrations/google-chat/webhook
```
5. Under **Authentication Audience**, select **Project Number**
6. Under **Visibility**, choose who can discover and use the app
7. Click **Save**
Add the Google Chat integration to your engine config:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
integrations:
- provider: "GOOGLE_CHAT"
enabled: true
config:
service_account_credentials_json: '{"type": "service_account", "project_id": "my-project", ...}'
project_number: "123456789012"
```
| Field | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------- |
| `service_account_credentials_json` | Full JSON content of the service account key file from step 2 |
| `project_number` | GCP project number (found on the Cloud Console project dashboard, not the project ID) |
You can also store the credentials JSON in an environment variable and reference it in the config to avoid putting secrets in YAML files.
1. Open Google Chat
2. Start a direct message with your bot, or add it to a space
3. @mention the bot followed by your message: `@YourBotName do stuff for me`
4. Your agent processes the message and replies in the same space
## How it works
1. User @mentions the bot in a space or sends a direct message
2. Google Chat POSTs the interaction event to your engine's webhook
3. Engine verifies the JWT bearer token (signed by `chat@system.gserviceaccount.com`) using the project number as audience
4. Engine extracts the message text (stripping the @mention prefix via `argumentText`)
5. Engine invokes the agent with the cleaned text
6. Engine sends the agent's reply back via the Google Chat API (`spaces.messages.create`)
**Session tracking**: The Google Chat user resource name (`users/123456`) is used as the session ID, so conversation context is maintained per user.
**Bot messages ignored**: The handler skips messages from senders with type `BOT` to avoid infinite loops.
**@mention stripping**: Google Chat provides an `argumentText` field that contains the message text without the @mention. The engine uses this so your agent receives clean input (e.g. "do stuff for me" instead of "@BotName do stuff for me").
## Next steps
Connect your agent to Slack DMs and channels.
Reach the same agent through Bot Framework in Teams.
Secure the engine before exposing webhooks to the public internet.
# Slack
Source: https://docs.idun-group.com/integrations/slack
Connect your Idun agent to Slack so users can interact with it through direct messages and channel messages.
Connect your Idun agent to Slack so users can interact with it through direct messages or channel messages.
## Prerequisites
* A running Idun agent (engine)
* A [Slack](https://slack.com) workspace where you have permission to install apps
* Your engine must be publicly reachable (use [ngrok](https://ngrok.com) for local development)
## Setup
Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels.
Click **Slack** and fill in the bot token and signing secret.
| Field | Value |
| ---------------- | ----------------------------------------------------- |
| `bot_token` | Bot User OAuth Token from your Slack app (`xoxb-...`) |
| `signing_secret` | Signing secret from your Slack app |
Save the form. The reload pipeline registers the Slack webhook handler on the running engine; the agent now responds to messages forwarded by Slack.
1. Go to the [Slack API Portal](https://api.slack.com/apps)
2. Click **Create an App** > **From scratch**
3. Give it a name and select your workspace
1. Go to **Basic Information** > **App Credentials** > copy the **Signing Secret**
2. Go to **OAuth & Permissions** > under **Bot Token Scopes**, add:
* `chat:write` (send messages)
* `channels:read` (view basic channel info)
3. Click **Install to Workspace** > **Allow**
4. Copy the **Bot User OAuth Token** (`xoxb-...`)
Add the Slack integration to your engine config:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
integrations:
- provider: "SLACK"
enabled: true
config:
bot_token: "xoxb-..."
signing_secret: "abc123..."
```
| Field | Description |
| ---------------- | ---------------------------------------------------------------------- |
| `bot_token` | Bot User OAuth Token from step 2 (`xoxb-...`) |
| `signing_secret` | Signing secret from step 2 (used for HMAC-SHA256 request verification) |
1. Make sure your engine is running and publicly reachable
2. In the Slack API Portal, go to **Event Subscriptions** > toggle **ON**
3. Set **Request URL** to:
```
https:///integrations/slack/webhook
```
Slack sends a `url_verification` challenge. The engine handles this automatically. You should see a green **Verified** checkmark.
4. Under **Subscribe to bot events**, add:
* `message.im` (messages in direct conversations with the bot)
* `message.channels` (messages in channels the bot is a member of)
5. Click **Save Changes**
If you see a yellow banner saying "You've changed the permission scopes. Please reinstall your app", click the reinstall link. Events are not delivered until you reinstall.
1. Go to **App Home** in the sidebar
2. Under **Show Tabs**, enable the **Messages Tab**
3. Check **"Allow users to send Slash commands and messages from the messages tab"**
If you want the bot to respond in channels (not only DMs):
* Go to the channel in Slack and type `/invite @YourBotName`
* Or click the channel name > **Integrations** tab > **Add an App**
1. Open Slack
2. Send a direct message to your bot, or post in a channel it has been invited to
3. Your agent processes the message and replies in the same conversation
## How it works
1. User sends a message to the bot (DM or channel)
2. Slack POSTs the event to your engine's webhook
3. Engine verifies the HMAC-SHA256 signature using the signing secret
4. Engine invokes the agent with the message text
5. Engine sends the agent's reply back via the Slack Web API (`chat.postMessage`)
**Session tracking**: The Slack user ID is used as the session ID, so conversation context is maintained per user.
**Bot messages ignored**: The handler skips messages with a `bot_id` to avoid infinite loops.
## Next steps
Connect your agent to a Discord server via slash commands.
Reach the same agent through Bot Framework in Teams.
Secure the engine before exposing webhooks to the public internet.
# Microsoft Teams
Source: https://docs.idun-group.com/integrations/teams
Connect your Idun agent to Microsoft Teams as a Bot Framework bot so users can @mention it in channels and direct messages.
Connect your Idun agent to Microsoft Teams so users can interact with it through @mentions and direct messages. The integration is single-tenant: each deployment registers its own Microsoft app in its own Azure AD and runs its own engine against it.
## Prerequisites
* A running Idun agent (engine)
* An [Azure](https://portal.azure.com) tenant where you can register an application
* Your engine must be publicly reachable (use [ngrok](https://ngrok.com) or Bot Framework's tunnel for local development)
## Setup
Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels.
Click **Microsoft Teams** and fill in the credentials from your Azure AD app registration.
| Field | Value |
| --------------- | ------------------------------------------------------------------------------- |
| `app_id` | Microsoft App ID (the Application/Client ID from the Azure AD app registration) |
| `app_password` | Client secret from the Azure AD app registration |
| `app_tenant_id` | Azure AD tenant ID that owns the app registration |
Save the form. The reload pipeline registers the Teams webhook handler on the running engine; the agent now responds to messages forwarded by the Bot Framework.
1. Open the [Azure portal](https://portal.azure.com) and go to **Microsoft Entra ID > App registrations**
2. Click **New registration**, give it a name, and choose **Single tenant**
3. From the overview page, copy the **Application (client) ID** and the **Directory (tenant) ID**
4. Open **Certificates & secrets > Client secrets**, click **New client secret**, and copy the **Value** (not the Secret ID) immediately — it's only shown once
Standard Teams bot messaging (`@mention` detection and replies) is handled by the Bot Framework channel and does not require Microsoft Graph application permissions. If your bot needs to receive every channel message (not only `@mentions`), declare Resource-Specific Consent (RSC) permissions in the Teams app manifest instead: `ChannelMessage.Read.Group` for channels or `ChatMessage.Read.Chat` for chats. Graph application permissions like `ChannelMessage.Read.All` are only needed for administrative or archival scenarios outside the bot's conversation context.
Add the Teams integration to your engine config:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
integrations:
- provider: "TEAMS"
enabled: true
config:
app_id: "00000000-0000-0000-0000-000000000000"
app_password: "${TEAMS_APP_PASSWORD}"
app_tenant_id: "11111111-1111-1111-1111-111111111111"
```
| Field | Description |
| --------------- | ---------------------------------------------------------- |
| `app_id` | Application (client) ID from the Azure AD app registration |
| `app_password` | Client secret value (not the secret ID) |
| `app_tenant_id` | Directory (tenant) ID that owns the app registration |
Authentication uses Bot Framework's `ConfigurationBotFrameworkAuthentication` with `MicrosoftAppType=SingleTenant` hardcoded. The integration does not currently support multi-tenant apps.
1. In the Azure portal, search for **Azure Bot** and click **Create**
2. Give the bot a handle, pick the resource group, and select **Use existing app registration** with the app ID from step 1
3. Once the bot is created, open **Channels** and add **Microsoft Teams**
4. Open **Configuration** and set the **Messaging endpoint** to:
```
https:///integrations/teams/messages
```
1. From the bot's **Channels > Microsoft Teams** page, follow the **Open in Teams** link
2. Install the app in a team or use it as a personal chat
3. @mention the bot in a channel, or DM it directly
## How it works
1. User sends a message in Teams (DM or @mention in a channel)
2. Bot Framework POSTs the activity to your engine's `/integrations/teams/messages` webhook
3. The engine validates the Bot Framework signature against the configured tenant + app credentials
4. The engine invokes the agent with the message text
5. The engine sends the agent's reply back through the Bot Framework adapter
**Session tracking**: the Teams user identifier is used as the session ID, so conversation context is maintained per user across messages.
## Next steps
Connect your agent to Slack DMs and channels.
Reach the same agent through Discord slash commands.
Secure the engine before exposing webhooks to the public internet.
# WhatsApp
Source: https://docs.idun-group.com/integrations/whatsapp
Connect your Idun agent to WhatsApp so users can interact with it through messages using the Meta Business API.
Connect your Idun agent to WhatsApp so users can interact with it through messages.
## Prerequisites
* A running Idun agent (engine)
* A [Meta Business](https://business.facebook.com/) account
* A WhatsApp Business API app set up in the [Meta Developer Portal](https://developers.facebook.com/)
* Your engine must be publicly reachable (use [ngrok](https://ngrok.com) for local development)
## Setup
Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels.
Click **WhatsApp** and fill in the access token, phone number ID, and verify token.
| Field | Value |
| ----------------- | ------------------------------------------------------------------------------ |
| `access_token` | Meta Graph API permanent access token |
| `phone_number_id` | WhatsApp Business phone number ID |
| `verify_token` | Webhook verification token (must match what you set in the Meta App Dashboard) |
Save the form. The reload pipeline registers the WhatsApp webhook handler on the running engine.
1. Go to the [Meta Developer Portal](https://developers.facebook.com/apps/)
2. Click **Create App** > select **Business** type
3. Fill in the app name and select your Business account
4. On the app dashboard, add the **WhatsApp** product
1. Go to **WhatsApp > API Setup**
2. Copy the following values:
* **Phone Number ID**: The ID of the phone number you send from
* **Permanent Access Token**: Generate one under **System Users** in your Meta Business settings (or use the temporary token for testing)
3. Choose a **Verify Token**: Any secret string you will use to verify the webhook
Add the WhatsApp integration to your engine config:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
integrations:
- provider: "WHATSAPP"
enabled: true
config:
access_token: "EAAxxxxxxx..."
phone_number_id: "123456789012345"
verify_token: "my-webhook-verify-secret"
api_version: "v21.0"
```
| Field | Description |
| ----------------- | ------------------------------------------------------------------------------------ |
| `access_token` | Meta Graph API permanent access token |
| `phone_number_id` | WhatsApp Business phone number ID from step 2 |
| `verify_token` | Webhook verification token (must match what you configure in the Meta App Dashboard) |
| `api_version` | (Optional) Meta Graph API version, defaults to `v21.0` |
1. Make sure your engine is running and publicly reachable
2. In the Meta Developer Portal, go to **WhatsApp > Configuration**
3. Under **Webhook**, click **Edit** and set:
* **Callback URL**: `https:///integrations/whatsapp/webhook`
* **Verify Token**: The same `verify_token` from your config
4. Click **Verify and Save** (Meta sends a GET request to verify the endpoint)
5. Subscribe to the **messages** webhook field
1. Open WhatsApp on your phone
2. Send a message to the WhatsApp Business number
3. Your agent processes the message and replies directly in the chat
With a test phone number, you can only send messages to numbers registered in the Meta Developer Portal under **WhatsApp > API Setup > Test Numbers**.
## How it works
1. User sends a message to the WhatsApp Business number
2. Meta POSTs the webhook payload to your engine
3. Engine verifies the payload structure
4. Engine invokes the agent with the message text
5. Engine sends the agent's reply back via the Meta Graph API
**Session tracking**: The sender's phone number is used as the session ID, so conversation context is maintained per user.
**Webhook verification**: On setup, Meta sends a GET request with a challenge token. The engine verifies it against your `verify_token` and responds automatically.
## Next steps
Connect your agent to Slack DMs and channels.
Reach the same agent through Discord slash commands.
Secure the engine before exposing webhooks to the public internet.
# Introduction
Source: https://docs.idun-group.com/introduction
Everything Idun Engine ships: agent runtime, chat UI, admin, traces, guardrails, memory, MCP, integrations, and auth, in one self-hosted process.
Idun Engine is the open-source production wrapper for LangGraph and Google ADK agents. `pip install idun-agent-engine`, point it at your agent, and get a self-hosted FastAPI service with chat UI, admin panel, traces viewer, guardrails, memory, and MCP server support, all on your infrastructure.
## What you get
Streaming HTTP service with AG-UI protocol compatibility. Drop it behind any CopilotKit or AG-UI client.
LangGraph and Google ADK, served with AG-UI streaming.
Activity, traces, p50 / p95 latency, error counts, and recent runs at `/admin/`.
15+ built-in guards powered by Guardrails AI.
Langfuse, Phoenix, LangSmith, or GCP, plus a local trace store with a waterfall viewer at `/admin/traces/`.
In-memory, SQLite, or PostgreSQL checkpointers.
stdio, SSE, streamable HTTP, or WebSocket with auto-discovery.
Versioned templates with Jinja2 variables.
Slack, Discord, Microsoft Teams, Google Chat, and WhatsApp.
OIDC SSO on `/agent/*` routes; `none` / `password` for the admin panel.
## Community
Questions and help.
Proposals and ideas.
Bugs and feature requests.
## Next steps
Deploy your first agent in under 30 minutes.
How the engine and standalone fit together.
LangGraph and Google ADK adapters.
# Docker MCP toolkit
Source: https://docs.idun-group.com/mcp-servers/docker-toolkit
Add external tool capabilities to your agents using pre-built MCP servers from the Docker MCP toolkit.
The Docker MCP toolkit is a collection of pre-built MCP servers packaged as Docker containers. These servers provide common functionality (web fetching, file system access, database operations) without requiring you to write or maintain custom MCP server code.
* **No custom code**: Pull and run pre-configured MCP servers
* **Isolation**: Each server runs in its own container with controlled resource limits
* **Community-maintained**: Implementations follow the MCP specification
* **Portable**: Works the same in development and production
This guide walks through integrating the Fetch MCP server from the Docker toolkit with an existing agent.
By the end of this guide, you will have an agent that can retrieve and analyze content from any URL through the Fetch MCP tool.
## Prerequisites
Before starting, you need:
* A working agent deployed with Idun (see the [quickstart](/quickstart))
* [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running
Pull the Fetch MCP server image:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker pull mcp/fetch
```
## Set up the Fetch MCP server
Open Docker Desktop and verify:
1. Docker Desktop is running
2. The `mcp/fetch` image appears in the **Images** section
Add the Fetch server to your standalone, either through the admin panel or in YAML.
**Admin UI:** open `/admin/mcp/` and click **stdio**. Fill in:
| Field | Value |
| ----------- | ------------------------------------ |
| **Name** | `fetch` |
| **Command** | `docker` |
| **Args** | `["run", "-i", "--rm", "mcp/fetch"]` |
**`config.yaml`:**
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
mcp_servers:
- name: fetch
transport: stdio
command: docker
args: ["run", "-i", "--rm", "mcp/fetch"]
```
The args breakdown:
* `run`: Execute a new container
* `-i`: Interactive mode (keeps STDIN open for MCP communication)
* `--rm`: Remove container when it stops
* `mcp/fetch`: The Docker image to run
Save (admin UI) or restart `idun serve` (YAML). The engine discovers the Fetch server's tools at boot.
Import MCP tools in your agent code:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from google.adk.agents import LlmAgent
from idun_agent_engine.mcp import get_adk_tools
import os
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "TRUE"
os.environ["GOOGLE_CLOUD_PROJECT"] = "your-project-id"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city."""
return {"status": "success", "city": city, "time": "10:30 AM"}
idun_tools = get_adk_tools()
tools = [get_current_time] + idun_tools
root_agent = LlmAgent(
model="gemini-2.5-flash",
name="root_agent",
description="Tells the current time in a specified city.",
instruction="You are a helpful assistant.",
tools=tools,
)
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from idun_agent_engine.mcp import get_langchain_tools
model = ChatOpenAI(model="gpt-4")
mcp_tools = await get_langchain_tools()
agent = create_react_agent(
model=model,
tools=mcp_tools,
state_modifier="You are a helpful assistant with access to web content fetching.",
)
```
`get_adk_tools()` and `get_langchain_tools()` discover every MCP server in the engine config and make their tools available. You do not need to configure individual tools.
From your agent directory:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
idun serve
```
The engine reads its config from the DB, starts the Fetch MCP server as a Docker container, registers the fetch tool with your agent, and serves the chat UI at `http://localhost:8000/`.
Open `http://localhost:8000/` and use the chat to test:
```
Give me the information on this website: https://www.idun-group.com/idun-agent-platform
```
```
Go to https://news.ycombinator.com and summarize the top 3 stories
```
```
Fetch https://github.com/trending and list the trending repositories
```
When you send a query, the agent recognizes it needs web content, invokes the Fetch MCP tool, and the Docker container retrieves the URL content for the agent to analyze.
## Verify the MCP server
Check that the Docker container is running:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker ps | grep mcp/fetch
```
View MCP server logs for debugging:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker logs $(docker ps -q --filter ancestor=mcp/fetch)
```
## Advanced configuration
### Multiple MCP servers
Add more MCP servers through `/admin/mcp/` or in the same `mcp_servers` list in `config.yaml`. Each entry runs alongside the others and contributes its tools to the registry.
**Filesystem access:**
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
```
Allows the agent to read, write, and manipulate files within specified directories.
**Custom Docker MCP server:**
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- name: custom
transport: stdio
command: docker
args: ["run", "-i", "--rm", "your-registry/your-mcp:latest"]
```
Deploy your own MCP servers for database access, API integrations, or internal tools.
## Troubleshooting
**Symptoms:** Agent starts but MCP tools are not available.
**Solutions:**
* Verify Docker Desktop is running: `docker info`
* Check the image exists: `docker images | grep mcp/fetch`
* Test the container manually: `docker run -i --rm mcp/fetch`
* Review Docker Desktop logs
**Symptoms:** Agent responds but does not fetch web content.
**Solutions:**
* Check Docker container is running: `docker ps | grep mcp/fetch`
* Review MCP server logs: `docker logs `
* Try an explicit query: "Use the fetch tool to get [https://example.com](https://example.com)"
* Restart the agent
* Verify the MCP config saved correctly in `/admin/mcp/` or in your `config.yaml`
**Symptoms:** "Invalid args format" error when saving.
**Solution:** Args must be a properly formatted JSON array.
Correct:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
["run", "-i", "--rm", "mcp/fetch"]
```
Incorrect:
```
run -i --rm mcp/fetch
```
Incorrect:
```
["run -i --rm mcp/fetch"]
```
## Best practices
* **Naming**: Use descriptive, lowercase names for MCP servers: `fetch`, `filesystem`, `database`
* **Incremental testing**: Add one MCP server at a time. Test functionality before adding more
* **Resource limits**: In production, set Docker resource constraints to prevent runaway usage:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
["run", "-i", "--rm", "--memory=512m", "--cpus=0.5", "mcp/fetch"]
```
* **Logging**: Configure Docker logging for better observability:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
["run", "-i", "--rm", "--log-driver=json-file", "--log-opt=max-size=10m", "mcp/fetch"]
```
* **Credentials**: Never hardcode sensitive information in MCP configurations. Use environment variables
* **Monitoring**: Use [observability](/observability/overview) to track MCP server latency and error rates
## Next steps
How the engine discovers and registers MCP tools.
Trace MCP tool calls alongside agent runs.
Diagnose container and transport failures.
# MCP Servers
Source: https://docs.idun-group.com/mcp-servers/overview
Extend your agent with external tool servers using the Model Context Protocol.
Idun Engine uses the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) to extend agent capabilities with external tools. You declare MCP servers in your `config.yaml` (or through the admin panel at `/admin/mcp/`), and the engine discovers every advertised tool at boot.
The [langgraph-tool-local template](/templates) shows how to mix local tools with MCP tools in a single agent.
## How it works
1. **Register MCP servers** in the admin panel or YAML config with transport settings and connection details
2. **The engine discovers tools** from each configured MCP server at startup
3. **Agents invoke tools** during conversations as needed, with results passed back for response generation
The standalone runs a single agent per process, so every MCP server registered in the config (or admin panel) is available to that agent.
## Transport types
MCP servers connect to the engine through one of four transport protocols:
| Transport | Use case | Required fields |
| ----------------- | -------------------------------------------- | ----------------- |
| `stdio` | Local processes, Docker containers | `command`, `args` |
| `sse` | Remote servers with Server-Sent Events | `url` |
| `streamable_http` | Remote servers with HTTP streaming (default) | `url` |
| `websocket` | Persistent bidirectional connections | `url` |
The transport is decided by the MCP server, not the client. Most hosted servers expose exactly one. For example, `https://mcp.data.gouv.fr/mcp` is `streamable_http` only, with no `sse` or `stdio` fallback. Check the server's documentation before picking a transport; mismatched values fail at the handshake, not the YAML schema.
## Configuration example
Define MCP servers in your `config.yaml` or through the admin panel:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
mcp_servers:
- name: fetch
transport: stdio
command: docker
args: ["run", "-i", "--rm", "mcp/fetch"]
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
env:
ALLOWED_DIRECTORY: /data
- name: custom-api
transport: streamable_http
url: https://mcp.example.com/api
headers:
Authorization: "Bearer ${MCP_API_TOKEN}"
```
Tools are discovered at engine boot and made available to your agent via `get_langchain_tools()` / `get_adk_tools()`.
Navigate to `/admin/mcp/` in the running standalone. The catalog at the top groups MCP servers by transport: Streamable HTTP, SSE, WebSocket, and stdio. Existing servers are listed below with their transport, endpoint, and status.
Click the transport card you want and fill in the form: a unique name, the URL (or command and args for stdio), optional headers. The **Enabled** toggle lets you keep a server configured but skipped at startup.
Save; the reload pipeline re-instantiates the engine with the new MCP registry. Click the wrench icon next to a server to probe it and list the tools it advertises. The probe doubles as a connection check.
## Integration approaches
Pre-built MCP servers packaged as Docker containers. Pull, configure, and use without writing server code.
Host your own MCP servers for custom business logic, proprietary data sources, or internal APIs.
## Framework integration
The engine provides helper functions to load MCP tools into your agent code:
```python ADK theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine.mcp import get_adk_tools
idun_tools = get_adk_tools()
```
```python LangGraph theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
from idun_agent_engine.mcp import get_langchain_tools
async def _load_tools():
return await get_langchain_tools()
mcp_tools = asyncio.run(_load_tools())
```
These functions discover all MCP servers attached to your agent and make their tools available. You do not need to configure individual tools.
## Probing a server programmatically
The wrench icon in the MCP admin page is backed by an admin REST endpoint, so the same probe runs from a script or a CI smoke check. `POST /admin/api/v1/mcp-servers/{mcp_id}/tools` connects to the registered server, lists every advertised tool, and returns a `StandaloneConnectionCheck`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sX POST "http://localhost:8000/admin/api/v1/mcp-servers/$MCP_ID/tools" \
--cookie "$IDUN_SESSION_COOKIE"
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"ok": true,
"details": {
"name": "data_gouv",
"transport": "streamable_http",
"toolCount": 9,
"tools": ["get_dataset_info", "search_datasets", "..."]
},
"error": null
}
```
On failure (unreachable URL, MCP handshake mismatch, malformed config, registry init error) the same shape returns with `ok: false`, the upstream message in `error`, and any partial detail still attached. The probe runs under a 5-second timeout (`services/connection_checks.py`) and converts every exception into the `ok: false` payload, so it is safe in a polling loop. The HTTP status stays 200 on every outcome except the auth check failing or the row id not existing.
The route is admin-authenticated: under `IDUN_ADMIN_AUTH_MODE=password` you need the session cookie minted by `POST /admin/api/v1/auth/login`. Use this in CI to fail fast when a production MCP server has gone away, or in a release-time check before flipping traffic.
`get_langchain_tools()` is async, so it must be awaited inside an `async` function. If you need to bind tools at module load, run it with `asyncio.run(...)` as shown above. A synchronous helper (`get_langchain_tools_sync`) is also available; see the [engine MCP reference](/mcp-servers/overview) for details.
## Next steps
Pre-built MCP servers packaged as Docker containers. Pull, configure, and use without writing server code.
Full schema for `config.yaml` including the `mcp_servers` block.
# ADK memory
Source: https://docs.idun-group.com/memory/adk
Configure session services and memory services for ADK agents to manage conversation state and long-term knowledge.
ADK agents use **session services** and **memory services** to manage conversational context. Session services handle conversation state for individual interactions. Memory services store long-term knowledge that spans multiple sessions.
For more details, see the [ADK Sessions documentation](https://google.github.io/adk-docs/sessions/).
## Session services vs memory services
| | Session service | Memory service |
| ------------ | --------------------------------------------- | ------------------------------------------ |
| **Purpose** | Manages individual conversation state | Stores long-term knowledge across sessions |
| **Scope** | Single conversation (events, temporary state) | Cross-session (searchable knowledge base) |
| **Required** | Yes | Optional |
| **Options** | In Memory, Vertex AI, Database | In Memory, Vertex AI |
## Set up ADK memory
During agent creation or editing, navigate to the **Memory** step in the agent form.
Choose a session service backend (required for conversation state):
* **In Memory**: For development and testing
* **Vertex AI**: For production on Google Cloud
* **Database**: For production with SQL persistence
Fill in connection details if required.
Choose a memory service backend (optional, for long-term memory):
* **In Memory**: For development and testing
* **Vertex AI**: For production with long-term storage
Fill in connection details if required.
Click **Next** to continue, then finalize with **Save changes**. Restart the agent to apply the new configuration.
## Session service options
Session services manage conversation state and events for individual sessions.
### In-memory session service
The `InMemorySessionService` stores session data in the application's memory.
| Property | Detail |
| --------------- | ------------------------------------------------- |
| **Persistence** | None. Data is lost when the application restarts. |
| **Performance** | Fastest option, no I/O overhead. |
| **Use cases** | Development, testing, quick prototyping. |
**Configuration**: No additional configuration required.
### Vertex AI session service
The `VertexAiSessionService` uses Google Cloud's Vertex AI infrastructure for session management.
| Property | Detail |
| --------------- | --------------------------------------------- |
| **Persistence** | Cloud-native, persistent storage. |
| **Scalability** | Handles high-volume, distributed deployments. |
| **Integration** | Works with other Google Cloud services. |
| **Use cases** | Production deployments on Google Cloud. |
**Configuration**: Requires the following fields:
* `project_id`: Google Cloud project ID
* `location`: GCP region (for example, `us-central1`)
* `reasoning_engine_app_name`: Vertex AI Reasoning Engine application name
### Database session service
The `DatabaseSessionService` connects to a relational database (PostgreSQL, MySQL) for persistent session storage using SQLAlchemy.
| Property | Detail |
| --------------- | ------------------------------------------------------------- |
| **Persistence** | SQL-based storage with transaction support. |
| **Scalability** | Supports multi-instance deployments. |
| **Reliability** | Production-grade features including transactions and backups. |
| **Use cases** | Production deployments requiring SQL persistence. |
**Configuration**: Requires a database connection string.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "ADK"
config:
session_service:
type: "database"
db_url: "postgresql://user:pass@localhost:5432/dbname"
```
## Memory service options
Memory services manage long-term knowledge storage that persists across multiple sessions.
### In-memory memory service
The `InMemoryMemoryService` provides ephemeral memory storage.
| Property | Detail |
| --------------- | ----------------------------------------------------------------------- |
| **Persistence** | None. Data is lost when the application restarts. |
| **Performance** | Fast access, no external dependencies. |
| **Use cases** | Development, testing, scenarios where long-term memory is not required. |
**Configuration**: No additional configuration required.
### Vertex AI memory service
The `VertexAiMemoryService` provides cloud-backed memory with long-term storage using Vertex AI Memory Banks.
| Property | Detail |
| --------------- | -------------------------------------------------------------- |
| **Persistence** | Long-term, persistent storage across sessions. |
| **Scalability** | Cloud-native, handles large knowledge bases. |
| **Search** | Semantic search capabilities for retrieving relevant memories. |
| **Use cases** | Production deployments requiring long-term memory. |
**Configuration**: Requires the following fields:
* `project_id`: Google Cloud project ID
* `location`: GCP region
* `memory_bank_resource_id`: Vertex AI Memory Bank resource ID
## Best practices
### Session services
* **Use in-memory for local development**: No setup required, fast iteration
* **Use Database for production**: Reliable SQL-based persistence with multi-instance support
* **Use Vertex AI for Google Cloud production**: Cloud-native and scalable
* **Configure session isolation**: Each conversation should have a unique session ID to prevent state leakage
### Memory services
* **Use in-memory for development**: Fast iteration, no external dependencies
* **Use Vertex AI for production**: Long-term persistence with semantic search capabilities
* **Ingest session data into memory**: Periodically move important information from sessions to long-term memory
### General
* **Separate concerns**: Use session services for conversation state and memory services for long-term knowledge
* **Monitor storage usage**: Long-running sessions and large memory stores can consume significant resources
* **Implement backup strategies**: Set up regular backups for production database and Vertex AI configurations
## Troubleshooting
### Session service issues
1. **Database connection errors**: Use the **Verify** button on the memory configuration card to check connectivity. If the standalone runs in Docker and the database is on the host, use `host.docker.internal` instead of `localhost`
2. **Vertex AI authentication**: Verify Google Cloud credentials are configured and the service account has the required permissions
3. **Session not persisting**: Confirm the session service is initialized and session IDs are used consistently
### Memory service issues
1. **Vertex AI Memory Bank**: Verify the Memory Bank resource ID is correct, the bank exists in the specified project and location, and IAM permissions are set
2. **Memory not accessible**: Confirm the memory service is initialized and its configuration matches your setup
### General
1. **Review logs**: Check agent logs for session and memory-related errors
2. **Check permissions**: Verify the agent has access to all required storage resources
3. **Verify configuration**: Double-check all connection strings, credentials, and resource IDs
## Next steps
Configure the ADK adapter and Gemini-powered agents.
Add safety guards to your agent inputs and outputs.
Trace runs, monitor latency, and inspect token usage.
# LangGraph memory
Source: https://docs.idun-group.com/memory/langgraph
Configure checkpointing for LangGraph agents to persist conversation state across interactions.
LangGraph agents use **checkpointing** to save state during execution. The platform saves a snapshot of the graph state at every super-step, so conversations survive failures and restarts.
**A `checkpointer:` block is required** in every LangGraph `config.yaml`. Requests to a LangGraph agent without one fail at runtime with `No checkpointer set`. Pick `type: memory` for ephemeral state, `type: sqlite` for laptop / single-process persistence, or `type: postgres` for production.
For more details, see the [LangGraph checkpointing persistence documentation](https://docs.langchain.com/oss/python/langgraph/persistence).
## What checkpointing provides
Checkpoints are saved to a **thread**, identified by a unique `thread_id`. This enables:
* **Memory**: Maintain context between interactions in conversations
* **Human-in-the-loop**: Inspect, interrupt, and approve graph steps
* **Time travel**: Replay prior executions and debug specific steps
* **Fault tolerance**: Resume from the last successful step after failures
## Set up checkpointing
During agent creation or editing, navigate to the **Checkpointing** step in the agent form.
Select the checkpointing backend that matches your deployment needs. See the sections below for details on each option.
Enter the required configuration for your chosen backend (file path for SQLite, connection string for PostgreSQL).
Click **Next** to continue, then finalize with **Save changes**. Restart the agent to apply the new configuration.
## Checkpointing backends
### In-memory
The `InMemorySaver` stores checkpoints in the application's memory. This is the default option with no external dependencies.
| Property | Detail |
| --------------- | ------------------------------------------------- |
| **Persistence** | None. Data is lost when the application restarts. |
| **Performance** | Fastest option, no I/O overhead. |
| **Use cases** | Development, testing, stateless workflows. |
**Configuration**: No additional configuration required.
### SQLite
The `SqliteSaver` uses a file-based SQLite database to store checkpoints.
| Property | Detail |
| --------------- | -------------------------------------------- |
| **Persistence** | Data persists on disk in a single file. |
| **Performance** | Fast for single-process applications. |
| **Concurrency** | Limited to single-writer scenarios. |
| **Use cases** | Local development, small-scale applications. |
**Configuration**: Requires a database file path.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "LANGGRAPH"
config:
checkpointer:
type: "sqlite"
db_url: "checkpoints.db"
```
### PostgreSQL
The `PostgresSaver` uses PostgreSQL for checkpoint storage. This is the recommended backend for production deployments.
| Property | Detail |
| --------------- | -------------------------------------------------- |
| **Persistence** | Production-grade database storage. |
| **Performance** | Optimized for multi-process and concurrent access. |
| **Scalability** | Supports multiple agent instances. |
| **Use cases** | Production deployments, multi-instance setups. |
**Configuration**: Requires a PostgreSQL connection string.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "LANGGRAPH"
config:
checkpointer:
type: "postgres"
db_url: "postgresql://user:pass@localhost:5432/dbname"
```
## Threads and state
### Threads
A **thread** is a unique identifier (`thread_id`) that groups related checkpoints together. Each conversation or interaction should use a unique `thread_id` to maintain isolation between sessions.
When invoking a graph with a checkpointer, you must specify a `thread_id`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
config = {"configurable": {"thread_id": "user-123-session-1"}}
```
### State snapshots
Each checkpoint contains a `StateSnapshot` with:
* **values**: The state channel values at that point in time
* **config**: Configuration associated with the checkpoint
* **metadata**: Additional metadata about the checkpoint
* **next**: Nodes to execute next in the graph
You can retrieve the latest state or a specific checkpoint using `graph.get_state(config)`.
## Best practices
* **Use SQLite for local development**: No server setup required, file-based storage
* **Use PostgreSQL for production**: Multi-process support, reliability, and scalability
* **Use in-memory for testing**: Fastest option for stateless testing scenarios
* **Configure thread isolation**: Each conversation should have a unique `thread_id`
* **Monitor checkpoint storage**: Long-running conversations can accumulate significant state
## Troubleshooting
1. **Verify database connection**: Use the **Verify** button on the memory configuration card to check connectivity. If the standalone runs in Docker and the database is on the host, use `host.docker.internal` instead of `localhost`
2. **Check permissions**: Confirm the agent has read/write access to the database or file system
3. **Thread ID required**: Always provide a `thread_id` in the config when using checkpointers
4. **Database schema**: PostgreSQL checkpointers automatically create required tables on first use
5. **Review logs**: Check agent logs for checkpoint-related errors
## Next steps
Configure the LangGraph adapter and graph definition.
Add safety guards to your agent inputs and outputs.
Trace runs, monitor latency, and inspect token usage.
# Memory
Source: https://docs.idun-group.com/memory/overview
Enable agents to maintain state and context across conversations using checkpointing and session services.
Memory enables agents to maintain state and context across conversations. With memory configured, agents remember previous interactions and can resume conversations after failures or restarts.
Idun Engine supports multiple memory and checkpointing strategies depending on your agent framework.
## Framework support
Checkpointing for conversation state persistence. Supports in-memory, SQLite, and PostgreSQL backends.
Session services for conversation state and memory services for long-term knowledge storage.
## How checkpointing works
Checkpointing saves your agent's state during execution. Each interaction is associated with a unique session or thread identifier, and the platform saves state snapshots at each step of the agent graph. This enables:
* **Conversation continuity**: Maintain context between interactions
* **Fault tolerance**: Resume from the last successful step after a failure
* **Thread isolation**: Each `session_id` maps to a unique thread, keeping conversations separate
* **Concurrent conversations**: Multiple users can interact with the same agent simultaneously
## Backend comparison
| Backend | Persistence | Concurrency | Best for |
| ------------------ | ---------------------- | -------------- | ---------------------------------------------- |
| **In-memory** | None (lost on restart) | Single process | Development and testing |
| **SQLite** | File-based | Single writer | Local development, single-instance deployments |
| **PostgreSQL** | Database | Multi-process | Production, multi-instance deployments |
| **Vertex AI** | Cloud-native | Distributed | Production on Google Cloud (ADK only) |
| **Database (SQL)** | SQL-based | Multi-process | Production with SQL persistence (ADK only) |
## Quick configuration examples
Navigate to `/admin/memory/` in the running standalone. The catalog shows the supported backends: SQLite, PostgreSQL, In Memory, Vertex AI, and Database (ADK-only).
Click the backend you want and fill in the connection details. Save; the reload pipeline re-instantiates the engine with the new checkpointer or session service.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "LANGGRAPH"
config:
checkpointer:
type: "postgres"
db_url: "postgresql://user:pass@localhost:5432/dbname"
```
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
type: "ADK"
config:
session_service:
type: "in_memory"
memory_service:
type: "in_memory"
```
## Best practices
* **Use in-memory for development**: No setup required, fastest iteration
* **Use PostgreSQL or Database for production**: Multi-process support, reliability, and crash recovery
* **Configure thread isolation**: Each conversation should have a unique `session_id` to prevent state leakage
* **Monitor storage usage**: Long-running conversations can accumulate significant state over time
## Next steps
Checkpointing for conversation state persistence. Supports in-memory, SQLite, and PostgreSQL backends.
Session services for conversation state and memory services for long-term knowledge storage.
# Arize Phoenix
Source: https://docs.idun-group.com/observability/arize-phoenix
Set up Arize Phoenix observability to trace, evaluate, and troubleshoot your agents.
Set up Arize Phoenix to trace agent execution and view performance data in your Phoenix dashboard.
Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine.
## Set up Phoenix observability
1. Go to [Arize Phoenix](https://phoenix.arize.com)
2. Sign up or log in to your account
3. Create a new project or select an existing one
4. Note your **Project Name** and the **Collector Endpoint** (typically `https://collector.phoenix.com`)
If you are hosting Phoenix yourself, have your collector endpoint URL ready. This is the URL where your Phoenix instance accepts trace data.
Open the running standalone at `/admin/observability/` and click **Phoenix**. Fill in:
* **Collector endpoint**: URL of the Phoenix collector (e.g., `https://collector.phoenix.com` or your self-hosted URL)
* **Project name**: The project in Phoenix to bucket these traces under
Save the form. The reload pipeline re-instantiates the engine with the new observability config; the next agent run starts streaming spans to Phoenix.
Alternatively, configure Phoenix in your `config.yaml` for first-boot seeding or engine-only mode:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "PHOENIX"
enabled: true
config:
collector_endpoint: "https://collector.phoenix.com"
project_name: "idun-agent"
```
## View observability data
Once your agent is running with observability enabled:
1. Interact with your agent through the chat UI at `/` or the API at `/agent/run`
2. Open your Phoenix dashboard
3. Navigate to your project to view traces
You will see traces showing agent execution flows, tool usage, and latency data.
## Best practices
* Use descriptive names for observability configurations
* Monitor latency using Phoenix's performance tools
* Check traces regularly to understand agent behavior
## Troubleshooting
ADK does not currently support simultaneous tracing with multiple providers.
1. **Check collector endpoint**: Verify the URL is correct and accessible from the agent environment
2. **Verify project name**: Confirm it matches the project name in Phoenix exactly
3. **Check network**: Confirm your agent environment can reach the Phoenix collector
## Next steps
See the same agent runs in the bundled admin UI without extra config.
Compare built-in providers and their configuration shapes.
Wire a different OTel-compatible backend through Pattern B.
# Writing a custom observability handler
Source: https://docs.idun-group.com/observability/custom-handler
Extend the engine to support a new tracing or observability provider by subclassing ObservabilityHandlerBase. Covers the abstract surface, callback vs global instrumentation patterns, and the factory registry.
The engine ships handlers for Langfuse, Arize Phoenix, LangSmith, Google Cloud Trace, and Google Cloud Logging. If you run a different observability stack (Datadog, Honeycomb, Grafana Tempo, a custom OTLP collector), you can wrap it as an Idun handler by subclassing `ObservabilityHandlerBase`.
Like the agent adapter factory, the observability factory in `idun_agent_engine.observability.base.create_observability_handler` is currently a hardcoded `if/elif` over the `ObservabilityProvider` enum. Wiring a new handler requires a small upstream change. There is no Python entry-point hook today.
## The base class
`ObservabilityHandlerBase` lives in [`libs/idun_agent_engine/src/idun_agent_engine/observability/base.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/observability/base.py). It is small: three members and one optional helper:
| Member | Kind | Purpose |
| ------------------- | --------------- | ------------------------------------------------------------------------------ |
| `provider` | class attribute | Lowercase provider identifier (e.g. `"langfuse"`, `"phoenix"`) |
| `__init__(options)` | method | Read your provider's API keys and config from `options`, initialise the client |
| `get_callbacks()` | method | Return a list of LangChain `BaseCallbackHandler` instances (can be empty) |
| `get_run_name()` | method | Optional. Return a run name from `options`, or `None` |
That's it. The minimal handler is fifteen lines.
## Two integration patterns
Observability providers fall into one of two categories. Your handler should pick one.
### Pattern A: LangChain callbacks (Langfuse-style)
For providers that expose a LangChain `BaseCallbackHandler`. Return your callback list from `get_callbacks()` and the engine attaches them to every agent invocation:
```python langfuse_like.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
from langfuse.callback import CallbackHandler
from idun_agent_engine.observability.base import ObservabilityHandlerBase
class LangfuseLikeHandler(ObservabilityHandlerBase):
provider = "langfuse"
def __init__(self, options: dict | None = None) -> None:
options = options or {}
host = self._resolve_env(options.get("host"))
public_key = self._resolve_env(options.get("public_key"))
secret_key = self._resolve_env(options.get("secret_key"))
self._callbacks: list = []
try:
self._callbacks.append(
CallbackHandler(
host=host,
public_key=public_key,
secret_key=secret_key,
)
)
except Exception:
# Init failure should not block agent boot. Log and continue.
pass
def get_callbacks(self) -> list:
return self._callbacks
```
Use this pattern for: Langfuse, LangSmith via the LangChain integration, Helicone, Comet Opik.
### Pattern B: Global OpenTelemetry instrumentation (Phoenix-style)
For providers that hook into the global OpenTelemetry SDK. Configure your tracer provider and exporter inside `__init__`, then return an empty list from `get_callbacks()`: the agent runtime will be auto-instrumented globally:
```python phoenix_like.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from openinference.instrumentation.langchain import LangChainInstrumentor
from idun_agent_engine.observability.base import ObservabilityHandlerBase
class OTLPHandler(ObservabilityHandlerBase):
provider = "otlp"
def __init__(self, options: dict | None = None) -> None:
options = options or {}
endpoint = self._resolve_env(options.get("endpoint"))
api_key = self._resolve_env(options.get("api_key"))
provider = TracerProvider()
exporter = OTLPSpanExporter(
endpoint=endpoint,
headers={"authorization": f"Bearer {api_key}"} if api_key else None,
)
provider.add_span_processor(BatchSpanProcessor(exporter))
# LangChainInstrumentor wires the global provider for LangGraph / LangChain
LangChainInstrumentor().instrument(tracer_provider=provider)
def get_callbacks(self) -> list:
return []
```
Use this pattern for: any OTLP-compatible backend (Honeycomb, Tempo, Jaeger, Datadog OTLP, NewRelic OTLP), GCP Trace, Arize Phoenix.
## The `_resolve_env` helper
The canonical implementation lives in `idun_agent_schema.engine.observability`. Each shipping handler imports it and re-exposes it as a `staticmethod` for convenience:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_schema.engine.observability import _resolve_env
class MyHandler(ObservabilityHandlerBase):
provider = "my_provider"
@staticmethod
def _resolve_env(value: str | None) -> str | None:
return _resolve_env(value)
```
Import it from the schema rather than copying the body, so future changes (e.g. expanded env-var syntax) propagate to your handler automatically.
The helper lets users keep secrets in env vars and reference them from `config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "OTLP"
enabled: true
config:
endpoint: "${OTLP_ENDPOINT}"
api_key: "${OTLP_API_KEY}"
```
## Init failures should not crash boot
Following the engine's CLAUDE.md guidance: telemetry must never alter command or runtime semantics. Wrap your handler's init in `try/except Exception` and log the failure. The engine continues without the failed provider rather than aborting the agent boot.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def __init__(self, options: dict | None = None) -> None:
try:
# ... init client ...
self._callbacks = [build_callback()]
except Exception as exc:
logger.exception("Failed to init my observability provider: %s", exc)
self._callbacks = []
```
## Wiring the handler into the engine
Until a plugin API ships, register your handler in two places.
### 1. Add an enum value
In your fork of `idun_agent_schema`, extend `ObservabilityProvider`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class ObservabilityProvider(str, Enum):
LANGFUSE = "LANGFUSE"
PHOENIX = "PHOENIX"
LANGSMITH = "LANGSMITH"
GCP_TRACE = "GCP_TRACE"
GCP_LOGGING = "GCP_LOGGING"
OTLP = "OTLP" # add this
```
### 2. Add a branch in `create_observability_handler`
In `libs/idun_agent_engine/src/idun_agent_engine/observability/base.py`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
elif provider_upper == ObservabilityProvider.OTLP:
handler = OTLPHandler(options)
return handler, {"enabled": True, "provider": provider}
```
Then in your `config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "OTLP"
enabled: true
config:
endpoint: "https://otlp.example.com/v1/traces"
api_key: "${OTLP_API_KEY}"
```
## The local trace pipeline (standalone)
The standalone runtime ships its own local trace store backed by SQLite or Postgres. It uses the same OpenTelemetry SpanProcessor hook as Pattern B, registered via `attach_span_processor()` from `idun_agent_engine.observability.otel_lifecycle`. Your handler does not need to do anything for the local pipeline to work: it runs alongside whatever provider the user has configured.
If you want your handler to *interoperate* with the local pipeline (e.g. share the same TracerProvider), call `attach_span_processor()` rather than instantiating your own `TracerProvider`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from idun_agent_engine.observability.otel_lifecycle import init_otel, attach_span_processor
def __init__(self, options: dict | None = None) -> None:
init_otel() # idempotent
attach_span_processor(BatchSpanProcessor(OTLPSpanExporter(...)))
```
## Reference handlers
The five shipping handlers cover both patterns. Read them in this order:
* **Langfuse** ([`observability/langfuse/langfuse_handler.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/observability/langfuse/langfuse_handler.py)): simplest Pattern A handler. Env-var resolution, lazy client init, auth check.
* **Phoenix** ([`observability/phoenix/phoenix_handler.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/observability/phoenix/phoenix_handler.py)): simplest Pattern B handler. `phoenix.otel.register()` + `LangChainInstrumentor().instrument()`.
* **GCP Trace** ([`observability/gcp_trace/`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/observability/gcp_trace/)): Pattern B with multiple instrumentors (LangChain, Guardrails, VertexAI, MCP).
* **LangSmith** ([`observability/langsmith/`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_engine/src/idun_agent_engine/observability/langsmith/)): env-var-only handler that returns no callbacks; LangChain auto-traces from `LANGSMITH_*` env vars.
## What's next
Built-in providers and configuration.
The same extension pattern for agent frameworks.
Diagnosing observability init failures in the log.
# Google Cloud Logging
Source: https://docs.idun-group.com/observability/gcp-logging
Set up Google Cloud Logging to capture structured logs from your agents.
Set up Google Cloud Logging to send structured logs from your agents to Google Cloud for centralized log management.
Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine.
## Set up GCP Logging observability
1. Create a Google Cloud project if you do not have one
2. Enable the **Cloud Logging API** in your project
3. Verify that the environment where your agent runs has credentials with permission to write logs (the `Logs Writer` role)
Open the running standalone at `/admin/observability/` and click **GCP Logging**. Fill in:
* **GCP project ID**: Your Google Cloud project ID
* **Region**: (Optional) Region/zone associated with the resource (e.g., `us-central1`)
* **Log name**: Identifier for the log stream (e.g., `application-log`)
* **Resource type**: Resource type label (e.g., `global`, `gce_instance`, `cloud_run_revision`)
* **Severity**: Minimum level to record (`INFO`, `WARNING`, `ERROR`, `CRITICAL`)
Save the form. The reload pipeline re-instantiates the engine with the new observability config; subsequent agent runs ship structured logs to GCP.
Alternatively, configure GCP Logging in your `config.yaml` for first-boot seeding or engine-only mode:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "GCP_LOGGING"
enabled: true
config:
project_id: "my-gcp-project"
log_name: "idun-agent-logs"
resource_type: "cloud_run_revision"
severity: "INFO"
```
## View log data
Once your agent is running with observability enabled:
1. Interact with your agent through the chat UI at `/` or the API at `/agent/run`
2. Open the [Google Cloud Console](https://console.cloud.google.com/)
3. Navigate to **Logging** > **Logs Explorer**
Filter by the **Log Name** you configured to see your agent's logs.
## Best practices
* **Use structured logging**: The agent platform sends structured logs, which are easier to query in GCP
* **Set appropriate severity**: Filter out debug noise in production by setting severity to `INFO` or `WARNING`
## Troubleshooting
1. **Check permissions**: Verify the `Logs Writer` role is assigned to the agent's service account
2. **Check log name**: Confirm you are filtering by the correct log name in Logs Explorer
## Next steps
Add distributed tracing alongside structured logs in Google Cloud.
See the same agent runs in the bundled admin UI without GCP setup.
Run the standalone next to your logs with managed Postgres.
# Google Cloud Trace
Source: https://docs.idun-group.com/observability/gcp-trace
Set up Google Cloud Trace to capture distributed traces and analyze agent latency.
Set up Google Cloud Trace to capture distributed traces from your agents and analyze latency in the Google Cloud Console.
Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine.
## Set up GCP Trace observability
1. Create a Google Cloud project if you do not have one
2. Enable the **Cloud Trace API** in your project
3. Verify that the environment where your agent runs has credentials with permission to write traces (the `Cloud Trace Agent` role)
If running on Cloud Run, GKE, or Compute Engine, the default service account typically has the required permissions if scopes are configured.
Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to your service account key file, or configure application default credentials.
1. On the Idun Engine main page, navigate to **Observability**
2. Click **Add configuration**
3. Select **GCP Trace**
4. Enter a **Configuration Name** (for example, "GCP Trace Prod")
5. Fill in the required details:
* **Project ID**: Your Google Cloud Project ID (for example, `my-project-123`)
* **Region**: (Optional) Specific region if applicable
* **Trace Name**: (Optional) Name for the trace session
* **Sampling Rate**: A number between 0.0 and 1.0 (for example, `1.0` for 100% sampling)
* **Flush Interval**: Time in seconds to wait before sending traces (default `5`)
* **Ignore URLs**: (Optional) Paths to exclude from tracing
6. Click **Create configuration**
1. Navigate to the agent you want to trace
2. Click **Edit Agent**
3. Click **Next** to reach the observability configuration step
4. Select **GCP Trace** as the observability provider
5. Select the configuration you created
6. Click **Next**
7. Click **Save changes** to finalize
After saving, click **Restart** on your agent page to reload the configuration.
## View trace data
Once your agent is running with observability enabled:
1. Interact with your agent
2. Open the [Google Cloud Console](https://console.cloud.google.com/)
3. Navigate to **Trace** > **Trace List**
You will see traces from your agent's execution. Use these to analyze latency and find bottlenecks.
## Best practices
* **Adjust sampling rate**: For high-traffic agents, lower the sampling rate to reduce costs and noise
* **Use meaningful trace names**: This helps when filtering traces in the GCP Console
## Troubleshooting
1. **Check API**: Verify the Cloud Trace API is enabled in your project
2. **Check permissions**: Confirm the agent's service account has the `Cloud Trace Agent` role
3. **Wait for flush**: Traces are sent in batches. Wait a few seconds (based on your flush interval) after execution before checking the console
## Next steps
Pair tracing with structured logs in Google Cloud Logging.
Inspect the same agent runs in the bundled admin UI.
Run the standalone next to your traces with managed Postgres.
# Langfuse
Source: https://docs.idun-group.com/observability/langfuse
Set up Langfuse observability to monitor, trace, and debug your agents in real time.
Set up Langfuse to trace agent execution and analyze performance in your Langfuse dashboard.
Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine.
## Set up Langfuse observability
If you do not have Langfuse API keys yet:
1. Go to [Langfuse Cloud](https://cloud.langfuse.com) or your [self-hosted](https://langfuse.com/self-hosting) instance
2. Sign up or log in to your account
3. Create a new project or select an existing one
4. Navigate to **Settings** > **API Keys**
5. Click **Create New API Key**
6. Copy the **Public Key** and **Secret Key**
Open the running standalone at `/admin/observability/` and click **Langfuse**. Fill in:
* **Host**: Your Langfuse instance URL (e.g., `https://cloud.langfuse.com` or your self-hosted URL)
* **Public key**: Langfuse public key (starts with `pk-lf-...`)
* **Secret key**: Langfuse secret key (starts with `sk-lf-...`)
* **Run name**: (Optional) Display label for traces in Langfuse (e.g., `my-agent`)
Save the form. The reload pipeline re-instantiates the engine; the next agent run starts streaming traces to Langfuse. Use the **Verify** button to test the connection.
Keep your secret key secure. Do not commit it to version control or share it publicly.
Alternatively, configure Langfuse in your `config.yaml` for first-boot seeding or engine-only mode:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "LANGFUSE"
enabled: true
config:
host: "https://cloud.langfuse.com"
public_key: "${LANGFUSE_PUBLIC_KEY}"
secret_key: "${LANGFUSE_SECRET_KEY}"
run_name: "my-agent"
```
## View observability data
Once your agent is running with observability enabled:
1. Interact with your agent through the chat UI at `/` or the API at `/agent/run`
2. Open your Langfuse dashboard at [cloud.langfuse.com](https://cloud.langfuse.com)
3. Navigate to your project to view traces
You will see traces showing:
* Agent execution flow
* LLM calls and responses
* Tool usage and results
* Execution time and costs
* Error traces and debugging information
## Best practices
* Use descriptive names for observability configurations to identify them when managing multiple agents
* Enable observability during development to catch issues early
* Monitor costs through your Langfuse dashboard to track token usage
## Troubleshooting
ADK does not currently support simultaneous tracing with both Langfuse and GCP tracing. If you need this feature, open an issue on [GitHub](https://github.com/Idun-Group/idun-agent-platform/issues) or join the [Discord server](https://discord.gg/KCZ6nW2jQe).
1. **Check API keys**: Verify that your public and secret keys are correct
2. **Verify host URL**: Confirm the URL is accessible and correctly formatted
3. **Check agent logs**: Look for connection errors in the agent runtime logs
4. **Test connectivity**: Verify your agent can reach the Langfuse host
## Next steps
Inspect every agent run in the bundled admin UI alongside Langfuse.
Compare built-in providers and their configuration shapes.
Write a handler for any provider not on the shipping list.
# LangSmith
Source: https://docs.idun-group.com/observability/langsmith
Set up LangSmith observability to debug, test, evaluate, and monitor your agents.
Set up LangSmith to debug, trace, and monitor your agents from the LangSmith dashboard.
Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine.
## Set up LangSmith observability
If you do not have a LangSmith API key yet:
1. Go to [LangSmith](https://smith.langchain.com/)
2. Sign up or log in to your account
3. Navigate to **Settings** (gear icon) > **API Keys**
4. Click **Create API Key**
5. Copy your **API Key**
Open the running standalone at `/admin/observability/` and click **LangSmith**. Fill in:
* **API Key**: Your LangSmith API key (starts with `lsv2-...`)
* **Project Name**: The name of the project in LangSmith (for example, `default` or `prod-agent`)
* **Endpoint**: (Optional) Custom endpoint if you are self-hosting LangSmith (defaults to `https://api.smith.langchain.com`)
* **Run Name**: (Optional) Display name for each trace run in LangSmith (for example, `my-agent`)
Save the form. The reload pipeline re-instantiates the engine with the new observability config; the next agent run starts streaming spans to LangSmith.
Keep your API key secure. Do not commit it to version control or share it publicly.
Alternatively, configure LangSmith in your `config.yaml` for first-boot seeding or engine-only mode:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "LANGSMITH"
enabled: true
config:
api_key: "${LANGSMITH_API_KEY}"
project_name: "prod-agent"
run_name: "my-agent"
```
## View observability data
Once your agent is running with observability enabled:
1. Interact with your agent through the chat UI at `/` or the API at `/agent/run`
2. Open your LangSmith dashboard at [smith.langchain.com](https://smith.langchain.com/)
3. Navigate to your project to view traces
You will see traces showing the execution run tree, LLM inputs/outputs, and latency. Each trace run is named after your agent by default.
## Best practices
* Use distinct projects for development and production environments
* Tag runs to filter traces when investigating specific issues (configured within agent logic)
* Review error traces in LangSmith to identify and resolve problems quickly
## Troubleshooting
1. **Check API key**: Verify it is valid and has the required permissions
2. **Verify project name**: Traces are sent to the "default" project if the project name is not specified or is incorrect
3. **Check tracing toggle**: Confirm the tracing toggle is enabled in the configuration
## Next steps
Inspect every agent run in the bundled admin UI alongside LangSmith.
Compare built-in providers and their configuration shapes.
Write a handler for any provider not on the shipping list.
# Observability
Source: https://docs.idun-group.com/observability/overview
Monitor, trace, and debug your agents with built-in integrations for popular observability platforms.
Idun Engine includes built-in observability backed by OpenTelemetry auto-instrumentation. It captures traces, logs, and metrics from your agents with minimal configuration.
The standalone runtime always captures traces locally into its own DB-backed [trace store](/observability/traces). External providers (Langfuse, Phoenix, LangSmith, GCP Trace) stack on top: configure one or more in the admin panel or `config.yaml`, and the engine fans spans out to local storage and every enabled provider.
## Supported providers
Open-source observability and analytics for LLM applications. Self-hosted or cloud.
AI observability for tracing, evaluation, and troubleshooting. Cloud or self-hosted.
Debugging, testing, evaluating, and monitoring for LangChain-based agents.
Distributed tracing to find latency bottlenecks in Google Cloud environments.
Structured log management and analysis in Google Cloud.
## How it works
When you attach an observability configuration to an agent, the platform automatically instruments the agent runtime. Depending on the provider, you get:
* **Traces** showing agent execution flow, LLM calls, and tool invocations
* **Latency metrics** for each step in the agent graph
* **Cost tracking** based on token usage
* **Error traces** with full context for debugging
* **Structured logs** for centralized log analysis
## Configuration
Add an `observability` section to your `config.yaml` with the provider and its credentials:
```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
observability:
- provider: "LANGFUSE"
enabled: true
config:
public_key: "pk-lf-..."
secret_key: "sk-lf-..."
host: "https://cloud.langfuse.com"
```
The `observability` key is a list, and you can attach more than one provider at a time; each entry is lazy-loaded and the engine fans spans out to every enabled provider. See the provider-specific pages for the full list of fields each provider requires.
Navigate to `/admin/observability/` in the running standalone. The catalog shows the supported providers: Langfuse, Arize Phoenix, LangSmith, GCP Trace, GCP Logging.
Click the provider you want and fill in the credentials. The reload pipeline re-instantiates the engine with the new observability config when you save.
## Probing the connection programmatically
The configured observability provider can be smoke-tested without going through the admin UI. `POST /admin/api/v1/observability/check-connection` runs the same probe the admin Test-connection button uses and returns a `StandaloneConnectionCheck`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -sX POST http://localhost:8000/admin/api/v1/observability/check-connection \
--cookie "$IDUN_SESSION_COOKIE"
```
Langfuse, Phoenix, and LangSmith providers get an HTTP HEAD (falling back to GET on `>= 400`) against their configured endpoint; success means an HTTP status under 500:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"ok": true,
"details": { "provider": "LANGFUSE", "host": "https://cloud.langfuse.com", "status": 200 },
"error": null
}
```
GCP\_TRACE and GCP\_LOGGING return `ok: true` with a `details.note` flagging that the runtime auth check needs GCP credentials and was not attempted; the probe only validates that `project_id` is set:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"ok": true,
"details": {
"provider": "GCP_TRACE",
"projectId": "my-project",
"note": "config valid; runtime auth check requires GCP credentials"
},
"error": null
}
```
The probe returns HTTP 404 when no provider is configured. Otherwise every outcome is HTTP 200 with `ok` reflecting reachability, runs under a 5-second timeout, and never raises (`services/connection_checks.py`).
The route is admin-authenticated: under `IDUN_ADMIN_AUTH_MODE=password` you need the session cookie minted by `POST /admin/api/v1/auth/login`. Use this in pre-deploy gates to confirm credentials still resolve before flipping traffic.
## Next steps
Browse, search, and inspect AG-UI run events captured by the standalone's trace store.
The OpenTelemetry event shape the engine emits.
Wire your own span handler when the built-in providers don't fit.
# Telemetry events
Source: https://docs.idun-group.com/observability/telemetry-events
Canonical list of browser-side PostHog events emitted by the Idun standalone UI.
The Idun standalone UI emits the events below through PostHog. The single off-switch is `IDUN_TELEMETRY_ENABLED=false`, which also kills Python engine telemetry.
## Stability policy
Event names are a public contract. Renaming or removing an event goes through one release with both the old and new names emitted, so downstream PostHog dashboards and queries have time to migrate. The release notes call out the deprecation; the old name is removed in the following release.
If you need to ship a breaking change faster than that, raise it explicitly in the PR description.
## Events
| Event | Where it fires | Properties |
| ------------------------------------------- | ----------------------------------------- | ------------------------------------------------------ |
| `$pageview` / `$autocapture` / `$pageleave` | Built-in PostHog autocapture | n/a |
| `auth.login.start` | Login form submit or OIDC redirect button | `method`, `provider?` |
| `auth.login.success` | OAuth callback or basic-auth response | `method`, `provider?`, `duration_ms` |
| `auth.login.failure` | OAuth callback or basic-auth response | `method`, `provider?`, `duration_ms`, `error_class` |
| `auth.logout` | Topbar logout button | `method` |
| `agent.config.saved` | Admin Save handlers | `agent_id`, `section`, `duration_ms`, `result` |
| `agent.config.reloaded` | Admin Reload button | `agent_id`, `duration_ms`, `result` |
| `agent.run.started` | Chat send | `agent_id`, `session_id`, `message_index` |
| `agent.run.completed` | Stream RUN\_FINISHED | `agent_id`, `session_id`, `duration_ms` |
| `agent.run.error` | Stream error | `agent_id`, `session_id`, `error_class`, `duration_ms` |
| `chat.message.sent` | `ChatInput` submit | `session_id`, `length_chars`, `length_words` |
| `chat.response.received` | First TEXT\_MESSAGE\_CONTENT delta | `session_id`, `time_to_first_token_ms` |
| `chat.error` | Error toast | `session_id`, `error_class`, `recoverable` |
## Properties policy
* Message content is **never** sent. Only counts, durations, and error class names.
* Sensitive keys (`api_key`, `token`, `password`, `secret`, `bearer`, …) are redacted before send.
* String values are truncated at 200 characters.
* `error_class` is the exception constructor name, never the message.
## Super-properties
Attached to every event:
* `deployment_type` — `cloud` / `self-hosted` / `dev`
* `idun_version`
* `surface` — `admin` / `chat`
## Session replay
`posthog-js` records DOM + interactions. Masking is enforced via:
* `maskAllInputs: true` (every ``/`