# 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. SSO admin page ## 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. Chat UI with SSO sign-in 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. <Frame> <img alt="Chat UI welcome state" /> </Frame> Send a message. The chat surface streams the AG-UI response in real time and renders tool calls inline. <Frame> <img alt="Chat UI showing a hello/Hey-what's-up exchange" /> </Frame> 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 <Card title="Programmatic chat" icon="terminal" href="/guides/programmatic-chat"> Hit `/agent/run` directly with the request shape and SSE event stream. </Card> <Card title="Customize the chat UI" icon="paintbrush" href="/standalone/customizing-ui"> Theme, layout, and full UI replacement. </Card> <Card title="Deploy to Cloud Run" icon="cloud-upload" href="/standalone/cloud-run"> Run `idun serve` on Google Cloud Run with a managed container. </Card> # 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 | <Warning> `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. </Warning> ## 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 <Tabs> <Tab title="Behind a reverse proxy"> 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`). </Tab> <Tab title="Cloud Run / containers"> 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. </Tab> </Tabs> ## 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 <Card title="SSO" icon="key" href="/auth/sso"> Require an OIDC JWT on the agent API. </Card> <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting"> What to do when reload fails or the admin panel says `agent_not_ready`. </Card> # 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 <Columns> <Card title="Google Cloud Run" icon="cloud" href="/standalone/cloud-run"> Managed container with HTTPS, autoscaling, and Cloud SQL Postgres. The shortest path to a production deployment. </Card> <Card title="Docker on any host" icon="docker" href="#docker-on-any-host"> `Dockerfile.example` + `cloud-run.example.yaml` adapt to AWS Fargate, Azure Container Apps, GKE, a VM with Docker, or any container host. </Card> <Card title="Engine-only mode" icon="terminal" href="/cli/overview"> 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. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> The minimum production checklist: admin auth, TLS termination, bind address, Postgres, secrets management. </Card> </Columns> ## 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 <Card title="Deploy to Cloud Run" icon="cloud" href="/standalone/cloud-run"> The shortest managed path with HTTPS, autoscaling, and Cloud SQL Postgres. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Lock down admin auth, TLS, secrets, and trace retention before exposing the service. </Card> <Card title="CLI reference" icon="terminal" href="/cli/overview"> Every flag and env var for `idun serve`, `idun setup`, and engine-only mode. </Card> # 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 <Card title="Enterprise SSO" icon="lock" href="/enterprise/sso"> Authenticated identity is the actor field for every log entry. </Card> <Card title="Multi-agent management" icon="layers" href="/enterprise/multi-agent"> See where audit events come from across the fleet. </Card> # 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 <Card title="Enterprise SSO" icon="lock" href="/enterprise/sso"> Wire Okta, Entra ID, or any SAML provider into every agent. </Card> <Card title="RBAC" icon="users" href="/enterprise/rbac"> Control who can see and edit which agents. </Card> # 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 <CardGroup> <Card title="Stop shadow AI" icon="eye-off"> 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. </Card> <Card title="Data sovereignty" icon="globe-lock"> On-prem, your VPC, or air-gapped. Your data never leaves your perimeter. No third-party LLM tenant, no shared inference logs. </Card> <Card title="Audit-ready" icon="badge-check"> Every admin action is captured with actor, timestamp, and diff. Append-only logs, configurable retention up to seven years, SIEM export. </Card> <Card title="No vendor lock-in" icon="git-fork"> Open standards under the hood: LangGraph, ADK, OpenTelemetry, MCP, OIDC. Swap providers any time. Your code stays yours. </Card> </CardGroup> ## Governance, end to end <CardGroup> <Card title="Multi-agent management" icon="layers" href="/enterprise/multi-agent"> Register, monitor, and update every standalone agent from a single control plane. Drift detection across the fleet. </Card> <Card title="Enterprise SSO" icon="lock" href="/enterprise/sso"> Okta, Microsoft Entra ID, Ping, SAML, and OIDC providers wired into every agent. Group-based allowlists with JIT provisioning. </Card> <Card title="Role-based access" icon="users" href="/enterprise/rbac"> Granular permissions across agents, configs, secrets, and the admin surface. Roles can be sourced from IdP groups. </Card> <Card title="Audit logs" icon="scroll-text" href="/enterprise/audit-logs"> Tamper-evident, append-only record of every admin write. Streams to your SIEM. Retention up to seven years. </Card> <Card title="Centralized policy" icon="shield-check"> Push org-wide guardrails, secret rotation policies, model allowlists, and approval workflows from one place to every agent. </Card> <Card title="On-prem deploy" icon="server"> Kubernetes, VMs, or air-gapped clusters. Your infrastructure, your rules. Helm charts and signed images. </Card> </CardGroup> ## 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 <Card title="Book a demo" icon="message-circle" href="https://calendar.app.google/RSzm7EM5VZY8xVnN9"> 30 minutes. We walk through your governance requirements and show what the control plane looks like with your existing standalones plugged in. </Card> # 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 <Card title="Enterprise SSO" icon="lock" href="/enterprise/sso"> Source role assignments from your identity provider. </Card> <Card title="Audit logs" icon="scroll-text" href="/enterprise/audit-logs"> Verify what users with each role actually did. </Card> # 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 <Card title="RBAC" icon="users" href="/enterprise/rbac"> Once users authenticate, control what they can do. </Card> <Card title="Audit logs" icon="scroll-text" href="/enterprise/audit-logs"> Track who signed in and what they changed. </Card> # FAQ Source: https://docs.idun-group.com/faq Frequently asked questions about Idun Engine, covering frameworks, authentication, data storage, guardrails, and licensing. <AccordionGroup> <Accordion title="What agent frameworks are supported?"> 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. </Accordion> <Accordion title="How does authentication work?"> 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. </Accordion> <Accordion title="Does Idun require a database?"> 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. </Accordion> <Accordion title="Where is data stored?"> * **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. </Accordion> <Accordion title="Can I run without the admin DB?"> 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. </Accordion> <Accordion title="How do guardrails work?"> 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. </Accordion> <Accordion title="Engine-only vs standalone, what's the difference?"> | | 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. </Accordion> <Accordion title="Is it free and open source?"> 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. </Accordion> <Accordion title="Does GPLv3 apply to my agent code?"> 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. <Tabs> <Tab title="Your agent's graph_definition"> 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. </Tab> <Tab title="MCP servers and external tools"> 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. </Tab> <Tab title="LLM-generated code"> 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. </Tab> </Tabs> If you need a non-GPL license for commercial redistribution, contact us via [Discord](https://discord.gg/KCZ6nW2jQe) to discuss options. </Accordion> <Accordion title="How do I report a bug or request a feature?"> * **Bugs and feature requests**: [GitHub Issues](https://github.com/Idun-Group/idun-agent-platform/issues) * **Questions and discussions**: [Discord](https://discord.gg/KCZ6nW2jQe) </Accordion> <Accordion title="How does observability work?"> 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. </Accordion> <Accordion title="Can I use multiple MCP servers with one agent?"> 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. </Accordion> </AccordionGroup> # 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. <Tip> Want to start from working code? The [agent templates](/templates) include ADK examples for tool calling and structured I/O. </Tip> ## 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.", ) ``` <Note> If you use Vertex AI models, authenticate with `gcloud auth application-default login` before running the agent. </Note> ## 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 `<file_path>:<variable_name>`. 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. <Warning> ADK does not support folder paths that contain spaces. Make sure your project directory path has no spaces. </Warning> ## Next steps <Card title="Memory and session details for ADK" icon="database" href="/memory/adk"> Configure session and memory services across backends. </Card> <Card title="Guardrails" icon="shield" href="/guardrails/overview"> Add safety guards to your agent inputs and outputs. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace runs, monitor latency, and inspect token usage. </Card> <Card title="MCP Servers" icon="plug" href="/mcp-servers/overview"> Connect external tools through the Model Context Protocol. </Card> # 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. <Note> 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. </Note> ## 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 | <Note> `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. </Note> ### 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 <Card title="Custom observability handler" icon="activity" href="/observability/custom-handler"> The same extension pattern for tracing providers. </Card> <Card title="SSO" icon="lock" href="/auth/sso"> JWT validation works for any adapter. </Card> <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting"> Common boot failures. </Card> # 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. <Tip> Want to start from working code? The [agent templates](/templates) include 7 LangGraph examples covering tool calling, structured I/O, and multi-step workflows. </Tip> ## 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 `<file_path>:<variable_name>`. 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 <Note> **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. </Note> <Note> **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. </Note> <Note> **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. </Note> ## Next steps <Card title="Memory and checkpointing details" icon="database" href="/memory/langgraph"> Backend options and configuration for persistent state. </Card> <Card title="Guardrails" icon="shield" href="/guardrails/overview"> Add safety guards to your agent inputs and outputs. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace runs, monitor latency, and inspect token usage. </Card> <Card title="MCP Servers" icon="plug" href="/mcp-servers/overview"> Connect external tools through the Model Context Protocol. </Card> # 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 <Columns> <Card title="LangGraph" icon="layers" href="/frameworks/langgraph"> 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. </Card> <Card title="Google ADK" icon="google" href="/frameworks/adk"> Google's Agent Development Kit for Gemini-powered agents. Supports session and memory services through in-memory, Vertex AI, or database backends. </Card> <Card title="Deep Agents" icon="boxes" href="/guides/deepagents-with-idun"> 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). </Card> </Columns> ### 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) | <Note> Haystack support was removed in 0.6.0. Existing Haystack agent configs need to be ported to LangGraph or ADK before upgrading. </Note> ## 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 <Card title="LangGraph integration guide" icon="layers" href="/frameworks/langgraph"> Graph-based agents with full AG-UI streaming, checkpointing, and CopilotKit support. </Card> <Card title="Google ADK integration guide" icon="bot" href="/frameworks/adk"> Google's Agent Development Kit for Gemini-powered agents with session and memory services. </Card> <Card title="Custom adapter" icon="plug" href="/frameworks/custom-adapter"> Build your own adapter to bring a different agent framework into Idun Engine. </Card> <Card title="Configuration reference" icon="file-text" href="/configuration"> Full schema for `config.yaml` including agent, guardrails, MCP, observability, and more. </Card> # 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 <Card title="Quickstart" icon="rocket" href="/quickstart"> Deploy your first agent in under 30 minutes </Card> <Card title="Architecture" icon="layers" href="/architecture"> How the engine, standalone, and schema connect </Card> # 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 <Tabs> <Tab title="Config file"> 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. </Tab> <Tab title="Admin UI"> <Steps> <Step title="Open the guardrails admin page"> Navigate to `/admin/guardrails/` in the running standalone. The catalog at the top groups guards by category; configured guards are listed below. <Frame> <img alt="Guardrails admin page" /> </Frame> </Step> <Step title="Create a guardrail"> 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. <Frame> <img alt="Add a Ban List guardrail" /> </Frame> </Step> <Step title="Save"> 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. </Step> </Steps> <Note> Some guards are marked "Soon" and not yet available: Code Scanner, Jailbreak, Prompt Injection, Model Armor, Custom LLM, and RAG Hallucination. </Note> </Tab> </Tabs> <Warning> 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). </Warning> ## 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`: <Tabs> <Tab title="Input guardrails"> ```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"] ``` </Tab> <Tab title="Output guardrails"> ```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 ``` </Tab> <Tab title="Both positions"> ```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 ``` </Tab> </Tabs> 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. <CodeGroup> ```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?"}' ``` </CodeGroup> 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 <Card title="Guardrails reference" icon="file-text" href="/guardrails/reference"> All 15 guardrail types and their configuration fields. </Card> <Card title="Observability" icon="chart-line" href="/observability/overview"> Monitor guardrail activity in traces. </Card> <Card title="Deployment" icon="cloud" href="/deployment/overview"> Deploy your agent to Cloud Run, a VM, or your laptop. </Card> # 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 ``` <Note> Model Armor requires a Google Cloud project with the Model Armor API enabled. This guardrail type does not use the Guardrails AI hub. </Note> ### 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." ``` <Note> Custom LLM guardrails use a separate LLM call for evaluation. This adds latency and cost to each guarded request. </Note> ## 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 <Card title="Guardrails overview" icon="shield" href="/guardrails/overview"> How guardrails fit into the agent request lifecycle. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace guardrail decisions alongside agent runs. </Card> <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting"> Diagnose configuration and provider errors. </Card> # 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 ``` <Note> 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. </Note> ## 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`: `"<path>:<variable>"` for LangGraph. For ADK, the equivalent field is `agent.config.agent`. ### `graph_definition` format The string is parsed as `<path_or_module>:<variable_name>`. 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 `<g>.compile()` where `<g>` 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 <Card title="Production hardening" icon="shield-check" href="/deployment/hardening"> before exposing this beyond localhost </Card> <Card title="Guardrails overview" icon="shield-halved" href="/guardrails/overview"> add input and output guards </Card> <Card title="Observability overview" icon="chart-line" href="/observability/overview"> wire Langfuse, Phoenix, or LangSmith </Card> <Card title="MCP Servers" icon="plug" href="/mcp-servers/overview"> attach MCP servers </Card> <Card title="Troubleshooting" icon="circle-help" href="/troubleshooting"> graph load errors, reload failures </Card> # 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 <Steps> <Step title="Discover the agent"> Clone the Deep Agents example, run `idun init`, point the wizard at `agent.py`. </Step> <Step title="Chat with it"> Ask natural-language questions, watch the planning + SQL tool calls stream live. </Step> <Step title="Persist conversations"> Switch the LangGraph checkpointer to SQLite — full chat history with one click. </Step> <Step title="See what's happening"> Use the built-in dashboard and trace viewer, then plug Langfuse on top. </Step> <Step title="Plug in a Google Workspace MCP"> Connect Gmail / Drive / Calendar through MCP and have the agent email the results. </Step> </Steps> ## 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 <Steps> <Step title="Clone the Deep Agents example"> 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. </Step> <Step title="Set up Idun"> 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 ``` <Note> `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. </Note> </Step> <Step title="`idun init` — the discover flow"> 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. </Step> <Step title="Verify the wired-up agent"> 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. <Frame> <img alt="Agent configuration — healthy, LangGraph, agent.py:agent" /> </Frame> 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. <Frame> <img alt="Agent graph — Deep Agent middleware + model + tools" /> </Frame> </Step> <Step title="Chat with the Deep Agent"> 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. <Frame> <img alt="Chat — Deep Agent reasoning, 8 steps" /> </Frame> 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. <Frame> <img alt="Chat — final formatted answer" /> </Frame> 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. </Step> <Step title="Persist conversations with SQLite"> 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. <Frame> <img alt="Memory — SQLite backend selected" /> </Frame> Back in the chat, every previous turn now lives in the **History** sidebar. New threads, named after their first message, persist across restarts. <Frame> <img alt="Chat — history sidebar with past conversations" /> </Frame> For multi-replica production deployments, swap SQLite for PostgreSQL the same way — same UI, same one-click reload. </Step> <Step title="Built-in observability"> 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. <Frame> <img alt="Built-in dashboard — requests, latency, error rate, cost" /> </Frame> The **Traces** page lists every run with model, tokens, cost, and status — filter by model, status, user, or session. <Frame> <img alt="Traces — per-run list with tokens and cost" /> </Frame> 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. <Frame> <img alt="Trace detail — tree view" /> </Frame> Switch to **Waterfall** for the time-ordered view — which span ran in parallel, which blocked, where the latency lives. <Frame> <img alt="Trace detail — waterfall view" /> </Frame> This works out of the box. Nothing to configure. </Step> <Step title="Add Langfuse on top"> 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**. <Frame> <img alt="Configure Langfuse from the admin" /> </Frame> 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. <Frame> <img alt="Langfuse trace of the same Deep Agent run" /> </Frame> <Note> 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. </Note> </Step> <Step title="Plug in the Google Workspace MCP"> 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. <Frame> <img alt="MCP servers — google-workspace registered" /> </Frame> 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. <Frame> <img alt="Tools — google-workspace, 120 tools discovered" /> </Frame> ### 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. <Frame> <img alt="Chat — agent creating the doc and sending the email" /> </Frame> 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. <Frame> <img alt="Email delivered with Google Doc attachment" /> </Frame> 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. </Step> </Steps> ## 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 <Card title="Lock down access" icon="lock" href="/auth/sso"> turn off `IDUN_ALLOW_OPEN_ADMIN`, add SSO/OIDC, generate API keys for `/agent/run`. </Card> <Card title="Swap SQLite for PostgreSQL" icon="database" href="/memory/overview"> when you go multi-replica — same Memory page, same one-click reload. </Card> <Card title="Add input guardrails" icon="shield-halved" href="/guardrails/reference"> (PII, prompt injection) before exposing the chat publicly. </Card> <Card title="Deploy to Cloud Run" icon="cloud" href="/standalone/cloud-run"> with the provided Dockerfile. </Card> 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. <Steps> <Step title="Register the MCP tools"> Open `/admin/mcp/`. Add four MCP servers. The agent will discover every tool they advertise on the next reload. <Frame> <img alt="/admin/mcp/ with four MCP servers configured" /> </Frame> | 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: <Frame> <img alt="Tool discovery probe for the atlassian MCP server" /> </Frame> </Step> <Step title="Write the system prompt"> 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. <Frame> <img alt="/admin/prompts/ with the system_prompt drafted" /> </Frame> 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. </Step> <Step title="Configure memory (SQLite)"> 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. <Frame> <img alt="/admin/memory/ with SQLite selected" /> </Frame> 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. </Step> <Step title="Wire up Langfuse observability"> Open `/admin/observability/` and click Langfuse. <Frame> <img alt="/admin/observability/ with Langfuse configured" /> </Frame> 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. </Step> <Step title="Lock down `/agent/*` with SSO (internal use only)"> 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. <Frame> <img alt="/admin/sso/ with Google OIDC and an idun-group.com allowlist" /> </Frame> 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`. </Step> </Steps> ## 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? <Frame> <img alt="Chat UI showing the multi-tool exchange" /> </Frame> 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: <Frame> <img alt="Gmail showing the summary email with the Google Doc attached" /> </Frame> 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. <Frame> <img alt="Admin dashboard with activity from the agent run" /> </Frame> 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. <Frame> <img alt="Trace detail for an idun-assistant run" /> </Frame> 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. <Frame> <img alt="Trace waterfall for the same idun-assistant run" /> </Frame> See [Local trace store](/observability/traces) for the trace UI reference. ## What's next <Card title="MCP Servers" icon="plug" href="/mcp-servers/overview"> the full transport reference and how to register your own MCP server. </Card> <Card title="Prompts" icon="file-text" href="/prompts"> versioning, `get_prompt()` resolution, and the admin REST API. </Card> <Card title="Memory" icon="database" href="/memory/overview"> when to swap SQLite for Postgres. </Card> <Card title="Local trace store" icon="activity" href="/observability/traces"> the bundled span-tree viewer at `/admin/traces/`. </Card> <Card title="SSO" icon="lock" href="/auth/sso"> provider presets, allowed\_domains + allowed\_emails, and how validation works. </Card> # 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: <CardGroup> <Card title="No API layer" icon="server"> No built-in HTTP server. You write FastAPI routes, CORS, request parsing, and streaming yourself. LangServe is deprecated. LangGraph Platform requires a LangSmith account. </Card> <Card title="No conversation memory" icon="database"> LangGraph supports checkpointers, but you wire up database connections, async lifecycle, and thread ID routing from HTTP requests yourself. </Card> <Card title="No guardrails" icon="shield"> Your agent will process PII, jailbreak attempts, and toxic content unless you build input/output validation from scratch. </Card> <Card title="No observability" icon="chart-line"> When a production agent returns garbage at 3am, you need traces. LangGraph has no built-in tracing. You instrument it yourself. </Card> </CardGroup> 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. <Note> 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. </Note> ## What you will build <Steps> <Step title="Streaming API"> A LangGraph agent served as a REST + AG-UI streaming endpoint. </Step> <Step title="Conversation memory"> In-memory persistence, upgradeable to PostgreSQL or SQLite. </Step> <Step title="Input guardrails"> PII detection that blocks requests before they reach the agent. </Step> <Step title="Observability"> Langfuse tracing on every invocation with full LLM call details. </Step> </Steps> ## Prerequisites * Python 3.12+ * A Gemini API key (or any LangChain-compatible LLM) * 5 minutes <Steps> <Step title="Write your LangGraph agent"> 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. </Step> <Step title="Set up Idun and run the agent"> 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`. </Step> <Step title="Test the agent"> 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. </Step> <Step title="Add your first guardrail"> 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. </Step> <Step title="Add observability with Langfuse"> 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. <Frame> <img alt="Langfuse trace showing LangGraph agent with tool calls" /> </Frame> 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). </Step> <Step title="Upgrade to PostgreSQL memory"> 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" ``` </Step> <Step title="Connect a frontend"> 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). </Step> </Steps> ## 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: <Card title="Add MCP tool servers" icon="plug" href="/mcp-servers/overview"> 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/`. </Card> <Card title="Add SSO/OIDC protection" icon="lock" href="/auth/sso"> to require JWT authentication on all agent endpoints. Add an `sso` section with your OIDC issuer and client ID. </Card> <Card title="Connect messaging integrations" icon="message-square" href="/integrations/overview"> (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/`. </Card> <Card title="Manage prompts" icon="file-text" href="/prompts"> with versioning and Jinja2 variables through the admin panel at `/admin/prompts/` or in your YAML. </Card> 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. <CardGroup> <Card title="Quickstart" icon="zap" href="/quickstart"> Deploy your first agent in minutes </Card> <Card title="LangGraph production deployment" icon="layers" href="/guides/langgraph-production-deployment"> Take a LangGraph agent from notebook to production API </Card> <Card title="Building a copilot (idun-assistant)" icon="messages-square" href="/guides/idun-assistant-copilot"> A real internal copilot in \~180 lines + a few clicks </Card> <Card title="Connect your existing agent" icon="plug" href="/guides/connect-your-agent"> Wrap an existing LangGraph or ADK agent in Idun </Card> <Card title="Deep Agents with Idun" icon="brain-circuit" href="/guides/deepagents-with-idun"> Wrap a LangChain Deep Agent (planner, virtual filesystem, sub-agents) end to end </Card> <Card title="Programmatic chat" icon="terminal" href="/guides/programmatic-chat"> Drive your hosted agent from a script via /agent/run </Card> <Card title="Pick a framework" icon="layers" href="/frameworks/overview"> LangGraph or Google ADK adapter reference </Card> <Card title="Connect MCP servers" icon="puzzle" href="/mcp-servers/overview"> Extend the agent with external tools over stdio, SSE, HTTP, or WebSocket </Card> <Card title="Add guardrails" icon="shield" href="/guardrails/overview"> Protect your agent with safety guards </Card> <Card title="Set up observability" icon="chart-line" href="/observability/overview"> Trace and monitor agent runs </Card> <Card title="Configure memory" icon="database" href="/memory/overview"> Persist conversation state across sessions </Card> <Card title="Deploy to Cloud Run" icon="cloud" href="/standalone/cloud-run"> Single-container deploy with managed Postgres </Card> </CardGroup> # 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: <json>\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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the integrations admin page"> Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels. </Step> <Step title="Create the Discord integration"> 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 | <Frame> <img alt="Add Discord integration" /> </Frame> </Step> <Step title="Save"> Save the form. The reload pipeline registers the Discord webhook handler on the running engine. </Step> </Steps> </Tab> <Tab title="Config file"> <Steps> <Step title="Create a Discord application"> 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** </Step> <Step title="Create a bot"> 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) </Step> <Step title="Invite the bot to your server"> 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** </Step> <Step title="Configure the integration"> 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 | </Step> <Step title="Set the interactions endpoint URL"> 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://<your-domain>/integrations/discord/webhook ``` Discord sends a PING request to verify the endpoint. The engine handles this automatically. </Step> <Step title="Register a slash command"> 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. <Note> `"type": 3` means a STRING option. The engine extracts this as the query text sent to your agent. </Note> <Tip> 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. </Tip> </Step> <Step title="Test it"> 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 </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Slack" icon="hash" href="/integrations/slack"> Connect your agent to Slack DMs and channels. </Card> <Card title="Microsoft Teams" icon="users" href="/integrations/teams"> Reach the same agent through Bot Framework in Teams. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Secure the engine before exposing webhooks to the public internet. </Card> # 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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the integrations admin page"> Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows the available channels including Google Chat. </Step> <Step title="Create the Google Chat integration"> 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) | </Step> <Step title="Save"> Save the form. The reload pipeline registers the Google Chat webhook handler on the running engine. </Step> </Steps> </Tab> <Tab title="Config file"> <Steps> <Step title="Create a GCP project and enable the Chat API"> 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** </Step> <Step title="Create a service account"> 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 </Step> <Step title="Configure the Chat app"> 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://<your-domain>/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** </Step> <Step title="Configure the integration"> 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) | <Note> You can also store the credentials JSON in an environment variable and reference it in the config to avoid putting secrets in YAML files. </Note> </Step> <Step title="Test it"> 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 </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Slack" icon="hash" href="/integrations/slack"> Connect your agent to Slack DMs and channels. </Card> <Card title="Microsoft Teams" icon="users" href="/integrations/teams"> Reach the same agent through Bot Framework in Teams. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Secure the engine before exposing webhooks to the public internet. </Card> # 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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the integrations admin page"> Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels. </Step> <Step title="Create the Slack integration"> 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 | <Frame> <img alt="Add Slack integration" /> </Frame> </Step> <Step title="Save"> Save the form. The reload pipeline registers the Slack webhook handler on the running engine; the agent now responds to messages forwarded by Slack. </Step> </Steps> </Tab> <Tab title="Config file"> <Steps> <Step title="Create a Slack app"> 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 </Step> <Step title="Get your credentials"> 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-...`) </Step> <Step title="Configure the integration"> 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) | </Step> <Step title="Enable event subscriptions"> 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://<your-domain>/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** <Warning> 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. </Warning> </Step> <Step title="Enable the messages tab"> 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"** </Step> <Step title="Invite the bot to a channel"> 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** </Step> <Step title="Test it"> 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 </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Discord" icon="message-circle" href="/integrations/discord"> Connect your agent to a Discord server via slash commands. </Card> <Card title="Microsoft Teams" icon="users" href="/integrations/teams"> Reach the same agent through Bot Framework in Teams. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Secure the engine before exposing webhooks to the public internet. </Card> # 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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the integrations admin page"> Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels. </Step> <Step title="Create the Teams integration"> 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 | <Frame> <img alt="Add Microsoft Teams integration" /> </Frame> </Step> <Step title="Save"> 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. </Step> </Steps> </Tab> <Tab title="Config file"> <Steps> <Step title="Register an Azure AD application"> 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 </Step> <Step title="Add the Microsoft Bot identity"> 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. </Step> <Step title="Configure the integration"> 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 | <Note> Authentication uses Bot Framework's `ConfigurationBotFrameworkAuthentication` with `MicrosoftAppType=SingleTenant` hardcoded. The integration does not currently support multi-tenant apps. </Note> </Step> <Step title="Create the bot resource"> 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://<your-domain>/integrations/teams/messages ``` </Step> <Step title="Install the bot in Teams"> 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 </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Slack" icon="hash" href="/integrations/slack"> Connect your agent to Slack DMs and channels. </Card> <Card title="Discord" icon="message-circle" href="/integrations/discord"> Reach the same agent through Discord slash commands. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Secure the engine before exposing webhooks to the public internet. </Card> # 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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the integrations admin page"> Navigate to `/admin/integrations/` in the running standalone. The channel catalog shows WhatsApp, Discord, Google Chat, Slack, and Microsoft Teams as active channels. </Step> <Step title="Create the WhatsApp integration"> 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) | </Step> <Step title="Save"> Save the form. The reload pipeline registers the WhatsApp webhook handler on the running engine. </Step> </Steps> </Tab> <Tab title="Config file"> <Steps> <Step title="Create a Meta app"> 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 </Step> <Step title="Get your credentials"> 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 </Step> <Step title="Configure the integration"> 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` | </Step> <Step title="Set up the webhook"> 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://<your-domain>/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 </Step> <Step title="Test it"> 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 <Note> With a test phone number, you can only send messages to numbers registered in the Meta Developer Portal under **WhatsApp > API Setup > Test Numbers**. </Note> </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Slack" icon="hash" href="/integrations/slack"> Connect your agent to Slack DMs and channels. </Card> <Card title="Discord" icon="message-circle" href="/integrations/discord"> Reach the same agent through Discord slash commands. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Secure the engine before exposing webhooks to the public internet. </Card> # 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 <CardGroup> <Card title="Production-ready API" icon="server"> Streaming HTTP service with AG-UI protocol compatibility. Drop it behind any CopilotKit or AG-UI client. </Card> <Card title="Framework adapters" icon="layers"> LangGraph and Google ADK, served with AG-UI streaming. </Card> <Card title="Dashboard" icon="layout-dashboard"> Activity, traces, p50 / p95 latency, error counts, and recent runs at `/admin/`. </Card> <Card title="Guardrails" icon="shield"> 15+ built-in guards powered by Guardrails AI. </Card> <Card title="Observability" icon="chart-line"> Langfuse, Phoenix, LangSmith, or GCP, plus a local trace store with a waterfall viewer at `/admin/traces/`. </Card> <Card title="Memory" icon="database"> In-memory, SQLite, or PostgreSQL checkpointers. </Card> <Card title="MCP tools" icon="wrench"> stdio, SSE, streamable HTTP, or WebSocket with auto-discovery. </Card> <Card title="Prompts" icon="file-text"> Versioned templates with Jinja2 variables. </Card> <Card title="Integrations" icon="plug"> Slack, Discord, Microsoft Teams, Google Chat, and WhatsApp. </Card> <Card title="Auth" icon="lock"> OIDC SSO on `/agent/*` routes; `none` / `password` for the admin panel. </Card> </CardGroup> ## Community <CardGroup> <Card title="Discord" icon="message-circle" href="https://discord.gg/KCZ6nW2jQe"> Questions and help. </Card> <Card title="GitHub Discussions" icon="github" href="https://github.com/Idun-Group/idun-agent-platform/discussions"> Proposals and ideas. </Card> <Card title="GitHub Issues" icon="circle-alert" href="https://github.com/Idun-Group/idun-agent-platform/issues"> Bugs and feature requests. </Card> </CardGroup> ## Next steps <Card title="Quickstart" icon="rocket" href="/quickstart"> Deploy your first agent in under 30 minutes. </Card> <Card title="Architecture" icon="layers" href="/architecture"> How the engine and standalone fit together. </Card> <Card title="Frameworks" icon="bolt" href="/frameworks/overview"> LangGraph and Google ADK adapters. </Card> # 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. <Info> 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. </Info> ## 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 <Steps> <Step title="Configure Docker Desktop"> Open Docker Desktop and verify: 1. Docker Desktop is running 2. The `mcp/fetch` image appears in the **Images** section </Step> <Step title="Register the MCP server"> 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. </Step> <Step title="Integrate MCP tools in your agent code"> Import MCP tools in your agent code: <Tabs> <Tab title="ADK"> ```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, ) ``` </Tab> <Tab title="LangGraph"> ```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.", ) ``` </Tab> </Tabs> `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. </Step> <Step title="Launch the agent"> 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/`. </Step> <Step title="Test MCP integration"> 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. </Step> </Steps> ## 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 <AccordionGroup> <Accordion title="MCP server fails to connect"> **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 </Accordion> <Accordion title="Agent does not use the fetch tool"> **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 <container_id>` * 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` </Accordion> <Accordion title="Args format invalid"> **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"] ``` </Accordion> </AccordionGroup> ## 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 <Card title="MCP Servers overview" icon="plug" href="/mcp-servers/overview"> How the engine discovers and registers MCP tools. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace MCP tool calls alongside agent runs. </Card> <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting"> Diagnose container and transport failures. </Card> # 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. <Tip> The [langgraph-tool-local template](/templates) shows how to mix local tools with MCP tools in a single agent. </Tip> ## 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` | <Note> 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. </Note> ## Configuration example Define MCP servers in your `config.yaml` or through the admin panel: <Tabs> <Tab title="Config file"> ```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()`. </Tab> <Tab title="Admin UI"> <Steps> <Step title="Open the MCP admin page"> 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. <Frame> <img alt="MCP admin page" /> </Frame> </Step> <Step title="Add an MCP server"> 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. </Step> <Step title="Save and verify"> 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. <Frame> <img alt="Tools discovered for the atlassian MCP server" /> </Frame> </Step> </Steps> </Tab> </Tabs> ## Integration approaches <Cards> <Card title="Docker MCP toolkit" icon="docker" href="/mcp-servers/docker-toolkit"> Pre-built MCP servers packaged as Docker containers. Pull, configure, and use without writing server code. </Card> <Card title="Custom MCP servers" icon="code" href="/mcp-servers/docker-toolkit#advanced-configuration"> Host your own MCP servers for custom business logic, proprietary data sources, or internal APIs. </Card> </Cards> ## Framework integration The engine provides helper functions to load MCP tools into your agent code: <CodeGroup> ```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()) ``` </CodeGroup> 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. <Note> `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. </Note> ## Next steps <Card title="Docker MCP toolkit" icon="server" href="/mcp-servers/docker-toolkit"> Pre-built MCP servers packaged as Docker containers. Pull, configure, and use without writing server code. </Card> <Card title="Configuration reference" icon="file-text" href="/configuration"> Full schema for `config.yaml` including the `mcp_servers` block. </Card> # 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 <Steps> <Step title="Navigate to the memory step"> During agent creation or editing, navigate to the **Memory** step in the agent form. </Step> <Step title="Configure your session service"> 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. </Step> <Step title="Configure your memory service"> 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. </Step> <Step title="Save and restart"> Click **Next** to continue, then finalize with **Save changes**. Restart the agent to apply the new configuration. </Step> </Steps> ## 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 <Card title="Google ADK framework" icon="workflow" href="/frameworks/adk"> Configure the ADK adapter and Gemini-powered agents. </Card> <Card title="Guardrails" icon="shield" href="/guardrails/overview"> Add safety guards to your agent inputs and outputs. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace runs, monitor latency, and inspect token usage. </Card> # 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. <Warning> **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. </Warning> 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 <Steps> <Step title="Navigate to the checkpointing step"> During agent creation or editing, navigate to the **Checkpointing** step in the agent form. </Step> <Step title="Choose a backend"> Select the checkpointing backend that matches your deployment needs. See the sections below for details on each option. </Step> <Step title="Fill in connection details"> Enter the required configuration for your chosen backend (file path for SQLite, connection string for PostgreSQL). </Step> <Step title="Save and restart"> Click **Next** to continue, then finalize with **Save changes**. Restart the agent to apply the new configuration. </Step> </Steps> ## 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 <Card title="LangGraph framework" icon="workflow" href="/frameworks/langgraph"> Configure the LangGraph adapter and graph definition. </Card> <Card title="Guardrails" icon="shield" href="/guardrails/overview"> Add safety guards to your agent inputs and outputs. </Card> <Card title="Observability" icon="activity" href="/observability/overview"> Trace runs, monitor latency, and inspect token usage. </Card> # 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 <Cards> <Card title="LangGraph memory" icon="diagram-project" href="/memory/langgraph"> Checkpointing for conversation state persistence. Supports in-memory, SQLite, and PostgreSQL backends. </Card> <Card title="ADK memory" icon="brain" href="/memory/adk"> Session services for conversation state and memory services for long-term knowledge storage. </Card> </Cards> ## 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 <Tabs> <Tab title="Admin UI"> <Steps> <Step title="Open the memory admin page"> Navigate to `/admin/memory/` in the running standalone. The catalog shows the supported backends: SQLite, PostgreSQL, In Memory, Vertex AI, and Database (ADK-only). <Frame> <img alt="Memory admin page" /> </Frame> </Step> <Step title="Configure the backend"> 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. </Step> </Steps> </Tab> <Tab title="Config file"> <Tabs> <Tab title="LangGraph"> ```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}} agent: type: "LANGGRAPH" config: checkpointer: type: "postgres" db_url: "postgresql://user:pass@localhost:5432/dbname" ``` </Tab> <Tab title="ADK"> ```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}} agent: type: "ADK" config: session_service: type: "in_memory" memory_service: type: "in_memory" ``` </Tab> </Tabs> </Tab> </Tabs> ## 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 <Card title="LangGraph memory" icon="layers" href="/memory/langgraph"> Checkpointing for conversation state persistence. Supports in-memory, SQLite, and PostgreSQL backends. </Card> <Card title="ADK memory" icon="database" href="/memory/adk"> Session services for conversation state and memory services for long-term knowledge storage. </Card> # 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. <Tip> Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine. </Tip> ## Set up Phoenix observability <Steps> <Step title="Get your Phoenix details"> <Tabs> <Tab title="Phoenix Cloud"> 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`) </Tab> <Tab title="Self-hosted"> If you are hosting Phoenix yourself, have your collector endpoint URL ready. This is the URL where your Phoenix instance accepts trace data. </Tab> </Tabs> </Step> <Step title="Configure Phoenix in the standalone"> 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 <Frame> <img alt="Observability admin page with Phoenix selected" /> </Frame> Save the form. The reload pipeline re-instantiates the engine with the new observability config; the next agent run starts streaming spans to Phoenix. </Step> </Steps> 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 <Warning> ADK does not currently support simultaneous tracing with multiple providers. </Warning> <AccordionGroup> <Accordion title="Observability not working?"> 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 </Accordion> </AccordionGroup> ## Next steps <Card title="Local trace store" icon="database" href="/observability/traces"> See the same agent runs in the bundled admin UI without extra config. </Card> <Card title="Observability overview" icon="chart-line" href="/observability/overview"> Compare built-in providers and their configuration shapes. </Card> <Card title="Custom handler" icon="puzzle" href="/observability/custom-handler"> Wire a different OTel-compatible backend through Pattern B. </Card> # 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`. <Note> 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. </Note> ## 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 <Card title="Observability overview" icon="chart-line" href="/observability/overview"> Built-in providers and configuration. </Card> <Card title="Custom framework adapter" icon="puzzle" href="/frameworks/custom-adapter"> The same extension pattern for agent frameworks. </Card> <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting"> Diagnosing observability init failures in the log. </Card> # 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. <Tip> Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine. </Tip> ## Set up GCP Logging observability <Steps> <Step title="Prepare your Google Cloud project"> 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) </Step> <Step title="Configure GCP Logging in the standalone"> 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`) <Frame> <img alt="Observability admin page with GCP Logging selected" /> </Frame> Save the form. The reload pipeline re-instantiates the engine with the new observability config; subsequent agent runs ship structured logs to GCP. </Step> </Steps> 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 <AccordionGroup> <Accordion title="Logs not showing up?"> 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 </Accordion> </AccordionGroup> ## Next steps <Card title="GCP Trace" icon="route" href="/observability/gcp-trace"> Add distributed tracing alongside structured logs in Google Cloud. </Card> <Card title="Local trace store" icon="database" href="/observability/traces"> See the same agent runs in the bundled admin UI without GCP setup. </Card> <Card title="Deploy to Cloud Run" icon="cloud" href="/standalone/cloud-run"> Run the standalone next to your logs with managed Postgres. </Card> # 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. <Tip> Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine. </Tip> ## Set up GCP Trace observability <Steps> <Step title="Prepare your Google Cloud project"> 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) <Tabs> <Tab title="Running on Google Cloud"> If running on Cloud Run, GKE, or Compute Engine, the default service account typically has the required permissions if scopes are configured. </Tab> <Tab title="Running locally or elsewhere"> Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to your service account key file, or configure application default credentials. </Tab> </Tabs> </Step> <Step title="Create an observability configuration"> 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** </Step> <Step title="Attach GCP Trace to your agent"> 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. </Step> </Steps> ## 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 <AccordionGroup> <Accordion title="Traces not showing up?"> 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 </Accordion> </AccordionGroup> ## Next steps <Card title="GCP Logging" icon="file-text" href="/observability/gcp-logging"> Pair tracing with structured logs in Google Cloud Logging. </Card> <Card title="Local trace store" icon="database" href="/observability/traces"> Inspect the same agent runs in the bundled admin UI. </Card> <Card title="Deploy to Cloud Run" icon="cloud" href="/standalone/cloud-run"> Run the standalone next to your traces with managed Postgres. </Card> # 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. <Tip> Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine. </Tip> ## Set up Langfuse observability <Steps> <Step title="Get your Langfuse API keys"> 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** </Step> <Step title="Configure Langfuse in the standalone"> 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`) <Frame> <img alt="Observability admin page with Langfuse selected" /> </Frame> 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. <Warning> Keep your secret key secure. Do not commit it to version control or share it publicly. </Warning> </Step> </Steps> 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 <Warning> 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). </Warning> <AccordionGroup> <Accordion title="Observability not working?"> 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 </Accordion> </AccordionGroup> ## Next steps <Card title="Local trace store" icon="database" href="/observability/traces"> Inspect every agent run in the bundled admin UI alongside Langfuse. </Card> <Card title="Observability overview" icon="chart-line" href="/observability/overview"> Compare built-in providers and their configuration shapes. </Card> <Card title="Custom handler" icon="puzzle" href="/observability/custom-handler"> Write a handler for any provider not on the shipping list. </Card> # 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. <Tip> Before starting, complete the [quickstart guide](/quickstart) to have an agent running on Idun Engine. </Tip> ## Set up LangSmith observability <Steps> <Step title="Get your LangSmith API key"> 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** </Step> <Step title="Configure LangSmith in the standalone"> 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`) <Frame> <img alt="Observability admin page with LangSmith selected" /> </Frame> Save the form. The reload pipeline re-instantiates the engine with the new observability config; the next agent run starts streaming spans to LangSmith. <Warning> Keep your API key secure. Do not commit it to version control or share it publicly. </Warning> </Step> </Steps> 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 <AccordionGroup> <Accordion title="Observability not working?"> 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 </Accordion> </AccordionGroup> ## Next steps <Card title="Local trace store" icon="database" href="/observability/traces"> Inspect every agent run in the bundled admin UI alongside LangSmith. </Card> <Card title="Observability overview" icon="chart-line" href="/observability/overview"> Compare built-in providers and their configuration shapes. </Card> <Card title="Custom handler" icon="puzzle" href="/observability/custom-handler"> Write a handler for any provider not on the shipping list. </Card> # 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 <Cards> <Card title="Langfuse" icon="chart-mixed" href="/observability/langfuse"> Open-source observability and analytics for LLM applications. Self-hosted or cloud. </Card> <Card title="Arize Phoenix" icon="fire" href="/observability/arize-phoenix"> AI observability for tracing, evaluation, and troubleshooting. Cloud or self-hosted. </Card> <Card title="LangSmith" icon="link" href="/observability/langsmith"> Debugging, testing, evaluating, and monitoring for LangChain-based agents. </Card> <Card title="Google Cloud Trace" icon="cloud" href="/observability/gcp-trace"> Distributed tracing to find latency bottlenecks in Google Cloud environments. </Card> <Card title="Google Cloud Logging" icon="file-lines" href="/observability/gcp-logging"> Structured log management and analysis in Google Cloud. </Card> </Cards> ## 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 <Tabs> <Tab title="Config file"> 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. </Tab> <Tab title="Admin UI"> <Steps> <Step title="Open the observability admin page"> Navigate to `/admin/observability/` in the running standalone. The catalog shows the supported providers: Langfuse, Arize Phoenix, LangSmith, GCP Trace, GCP Logging. <Frame> <img alt="Observability admin page with Langfuse selected" /> </Frame> </Step> <Step title="Add a provider"> 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. </Step> </Steps> </Tab> </Tabs> ## 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 <Card title="Local traces" icon="chart-line" href="/observability/traces"> Browse, search, and inspect AG-UI run events captured by the standalone's trace store. </Card> <Card title="Telemetry events" icon="file-json" href="/observability/telemetry-events"> The OpenTelemetry event shape the engine emits. </Card> <Card title="Custom handler" icon="wrench" href="/observability/custom-handler"> Wire your own span handler when the built-in providers don't fit. </Card> # 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 `<input>`/`<textarea>` masked) * `data-ph-mask` on chat textarea + auth forms (defensive text mask) * `data-ph-no-capture` on chat bubbles + prompts editor (full DOM block) Disable session replay independently with `IDUN_TELEMETRY_SESSION_REPLAY=false`. ## Next steps <Card title="Local trace store" icon="database" href="/observability/traces"> See how agent runs are captured and surfaced in the bundled admin UI. </Card> <Card title="Observability overview" icon="chart-line" href="/observability/overview"> Wire an external provider on top of the local store. </Card> <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Lock down the standalone before shipping to production. </Card> # Local trace store Source: https://docs.idun-group.com/observability/traces Use the standalone runtime's built-in trace store and admin UI to inspect every agent run, LLM call, tool invocation, and cost. The standalone runtime ships with a built-in trace store. Every agent run is captured automatically, written to the standalone database, and served through the bundled admin UI. No external collector, no extra config: it is on by default. This page covers what the trace store captures, how to read the trace UI, and when to switch from SQLite to Postgres for production. ## What the trace store captures The trace store consumes OpenTelemetry spans emitted by the engine's existing instrumentation (LangChain, ADK, MCP, guardrails). On boot, the standalone attaches its own `SpanExporter` to the engine's `TracerProvider`; spans flow through a bounded queue into an asyncio writer, land in the `standalone_trace` and `standalone_span` tables, and are served back through `/admin/api/v1/traces` to the trace UI. ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}} flowchart TB Agent["Agent run"] --> TP["Engine TracerProvider"] TP --> Exporter["Standalone SpanExporter<br/>(bounded queue + async writer)"] Exporter --> DB[("standalone_trace<br/>+ standalone_span")] DB --> UI["/admin/api/v1/traces<br/>(trace UI)"] ``` If no observability provider is configured, the runtime self-installs `LangChainInstrumentor` so spans are still produced. External providers stack on top of the local store: Langfuse and LangSmith ship spans through their own paths while the local store captures the same runs in parallel; Phoenix, GCP Trace, and other OTel-based providers attach to the same `TracerProvider` as the local exporter and fan out from there. You always have a local copy. ## Trace list view The list view at **Traces** shows one row per agent run. <Frame> <img alt="Trace list view" /> </Frame> Default columns: * **Name**: the root span name (typically the agent or graph entry point). * **Started at**: wall-clock timestamp. * **Latency**: end-to-end duration of the trace. * **Total tokens**: sum across every LLM span in the trace. * **Total cost**: sum across every LLM span, computed from the LiteLLM model-price snapshot. * **Model(s)**: distinct models touched in the run. * **Status**: `ok`, `error`, or `unset`. Hidden by default, toggleable from the column menu: * **User ID**: `user.id` projected onto spans by the engine's `using_user(...)` wrapper. * **Session ID**: `session.id`, auto-promoted from LangGraph's `configurable.thread_id`. * **Tags**: operator-defined trace metadata. ### Filter shortcuts Above the list, you can filter by: * **Time range**: last 1 hour, 24 hours, 7 days, custom. * **Model**: array-contains match against the trace's denormalised `models` column. * **Status**: `ok` / `error` / `unset`. * **User**: exact match on `user.id`. * **Session**: exact match on `session.id`. * **Free-text search**: searches span names. Postgres uses `pg_trgm`; SQLite uses prefix `LIKE`. Document character cap: 256. ## Trace detail view Click a row to open the trace detail. The layout has three panels: 1. **Span tree** (left): collapsible hierarchical view, one row per span, with kind icon, latency, token, and cost badges. 2. **Waterfall** (top-right): horizontal duration bars, percentage-positioned, with the critical path highlighted. 3. **Right rail** (bottom-right): the selected span's detail with five tabs: **Info**, **Input**, **Output**, **Attributes**, **Events**. <Frame> <img alt="Trace detail, tree mode" /> </Frame> Toggle to **Waterfall** to see the same spans as a timing chart with the critical path highlighted: <Frame> <img alt="Trace detail, waterfall mode" /> </Frame> The **Input** and **Output** tabs render chat messages as bubbles by default with a Pretty/Raw JSON toggle. Tool calls appear as structured cards. The **Attributes** tab shows the raw OpenInference attribute map. ### Span-kind icons The trace UI renders the full set of nine OpenInference span kinds. Each has a distinct icon and a tooltip describing the semantic. | Kind | Meaning | | ----------- | --------------------------------------------------------------------------- | | `LLM` | A call to a language model. Token counts and cost land here. | | `EMBEDDING` | A call to an embedding model. | | `CHAIN` | A composite step (LangChain `RunnableSequence`, LangGraph node grouping). | | `RETRIEVER` | A vector-store or similar retrieval step. | | `RERANKER` | A reranker model call. | | `TOOL` | A tool invocation. Arguments live in `output.value` JSON (LangChain quirk). | | `AGENT` | An agent-level step (e.g. ReAct agent loop). | | `GUARDRAIL` | A guardrail check. | | `EVALUATOR` | An evaluator step (rare in v1; reserved for the eval feature). | ### Streaming-cost prefix Spans with `cost_breakdown.partial = true` render their cost with a leading `~`, for example `~$0.0042`. Hovering shows the tooltip "Approximate. Streaming response dropped detail buckets, computed from headline prompt tokens only." This catches OpenAI streaming completions under LangChain, which drop the `*_details` buckets and so cannot be costed exactly. These approximate costs still aggregate into the trace-level total. Treat trace totals on a streaming-heavy workload as a lower-bound estimate. ### Truncated payloads Long `input.value` / `output.value` strings are truncated at 64 KB by default. Truncated values display a "truncated" badge and the original byte length. Tune the cap via `IDUN_TRACES_INPUT_VALUE_MAX_BYTES`. ## Trace pipeline health panel The admin UI exposes a small panel showing the trace pipeline's runtime health: queue depth and dropped-span count. * **Queue depth**: number of spans buffered between the engine's `BatchSpanProcessor` and the asyncio writer task. Normal values stay near zero. A non-trivial steady-state queue depth means the writer is falling behind. * **Drop count**: number of spans dropped by the bounded queue under sustained backpressure (drop-oldest policy). A non-zero count means input span rate exceeds the writer's drain rate. Sustained drops or a climbing queue depth on Postgres point at one of: * Span rate above the deployment's headroom (consider tuning `IDUN_TRACES_INPUT_VALUE_MAX_BYTES` down on heavy-payload workloads). * Network or disk pressure on the database host. * A worker stuck in slow recovery (check the standalone logs). On SQLite, sustained drops past a few thousand traces typically mean it is time to switch the deployment to Postgres. ## Retention Traces are kept for 14 days by default and dropped automatically beyond that. Tune via `IDUN_TRACE_RETENTION_DAYS`. * **Postgres**: monthly partition rotation. Expired partitions are detached and dropped, and the runtime pre-creates the next two months of partitions on boot. * **SQLite**: a scheduled `DELETE` runs daily. ## Switching to Postgres The standalone default is SQLite, which is fine for quickstart, demos, and local dev. For production, switch to Postgres. Past around 10k traces, SQLite list-view latency degrades noticeably, and the trace UI banner reminds you of this when SQLite is the active backend. See [Switching to Postgres](/quickstart#switching-to-postgres) for the full procedure and the four trace-store environment variables. ## Going beyond the local store The local trace store is the default floor. If you want richer dashboards, hosted retention, evaluation tooling, or team-shared traces, configure an additional observability provider in the engine's [observability config](/observability/overview): <Card title="Langfuse" icon="chart-line" href="/observability/langfuse"> Open-source observability with per-LLM analytics. </Card> <Card title="Arize Phoenix" icon="flame" href="/observability/arize-phoenix"> OpenInference-native trace viewer and evaluation suite. </Card> <Card title="LangSmith" icon="link" href="/observability/langsmith"> LangChain-native debugging and monitoring. </Card> <Card title="Google Cloud Trace" icon="cloud" href="/observability/gcp-trace"> Distributed tracing inside Google Cloud. </Card> These stack on top of the local store. You always keep the local copy. # Prompts Source: https://docs.idun-group.com/prompts Version, store, and render prompt templates with Jinja2 variables. Edit them from the admin panel or seed from YAML. Prompt management lets you version, store, and render prompt templates without hardcoding them in agent code. Define them in `config.yaml` (the seed shape) or edit them through the standalone admin panel at `/admin/prompts/`. Prompts support Jinja2 variables (`{{ variable }}`) for dynamic content at runtime. ## Key concepts | Concept | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Prompt ID** | A logical name for a prompt family (e.g., `system-prompt`, `rag-query`). | | **Versions** | Each prompt ID can have multiple versions. Content is immutable after creation. Updating a prompt creates a new version. | | **Latest tag** | The `latest` tag always points to the highest version and is managed automatically. | | **Tags** | Free-form labels (`production`, `staging`, `reviewed`) plus the managed `latest`. | ## Define prompts <Tabs> <Tab title="Config file"> ```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}} prompts: - prompt_id: "system-prompt" version: 1 content: "You are a helpful assistant specializing in {{ domain }}." tags: ["latest"] - prompt_id: "rag-query" version: 1 content: | Answer the question based on the following context. Context: {{ context }} Question: {{ query }} tags: ["latest"] ``` | Field | Type | Description | | ----------- | -------------- | ----------------------------------------------------------------------- | | `prompt_id` | `string` | Logical identifier for the prompt family. | | `version` | `integer` | Version number. Auto-incremented per `prompt_id` on admin-API writes. | | `content` | `string` | Prompt text, supports Jinja2 `{{ variables }}`. Immutable once created. | | `tags` | `list[string]` | Free-form labels. `latest` is server-managed. | </Tab> <Tab title="Admin UI"> <Steps> <Step title="Open the prompts admin page"> Navigate to `/admin/prompts/` in the running standalone. The Prompt library lists every version with its variables, version number, and a row of actions. <Frame> <img alt="Prompts admin page" /> </Frame> </Step> <Step title="Create a new prompt"> Click **New prompt**. Give it a unique `Name` (prompt ID), write the `Body` using `{{ name }}` syntax for variables, optionally add tags. The right panel surfaces detected variables as you type. <Frame> <img alt="New prompt drawer" /> </Frame> </Step> <Step title="Save"> Save. The reload pipeline re-instantiates the engine with the new prompt available to `get_prompt()`. Subsequent saves with the same name allocate the next version automatically. </Step> </Steps> </Tab> </Tabs> ## Admin REST API The standalone exposes the same operations at `/admin/api/v1/prompts/`. All routes require an authenticated admin session (the cookie set by `/admin/api/v1/auth/login`). Set two shell variables once and the snippets below run as-is: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # Capture the session cookie after `/admin/api/v1/auth/login` (e.g., copy from your browser's devtools). SESSION_COOKIE='your-session-cookie-value' # The integer row id for an existing prompt version. PROMPT_ROW_ID=123 ``` ### List versions ```curl theme={"theme":{"light":"github-light","dark":"github-dark"}} curl "http://localhost:8000/admin/api/v1/prompts" \ -b "idun_session=${SESSION_COOKIE}" ``` Returns every prompt version ordered by `prompt_id` then `version DESC`. ### Create a new version ```curl theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X POST "http://localhost:8000/admin/api/v1/prompts" \ -H "Content-Type: application/json" \ -b "idun_session=${SESSION_COOKIE}" \ -d '{ "prompt_id": "system-prompt", "content": "You are a helpful assistant for {{ domain }}.", "tags": ["production"] }' ``` The first write for a given `prompt_id` is version 1. Subsequent POSTs with the same `prompt_id` allocate the next version and move the `latest` tag onto the new row. The write triggers the reload pipeline; the response includes the new row plus the reload outcome. ### Update tags Content is immutable. Only tags can be patched: ```curl theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X PATCH "http://localhost:8000/admin/api/v1/prompts/${PROMPT_ROW_ID}" \ -H "Content-Type: application/json" \ -b "idun_session=${SESSION_COOKIE}" \ -d '{"tags": ["production", "reviewed"]}' ``` <Note> The `latest` tag is managed server-side. Removing it from a PATCH body is a no-op if the row is still the highest version; you cannot assign `latest` to an older version manually. </Note> ### Delete a version ```curl theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X DELETE "http://localhost:8000/admin/api/v1/prompts/${PROMPT_ROW_ID}" \ -b "idun_session=${SESSION_COOKIE}" ``` If the deleted row carried `latest`, the tag is automatically promoted onto the next-highest remaining version of the same `prompt_id`. ## Using prompts in agent code ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from idun_agent_engine.prompts import get_prompt prompt = get_prompt("system-prompt") rendered = prompt.format(domain="healthcare") # "You are a helpful assistant specializing in healthcare." ``` Resolution order: 1. Explicit `config_path` argument. 2. The standalone's in-process snapshot (set by the reload pipeline; covers the admin-UI / REST path). 3. `IDUN_CONFIG_PATH` YAML file (engine-only mode). <Warning> `format()` uses Jinja2 strict mode. Missing variables raise a `ValueError` with a message including the prompt ID and version. </Warning> ### LangChain integration Convert a prompt to a LangChain `PromptTemplate`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} prompt = get_prompt("rag-query") lc_prompt = prompt.to_langchain() result = lc_prompt.format(context="AI is...", query="What is AI?") ``` <Note> `to_langchain()` requires `langchain-core`. Install it with `pip install langchain-core`. </Note> ## Best practices * Use descriptive prompt IDs like `system-prompt`, `rag-query`, `summarization` (not `prompt-1`). * Keep prompts atomic: one prompt per concern (system instructions, query template, output format). * Create new versions for meaningful changes, not typo fixes. * Use tags like `production`, `staging`, `experimental` to track lifecycle. * Pin specific versions in your agent code rather than always resolving against `latest`. ## Troubleshooting <AccordionGroup> <Accordion title="Prompt not found at runtime"> 1. Confirm the prompt exists at `/admin/prompts/` (or in the YAML you bootstrapped from). 2. If you just created it via REST, the reload pipeline must complete before `get_prompt()` sees it; check the response's `reload.status` field. 3. In engine-only mode, verify `IDUN_CONFIG_PATH` points at a YAML containing the prompt. </Accordion> <Accordion title="Variables not rendering"> 1. Use double braces: `{{ variable }}`, not `{ variable }`. 2. Pass all required variables to `format()`. 3. The error message includes the prompt ID and version to help identify the issue. </Accordion> <Accordion title="Version numbers not incrementing"> Auto-increment is scoped per `prompt_id`. Creating a version with a different `prompt_id` starts at 1. </Accordion> </AccordionGroup> # Quickstart Source: https://docs.idun-group.com/quickstart Deploy your first AI agent with Idun in under 30 minutes, from pip install to live chat UI. `pip install` → chat UI → production agent in under 30 minutes. No Docker required, no separate frontend, no extra services to deploy. ## Prerequisites * **Python 3.12+** ([python.org](https://www.python.org/downloads/)) * **pip** (bundled with Python) * An LLM provider key (OpenAI, Anthropic, or Google, whichever your agent calls) ## Steps <Steps> <Step title="Create a project directory and install"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} mkdir my-agent && cd my-agent pip install idun-agent-engine langgraph langchain-google-genai ``` Save the next two files inside this `my-agent/` directory. </Step> <Step title="Save agent.py"> Save this as `my-agent/agent.py`: ```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) ``` </Step> <Step title="Save config.yaml"> Save this as `my-agent/config.yaml` next to `agent.py`: ```yaml config.yaml theme={"theme":{"light":"github-light","dark":"github-dark"}} server: api: port: 8000 agent: type: LANGGRAPH config: name: "my-agent" graph_definition: "./agent.py:graph" checkpointer: type: sqlite db_url: "sqlite:///conversations.db" ``` <Note> `graph_definition: "./agent.py:graph"` is resolved against the directory where you run `idun init` (the next step), so `agent.py` and `config.yaml` must live in the same directory you launch from. Use an absolute path if you want to invoke it from elsewhere. </Note> </Step> <Step title="Run"> Put `GEMINI_API_KEY=...` in a `my-agent/.env`. Get a key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). From inside `my-agent/`: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} idun init ``` Runs Alembic migrations, seeds the database from `config.yaml`, and boots the server at `http://localhost:8000`. Use `idun init --port 8080` to bind a different port (overrides the `IDUN_PORT` env var; default is 8000). <Warning> `config.yaml` is a **one-shot bootstrap**, not a runtime config file. Each resource (agent, MCP servers, guardrails, observability, …) is seeded into the DB only if the corresponding row is empty. After the first boot the admin panel at `/admin/` becomes the source of truth, and later edits to `config.yaml` are ignored. To re-seed from YAML, clear the relevant rows from the DB (or delete `idun_standalone.db` for a clean restart) and run `idun init` again. </Warning> <Frame> <img alt="Admin landing page" /> </Frame> </Step> <Step title="Chat"> Open [http://localhost:8000](http://localhost:8000) in your browser. Send a message and the response streams back in real time. <Frame> <img alt="Chat conversation" /> </Frame> </Step> <Step title="Explore the admin"> Open [http://localhost:8000/admin](http://localhost:8000/admin). Configure MCP servers, managed prompts, observability, messaging integrations, and SSO, all without redeploying. <Frame> <img alt="Agent detail" /> </Frame> </Step> </Steps> ## Next steps <Card title="Pick a framework" icon="layers" href="/frameworks/overview"> LangGraph or Google ADK </Card> <Card title="Add guardrails" icon="shield" href="/guardrails/overview"> 15+ built-in safety guards </Card> <Card title="Wire observability" icon="chart-line" href="/observability/overview"> Langfuse, Phoenix, LangSmith, or GCP Trace </Card> <Card title="Connect MCP servers" icon="plug" href="/mcp-servers/overview"> stdio, SSE, streamable HTTP, or WebSocket transports </Card> <Card title="Deploy" icon="cloud" href="/deployment/overview"> Cloud Run, Kubernetes, or any single-container host </Card> ## Switching to Postgres The standalone runtime ships with SQLite as the default database for quickstart and demo. For production, switch to Postgres. SQLite's trace-storage ceiling is around 10k traces before the list view starts to lag. The trace UI shows a permanent banner reminding operators of this when SQLite is the active backend. To switch: <Steps> <Step title="Provision Postgres 15+"> Provision a Postgres 15 or newer database. Make sure the `pg_trgm` extension is available, used for free-text trace search. </Step> <Step title="Set DATABASE_URL"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/dbname" ``` </Step> <Step title="Run migrations"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} idun setup ``` Alembic creates the trace tables with monthly partitioning, plus the standard standalone admin tables. </Step> <Step title="Restart the standalone runtime"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} idun init ``` </Step> </Steps> Postgres unlocks 5–8k spans/sec sustained write (Linux production floor) versus 1k spans/sec on SQLite, plus richer free-text search and bounded read latency at scale. ### Trace-store environment variables | Variable | Default | Purpose | | ----------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `IDUN_TRACE_RETENTION_DAYS` | `14` | How many days of traces to keep before dropping. On Postgres, expired monthly partitions are detached and dropped. On SQLite, a scheduled `DELETE` runs. | | `IDUN_TRACES_INPUT_VALUE_MAX_BYTES` | `65536` | Per-attribute byte cap before truncation. Lower this on heavy-payload deployments to recover throughput, or raise it to keep more raw input/output. | | `IDUN_PRICES_REFRESH` | `false` | When `true`, the cost calculator fetches the LiteLLM model-prices snapshot at boot (5-second timeout, snapshot fallback). Default uses the vendored monthly snapshot. | # Roadmap Source: https://docs.idun-group.com/roadmap Where Idun Engine is today and what we're building next. This page is the honest version. Things in "Today" exist in code on `main`. Things in "Next" are committed work but not shipped. Things in "Later" are direction, not commitments. No dates. If you depend on something in "Next" or "Later", open an issue or talk to us in [Discord](https://discord.gg/KCZ6nW2jQe). Priority follows what users actually need. ## Today Shipped on `main` and used by every install. * **Agent frameworks.** LangGraph (primary, with full AG-UI streaming) and Google ADK (mature, session + memory services, no stream yet). * **Single install path.** `pip install idun-agent-engine` ships the engine, the standalone admin/chat/traces app, and the `idun` console script. One wheel, one process, SQLite by default, Postgres optional. * **Engine-only mode.** `idun agent serve --source file --path config.yaml` runs the runtime layer without the DB or admin surface for teams with their own admin stack. * **Auth.** Admin panel: `none` (laptop default) or `password` (containerized default). Agent routes: per-agent OIDC JWT validation. * **Persistence.** LangGraph checkpointers in `InMemorySaver`, `AsyncSqliteSaver`, `AsyncPostgresSaver`. ADK session services in InMemory, VertexAI, Database (PostgreSQL). * **Observability.** Langfuse, Arize Phoenix, LangSmith, GCP Trace, GCP Logging. Multiple providers can run at once. Local trace store always on. * **Guardrails.** Guardrails AI Hub integration: `BanList`, `DetectPII`, `NSFWText`, `CompetitorCheck`, `BiasCheck`, `ValidLanguage`, `GibberishText`, `ToxicLanguage`, `RestrictToTopic`. Split into input and output stages. * **MCP toolchain.** Stdio, SSE, and HTTP transports via `langchain-mcp-adapters`. Helpers for both LangGraph and ADK agents. * **Messaging integrations.** WhatsApp Cloud API and Discord Interactions Endpoint, with webhook verification. * **Deployment.** Cloud Run + Docker images for the standalone. * **Telemetry.** Anonymous usage events to PostHog. Opt out with `IDUN_TELEMETRY_ENABLED=false`. ## Next Committed work that is in scope but not yet shipped. These appear in design specs, open issues, or active branches. * **OIDC for the admin panel.** Today the standalone ships `none` and `password` auth modes. OIDC is the next mode. * **Hub for community templates.** A way to import a vetted starter agent into your scaffold, instead of copy-pasting from the template repo. * **Broader LangChain story.** Today the first-class adapters are LangGraph and ADK. The plan is to expand support so a wider set of LangChain agents work with the same config and AG-UI surface. ## Later Direction we're investing in. Not a commitment, not a timeline. Open issues if any of this is load-bearing for your team. * **Native A/B test harness.** First-class support for shadow traffic and per-segment routing across agent versions. * **Marketplace for agents.** A discoverable index of community-published agents that drop into the scaffold. ## How this list is maintained This page is updated when a phase ships or when scope changes. The source of truth for what's in code is the per-package `CLAUDE.md` files in the repo (`libs/idun_agent_engine/CLAUDE.md`, `libs/idun_agent_standalone/CLAUDE.md`, `libs/idun_agent_schema/CLAUDE.md`). If this page disagrees with the code, the code wins; file an issue. # Admin panel and reload pipeline Source: https://docs.idun-group.com/standalone/admin How the standalone admin panel saves changes, validates them, and hot-reloads the running engine without a process restart. The admin panel at `/admin/` is the runtime source of truth for an Idun standalone deployment. After first boot, every change a user makes in the UI is staged as a DB write, validated, and applied to the running engine without restarting the process. This page explains that pipeline. ## What lives at `/admin/` Each section maps to a row in the standalone DB. Saving from the UI hits `/admin/api/v1/*` and triggers the reload pipeline below. * **Agent**: framework, model, system prompt, graph definition, ADK app name, session/memory services. * **MCP servers**: stdio, HTTP, SSE, WebSocket transports and the tools each advertises. * **Managed prompts**: versioned prompt rows your agent code reads via `get_prompt(...)`. * **Guardrails**: input and output guards from the Guardrails AI hub, validators, or custom validators. * **Observability**: Langfuse, LangSmith, Phoenix, GCP Trace. * **Integrations**: Slack, Teams, and other messaging surfaces that route through the agent. * **SSO and admin users**: when `IDUN_ADMIN_AUTH_MODE=password`, manage the local admin roster and SSO providers. * **Theme**: branding for the embedded chat UI. ## Save: the three-round pipeline When a user clicks **Save** in the admin panel, the standalone runs through three rounds. The pipeline lives in `services/reload.py` and is guarded by a single in-process `asyncio.Lock` so concurrent saves serialize cleanly. <Steps> <Step title="Round 1: request validation"> FastAPI validates the request body against the admin REST schema. A bad shape returns 422 before the handler runs. </Step> <Step title="Round 2: assembly and cross-field validation"> The staged DB mutation is assembled into a full `EngineConfig` via `assemble_engine_config(session)`, then re-validated for cross-field rules (e.g. LangGraph requires a checkpointer at request time). On failure the DB is rolled back and the API returns 422 with field-level errors. </Step> <Step title="Round 3: engine reload"> The new `EngineConfig` is applied to the live engine via the engine's own lifecycle hooks: `cleanup_agent(app)` tears down the current agent, then `configure_app(app, config)` rebuilds it on the same FastAPI instance. No process restart, no port flap. The active prompts snapshot is published just before this step so user code calling `get_prompt(...)` at module import time during `exec_module` sees the new value. On failure: the DB mutation is rolled back, the prompts snapshot reverts to the prior value, the reload outcome is recorded as `RELOAD_FAILED` in `runtime_state`, and the API returns 500 with `code: reload_failed`. </Step> </Steps> The reload callable itself lives in `services/engine_reload.py:build_engine_reload_callable`. It also catches the case where a new save introduces a guardrail that fails to install (typically a `hub://` 401 or a missing transitive dep) and surfaces that as a round-three failure rather than letting `configure_app` return success with a silently inactive guardrail. ## When a process restart IS required Most config changes hot-reload. Two fields make up the "structural slice" that cannot be hot-swapped: * `agent.type` (framework: LangGraph, ADK, Deep Agents) * `agent.config.graph_definition` (LangGraph entry point path) A save that changes either is committed to the DB but NOT applied to the running engine. The API returns `status: restart_required`, and the admin UI shows a banner reminding the operator to restart. The structural slice is defined in `services/reload.py:_structural_slice`. Every other field hot-reloads in place: agent name, description, prompts, MCP servers, guardrails, observability providers, messaging integrations, theme. ## Failure isolation Pre-existing failures from earlier saves do not block new ones. The reload callable tracks the set of currently-failing guardrails and only treats a new save as failed if it INTRODUCES a regression. This keeps the admin usable even when a previously-installed guardrail dep has gone stale. ## Source pointers | Concern | File | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Top-level pipeline | [`libs/idun_agent_standalone/services/reload.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_standalone/services/reload.py) | | Engine reload callable | [`libs/idun_agent_standalone/services/engine_reload.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_standalone/services/engine_reload.py) | | App composition and runtime gate | [`libs/idun_agent_standalone/src/idun_agent_standalone/app.py`](https://github.com/Idun-Group/idun-agent-platform/blob/main/libs/idun_agent_standalone/src/idun_agent_standalone/app.py) | | Engine lifecycle hooks | [`libs/idun_agent_engine/src/idun_agent_engine/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 <Card title="CLI reference" icon="terminal" href="/cli/overview"> Every `idun` command, what it does, and its flags. </Card> <Card title="Cloud Run deployment" icon="cloud" href="/standalone/cloud-run"> Deploy the standalone to Google Cloud Run. </Card> <Card title="Customizing the UI" icon="layers" href="/standalone/customizing-ui"> Theme the chat UI and pick a layout variant. </Card> # Deploy to Cloud Run Source: https://docs.idun-group.com/standalone/cloud-run Single-container deploy on Google Cloud Run with managed Postgres. Cloud Run runs a single container per request, scales to zero by default, and forwards HTTPS. The standalone is designed to fit. ## 1. Set up Cloud SQL (Postgres) The standalone uses SQLite by default; on Cloud Run that disappears between revisions. Use Cloud SQL Postgres: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} gcloud sql instances create idun-postgres \ --database-version=POSTGRES_16 --region=europe-west1 --tier=db-f1-micro gcloud sql databases create idun --instance=idun-postgres gcloud sql users create idun --instance=idun-postgres --password='changeme' ``` URL form (Cloud SQL Auth Proxy / unix socket): ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} postgresql+asyncpg://idun:changeme@/idun?host=/cloudsql/PROJECT:REGION:idun-postgres ``` ## 2. Store secrets in Secret Manager ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} echo -n "$(idun hash-password)" | gcloud secrets create idun-admin-hash --data-file=- openssl rand -hex 32 | gcloud secrets create idun-session-secret --data-file=- echo -n "postgresql+asyncpg://..." | gcloud secrets create idun-db-url --data-file=- ``` ## 3. Build & push Use `Dockerfile.example` as a starting point: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} docker build -f Dockerfile.example -t gcr.io/PROJECT/my-agent:0.1.0 . docker push gcr.io/PROJECT/my-agent:0.1.0 ``` ## 4. Deploy Copy the template into your deploy directory, then edit it: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} cp /path/to/idun-agent-platform/libs/idun_agent_standalone/docker/cloud-run.example.yaml cloud-run.yaml # Replace PROJECT, REGION, and the image tag in cloud-run.yaml. gcloud run services replace cloud-run.yaml --region=europe-west1 ``` The template ships two annotations you must keep: * `metadata.annotations."run.googleapis.com/cloudsql-instances"` — attaches the Cloud SQL instance to the service. Required for the unix-socket form `host=/cloudsql/PROJECT:REGION:idun-postgres` in `DATABASE_URL`. * `spec.template.metadata.annotations."run.googleapis.com/cloudsql-instances"` — same value at the revision level. Cloud Run requires both for new revisions to inherit the connection. Recommended runtime settings (already set in the template): * `minScale: "1"` — eliminates cold starts and keeps the trace retention scheduler running. * `cpu-throttling: "false"` — keeps the trace writer flushing between requests. * 1 GiB memory, 1 vCPU is plenty for a small agent. ## Caveats * **MCP servers using `command: docker run …`** won't work on Cloud Run. Switch to `transport: stdio` with a binary command (e.g. `npx`, `uvx`, a precompiled binary) or use HTTP transport pointing at another Cloud Run service. * **Trace retention purge** runs hourly via APScheduler — when Cloud Run scales to zero the scheduler stops too. With `minScale: "1"` it runs continuously. * Cookies need `Secure` — Cloud Run's load balancer sets `X-Forwarded-Proto: https`. The standalone honors that automatically. ## Next steps <Card title="Production hardening" icon="shield-halved" href="/deployment/hardening"> Lock down admin auth, TLS, secrets, and trace retention before exposing the service. </Card> <Card title="GCP Trace" icon="route" href="/observability/gcp-trace"> Send distributed traces to the same Google Cloud project. </Card> <Card title="GCP Logging" icon="file-text" href="/observability/gcp-logging"> Stream structured logs to Cloud Logging from the running service. </Card> # Customizing the chat UI Source: https://docs.idun-group.com/standalone/customizing-ui Theme, layout, and full UI replacement. The standalone ships a Next.js UI bundled into the wheel. There are three levels of customization. ## Level 1 — Theme Open `/admin/settings/`. Change the app name, greeting, layout (branded / minimal / inspector), color palette (light + dark), radius, font, and starter prompts. Save, and the change applies immediately to all clients. The theme is persisted in the `theme` table alongside the rest of the admin state. ## Level 2 — Replace the chat UI entirely The static UI is mounted from a directory. Override it via `IDUN_UI_DIR`: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} IDUN_UI_DIR=/path/to/your/built/ui idun serve ``` Your UI just needs to: * Be a static export (HTML + JS + CSS). * Call the standalone's REST endpoints — auth at `/admin/api/v1/auth/login`, agent run at `/agent/run`, etc. * Optionally read `/runtime-config.js` for theme + auth mode. The bundled UI lives in the [`services/idun_agent_standalone_ui`](https://github.com/Idun-Group/idun-agent-platform/tree/main/services/idun_agent_standalone_ui) directory; fork it. ## Level 3 — Headless Mount no UI at all (point `IDUN_UI_DIR` at an empty directory or omit the bundled UI from your image). The admin REST surface and `/agent/run` remain available for your own frontend. ## Next steps <Card title="CLI reference" icon="terminal" href="/cli/overview"> Every `idun` command, the flags it accepts, and what it does. </Card> <Card title="Deploy to Cloud Run" icon="cloud-upload" href="/standalone/cloud-run"> Run the standalone on Google Cloud Run with a managed container. </Card> <Card title="Authentication" icon="lock" href="/auth/overview"> Configure admin-panel auth for `/admin/*`. </Card> # Overview Source: https://docs.idun-group.com/standalone/overview A self-sufficient single-agent deployment with embedded chat UI, admin panel, and traces viewer. **Idun Agent Standalone** packages one agent into a single FastAPI process — chat UI, admin panel, traces viewer, and a local DB included. Deploy on Google Cloud Run, a VM, or your laptop. ## What you get * **Chat UI** at `/`, themeable, AG-UI streaming, three layout variants (branded / minimal / inspector). * **Admin panel** at `/admin/`, edit agent config, guardrails, MCP servers, prompts, observability, integrations, and theme. Hot-reload of the running agent on save. * **Dashboard** at `/admin/`, live activity stats sourced from the local trace store: request count, p50/p95 latency, error rate, total cost, traffic sparkline, top errors. * **First-party traces** at `/admin/traces/`, every AG-UI run event captured locally; debug an agent without external observability. * **Single Docker image**, single Python process. SQLite by default; Postgres for production. ## When to use it * You ship ONE agent and don't need the governance hub. * You want to iterate on an agent locally with the full Idun stack — not just the engine SDK. * You want a branded chat UI without writing a frontend. ## When NOT to use it * You have your own admin stack and only want the runtime layer. Use [engine-only mode](/cli/overview) with `idun agent serve --source file --path config.yaml` instead. * You're running multiple agents per host. Standalone is single-agent, single-tenant; run one process per agent. ## Architecture in 60 seconds The standalone wraps `idun-agent-engine` in a single FastAPI process. The engine still runs the agent and serves `/agent/run`. The standalone adds the chat UI, the admin REST surface, the trace store, password auth, and a hot-reload pipeline that rebuilds the engine on every admin write. ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}} flowchart LR Browser --> App["FastAPI process"] App --> UI["/<br/>chat + admin + traces"] App --> Agent["/agent/*<br/>engine routes"] App --> AdminAPI["/admin/api/v1/*<br/>admin REST + auth"] AdminAPI --> DB[(SQLite or Postgres)] Agent --> DB ``` Spans flow into `standalone_trace` + `standalone_span` tables via an OTel exporter the standalone attaches at boot, and the admin UI reads them back through the same REST surface. ## Dashboard The admin panel opens on a dashboard sourced from the trace store. It refreshes every time you reload and gives you a single-pane view of whether the agent is healthy. <Frame> <img alt="Standalone admin dashboard" /> </Frame> Default panels: * **Activity (24h / 7d / 30d toggle)**: total requests, p50/p95 latency, error rate, total cost across the window. * **Requests / min**: a sparkline of traffic over the window. Spikes here pair with spikes in the latency chart so you can spot saturation. * **Latency p50 / p95**: two-line chart over the window. * **Top errors**: a leaderboard of failing span names with a count, last-seen timestamp, and a one-click link into the offending trace. All of it reads from `standalone_trace` + `standalone_span` directly. No external observability provider needed. ## Next steps <Card title="Quickstart" icon="rocket" href="/quickstart"> Get the standalone running locally with your first agent. </Card> <Card title="Admin panel and reload pipeline" icon="sliders" href="/standalone/admin"> How saves validate, hot-reload, and roll back without a process restart. </Card> <Card title="CLI reference" icon="terminal" href="/cli/overview"> Every flag of `idun` and the supporting commands. </Card> <Card title="Cloud Run deployment" icon="cloud" href="/standalone/cloud-run"> Deploy the standalone to Google Cloud Run. </Card> <Card title="Customizing the UI" icon="layers" href="/standalone/customizing-ui"> Theme the chat UI and pick a layout variant. </Card> # Agent templates Source: https://docs.idun-group.com/templates Clone a runnable agent template to get started faster. Nine examples covering common LangGraph and ADK patterns. If you'd rather start from working code than write an agent from scratch, the [idun-agent-template](https://github.com/Idun-Group/idun-agent-template) repository has nine runnable examples you can clone and adapt. Each template is a self-contained folder with its own config, dependencies, and agent code. Pick the one closest to what you're building and modify from there. ## LangGraph templates | Template | What it demonstrates | | -------------------------- | ----------------------------------------------------------------------- | | `langgraph-simple` | Basic two-step planning pattern. Good starting point | | `langgraph-tool-node` | Tool calling with LangGraph's built-in ToolNode (recommended pattern) | | `langgraph-tool` | Manual tool invocation for cases where you need full control | | `langgraph-tool-local` | Mixing local tools with MCP tools from the platform | | `langgraph-structured` | Separate input/output schemas with typed state | | `langgraph-editorial-loop` | Multi-step researcher/writer/reviewer workflow with conditional routing | | `langgraph-copy-paste` | Minimal file operations example | ## ADK templates | Template | What it demonstrates | | ---------------- | ------------------------------------------ | | `adk-tool` | Basic LlmAgent with tool integration | | `adk-structured` | Structured input/output with typed schemas | ## Get started <Steps> <Step title="Clone the template repo"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} git clone https://github.com/Idun-Group/idun-agent-template.git cd idun-agent-template ``` </Step> <Step title="Pick a template"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} cd langgraph-tool-node ``` </Step> <Step title="Install dependencies and configure"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} pip install -r requirements.txt cp .env.example .env # Edit .env with your API keys ``` </Step> <Step title="Run it"> ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} idun init ``` Runs migrations, seeds the DB from the template's `config.yaml`, opens the browser at `http://localhost:<port>/` (default 8000; override with `--port` or `IDUN_PORT`), and serves the standalone app. `idun init` is idempotent, so you can re-run it on the same folder. For routine restarts that skip migration/seed work, prefer `idun serve`. </Step> </Steps> ## Which template should you pick? * **First time with Idun?** Start with `langgraph-simple`. It's the shortest path to a running agent. * **Need tool calling?** Use `langgraph-tool-node`. It follows the recommended LangGraph pattern. * **Want MCP tools from the platform AND local tools?** Use `langgraph-tool-local`. * **Building with Google ADK?** Start with `adk-tool`. * **Need typed input/output schemas?** Look at `langgraph-structured` or `adk-structured`. * **Building a multi-step workflow?** The `langgraph-editorial-loop` shows a researcher/writer/reviewer pattern with conditional routing. ## Templates vs. the quickstart The [quickstart](/quickstart) walks you through the minimal steps to run an agent (a few lines of code). Templates give you more structure: proper project layout, config files, environment management, and patterns you'll need as your agent grows. Both paths end at the same place: an agent running under the standalone with chat UI, admin, and traces. # Troubleshooting Source: https://docs.idun-group.com/troubleshooting Diagnose common Idun runtime errors: agent_not_ready, boot engine layer skipped, graph load failures, reload-pipeline rollbacks, and validation errors. This page covers the runtime errors and log messages you are most likely to hit, what each one means, and how to recover. The error strings below come directly from the engine and standalone source. <AccordionGroup> <Accordion title="Server returns HTTP 503 agent_not_ready"> **Symptom.** A POST to `/agent/run`, `/agent/stream`, or the deprecated `/agent/invoke` returns: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "detail": "agent_not_ready" } ``` **Cause.** The standalone server is running, but no agent has been materialised yet. This is the first-run state before the wizard completes. **Fix.** Open `http://<host>:<port>/` in a browser. The chat root detects the unconfigured state and redirects to `/onboarding/`, which scaffolds an agent and seeds the DB. After the wizard completes, the next `/agent/*` call returns 200. If you expected an agent to be configured, check the admin panel at `/admin/agent/` to confirm the agent row exists. </Accordion> <Accordion title="Boot log: engine layer skipped, admin-only mode"> **Symptom.** At startup the log contains: ``` boot engine layer skipped, admin only mode reason=AssemblyError(...) boot engine app started unconfigured (no agent yet, wizard will materialize) ``` **Cause.** The standalone read the DB and could not assemble an `EngineConfig` from the stored rows. This usually happens because no agent row exists yet, or because the seeded YAML failed validation. The server still boots so you can reach `/admin/` and `/onboarding/`, but the `/agent/*` routes return `agent_not_ready` until the agent is materialised. **Fix.** Either complete the onboarding wizard, or read the `AssemblyError` reason printed in the log. If your `config.yaml` was supposed to seed an agent on first boot, the YAML failed schema validation; fix the validation error and rerun `idun init`. </Accordion> <Accordion title="Reload pipeline outcomes"> Every admin-panel save flows through the three-round validation and reload pipeline in `libs/idun_agent_standalone/services/reload.py`. The response carries one of three statuses, each with a different effect on the running engine. <CardGroup> <Card title="RELOADED" icon="circle-check"> Validated and applied live. DB committed, engine rebuilt. UI toast: *Saved and reloaded.* </Card> <Card title="RESTART_REQUIRED" icon="rotate-right"> DB committed, engine NOT rebuilt. Triggered by a structural change (`agent.type` or `agent.config.graph_definition`). UI toast: *Saved. Restart required to apply.* </Card> <Card title="RELOAD_FAILED" icon="circle-xmark"> Engine refused the new config. DB write rolled back, prior config still active. UI toast: *Engine reload failed; config not saved.* Error detail attached. </Card> </CardGroup> </Accordion> <Accordion title="Engine reload failed (RELOAD_FAILED)"> **Cause.** One of: * The new `graph_definition` path is wrong or the variable does not export a `StateGraph`. * A guardrail config references a guard type the engine cannot instantiate (e.g. missing Guardrails Hub validator). * An MCP server is unreachable and the engine could not discover its tools. * An observability provider rejected the credentials at init time. **Fix.** Read the error detail in the toast (or in the standalone log). The previous config is still active; your agent did not break. Fix the underlying issue in the admin form and resave. </Accordion> <Accordion title="Round 2 validation failures (HTTP 422)"> A save that fails Round 2 returns: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "detail": { "code": "VALIDATION_FAILED", "field_errors": [ { "loc": ["memory", "type"], "msg": "..." } ] } } ``` **Common causes:** * **Framework mismatch**: e.g. setting `agent.type=langgraph` but configuring an ADK `SessionService` for memory. Each framework has its own valid memory and checkpointer set. * **Missing required field** for the chosen `agent.type`. The admin UI surfaces these inline; if you hit a 422 via API, the `field_errors` array points at the offending field. **Fix.** Resolve the field error and resave. The Round 2 check runs against the *assembled* config (DB rows merged into a synthetic `EngineConfig`), so the error may reference a field that lives in a different admin sub-page than the one you edited. </Accordion> <Accordion title="Graph load errors"> **Symptom.** At startup or after a `RESTART_REQUIRED` reboot, the engine fails to initialise with an exception like: ``` FileNotFoundError: graph_definition path does not exist ModuleNotFoundError: No module named <module> ImportError: cannot import name <variable> from <module> TypeError: graph_definition must resolve to a StateGraph or CompiledStateGraph ``` **Cause.** `agent.config.graph_definition` is parsed as `<path_or_module>:<variable_name>` and must resolve to a `StateGraph` (or a `CompiledStateGraph`, from which the engine extracts `.builder`). The engine tries the file-path interpretation first, then falls back to Python module import. So `my_agent.py:app` works if `my_agent.py` is in the current directory, and `my_pkg.agents.router:graph` works if `my_pkg` is importable. **Fix:** * Confirm the path or module exists and exports the named variable. * Confirm the variable is a `StateGraph` from `langgraph.graph`, not a custom wrapper or a function. If you have a builder function, call it before assigning to the module-level variable. * Confirm `langgraph` is installed in the same environment as `idun-agent-engine`. For a worked example, see [Connect your existing agent](/guides/connect-your-agent). </Accordion> <Accordion title="How to verify the graph loaded"> If the engine boots without error but you are not sure your graph is the one running: 1. `GET /health` returns the engine version and `agent_name` field. A non-null `agent_name` means an agent was successfully instantiated. 2. `GET /_engine/info` (engine-only deployments) returns more detail, including the framework type. 3. In the admin panel, open `/admin/agent/`. The live LangGraph visualisation reflects the currently loaded graph; if it shows your graph's nodes and edges, the graph is loaded. 4. Open a trace from `/admin/traces/` and inspect the span tree. The root span name corresponds to your graph's entry node. </Accordion> <Accordion title="ADK adapter: could not load spec for module"> **Symptom.** Engine startup fails with: ``` ImportError: Could not load spec for module at <path> ValueError: Failed to load agent from <path>:<variable>: ... ``` **Cause.** The ADK adapter (`libs/idun_agent_engine/src/idun_agent_engine/agent/adk/adk.py`) resolves `agent.config.agent` with `importlib.util.spec_from_file_location`, so the value must be a path to a `.py` file. Unlike LangGraph's `graph_definition`, it does **not** fall back to dotted module imports. Values like `my_pkg.agent:root_agent` are interpreted as the literal file `my_pkg.agent`, which doesn't exist, so `spec_from_file_location` returns `None` and the loader raises. **Fix.** Rewrite the path in file-path form: ```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}} agent: type: ADK config: agent: "./agent.py:root_agent" # not "agent:root_agent" ``` Relative paths resolve against the working directory you launched the server from. Absolute paths also work. </Accordion> <Accordion title="Edits to config.yaml don't apply after restart"> **Symptom.** You edit `config.yaml` (change a model, add an MCP server, tweak a guardrail), run `idun init` again, and nothing changes in the running agent. **Cause.** `config.yaml` is a **one-shot bootstrap**, not a runtime config file. The seeder (`libs/idun_agent_standalone/src/idun_agent_standalone/infrastructure/scripts/seed.py`) runs each `_seed_<resource>_if_empty` helper, and every helper bails when the corresponding row already exists. After the first successful boot, the DB is the source of truth and `config.yaml` is effectively ignored. **Fix.** Edit through the admin panel at `/admin/` instead. Saves there flow through the three-round validation + reload pipeline and apply immediately (subject to the `RESTART_REQUIRED` rules under "Reload pipeline outcomes" above). If you really want to re-seed from YAML, clear the relevant DB rows first. For a clean restart, stop the server, delete `idun_standalone.db` (on Postgres, truncate the seeded tables), then run `idun init` again. </Accordion> <Accordion title="CLI startup warnings"> Three deprecation warnings appear on every CLI invocation today: ``` LangChain allowed_objects deprecation LiteLLM Bedrock missing LiteLLM SageMaker missing ``` Noise from upstream packages, no effect on functionality. Will be filtered in a future release. </Accordion> </AccordionGroup> ## What's next <Card title="Connect your existing agent" icon="plug" href="/guides/connect-your-agent"> minimal config for an existing `StateGraph` </Card> <Card title="Production hardening" icon="shield-check" href="/deployment/hardening"> production env-var checklist </Card> <Card title="SSO" icon="lock" href="/auth/sso"> require an OIDC JWT on `/agent/*` </Card>