# Agent Flows
Source: https://docs.experio.cloud/admin-guide/agent-flows
Build node-based agent workflows that compile to LangGraph and run with human-in-the-loop approvals
## Overview
Agent Flows let you compose typed **nodes** into a flow on a visual canvas. When you publish a flow, Experio compiles it to a LangGraph graph and runs it — orchestrating LLM calls, knowledge graph queries, MCP tools, document generation, and human approvals as a single pipeline. A flow can run on its own from the canvas, or be enabled as a **sub-agent** that the chat agent launches on a user's behalf.
Navigate to **Admin > AI & Agents > Agent Flows**.
Agent Flows are distinct from [Flows](/admin-guide/flows), which orchestrate data-processing jobs (reader, ingestion, enrichment). Agent Flows orchestrate AI **agent** behavior and compile to LangGraph.
## The Flows List
The Agent Flows page lists every flow with its name, description, tags, and latest published version. A **Sub-agent** badge column shows whether a flow is exposed to the chat agent, and a filter above the list switches between **All flows** and **Sub-agent enabled**. From here you can:
* **Create** a new empty flow (give it a name — the system handles the internal id)
* **Open** a flow to edit it on the canvas
* **Duplicate a template** into a new draft (see [Templates](#templates))
* **Delete** a flow
## Building a Flow
Opening a flow shows the **canvas editor**. You build a flow by dragging nodes from the **palette** onto the canvas. Nodes are compact cards — they don't show input/output ports on the canvas. To **connect** two nodes, drag from one node onto another; the canvas auto-wires their compatible ports. Edges float between nodes and re-route automatically as you move nodes around.
The flow **name** is editable inline at the top of the editor. A collapsible right-hand pane holds the support panels; it re-opens automatically when you click a node:
* **Inspect** — configure the selected node (its config fields, input bindings, and budget), or the selected edge's routing. Each node also takes an optional **Display name** — the human-readable step name end users see in the chat progress view while the flow runs (it falls back to the node's label or id)
* **Run** — start and watch a run (see [Running a Flow](#running-a-flow))
* **Versions** — review prior published versions and control which one is **Live** (see [Publishing and Versioning](#publishing-and-versioning))
### Config fields
The Inspect panel renders each node's config from its schema:
* Fields that hold prose — **Query**, **Custom instructions**, **Prompt**, **System prompt**, **Output instructions** and the like — render as **multi-line text areas** you can scroll and resize, rather than a single-line box you have to scrub through. Short identifiers (titles, artifact keys, numeric limits) stay single-line.
* Fields that point at **another record** render as a picker rather than asking you to paste an identifier:
* **Model** (`model_id`, `fallback_model_id`, `report_model_id`) — the configured chat models, with **Default model** first. See [Model Configurations](/admin-guide/models).
* **Assistant** — whose per-assistant model configuration the node borrows. Leave it on **Default models** to use the global ones.
* **MCP server** — the server whose tools the step may call. See [MCP Servers](/admin-guide/mcp-servers).
* **Document template** — the branding template a generated file is rendered into. See [Document Templates](/admin-guide/document-templates).
* Fields with a **fixed set of values** — the document **Theme**, output modes, error policies — render as dropdowns.
* Fields that hold a **list** — **Tool groups**, **Allowed blocks**, an MCP **Tool whitelist** — render as checklists of the real choices. Leaving one empty is meaningful and the panel says what it means (for a tool whitelist, "all discovered tools").
* A **nested configuration** (such as the optional MCP enrichment on the research agent) renders as its own small group of controls, including the pickers above.
The pickers read admin lists. An author without permission to read one still sees that field as a plain text box and can edit the value by hand — the rest of the form is unaffected. If a flow holds a value that is no longer in the list, the field flags it rather than quietly showing "none".
### Bindings
Each node has typed **input** and **output** ports under the hood. Connecting two nodes auto-wires their compatible ports; to send one specific value, drag from a node's output port to a target input in the Inspect panel. A binding path looks like `$.nodes..outputs.`, and inputs can also bind to run parameters (`$.params.*`), the chat message (`$.params.message`), and uploaded files (`$.files.*`). Bindings are validated when you save — type mismatches and missing required inputs are surfaced inline on the canvas.
Edges order execution; bindings move the data. Reads count too: reading another node's output — through a binding, or through a `{{ nodes..outputs. }}` template in one of the node's config fields — counts as a dependency, not just the edges you drew. That is a safety net against a node running before the value it reads exists, not a guarantee of order. A node reading its own previous output (a loop body) is not treated as waiting on itself. If a node it reads from **fails**, the reading node fails too rather than running on an empty input — the branch stops there instead of producing a document out of nothing. An input that carries a **default** is the exception: it falls back to that default and the reading node still runs — but only because it reads the failed step, not because it follows it. A step you drew an **edge** from is on the failed branch, and that branch stops at the failure whatever its inputs default to.
Drawing the edge is what actually orders the two steps. Validation does check for a node reading the output of a node that isn't upstream of it, and reports it as a **warning**: run **Validate** — or apply an edited definition from **Edit as JSON** — and every such finding is listed in an amber strip above the canvas, naming the node to fix. Warnings never block publishing, by design, and there is no per-node marker on the canvas or in the Inspect panel, so that strip is the only place you can read the detail. Publish without validating first and nothing surfaces at publish time — the publish dialog lists blocking errors only; the new version's entry in the **Versions** tab carries a warning count instead, as a prompt to go back and run **Validate**.
### Edit as JSON
The canvas toolbar includes an **Edit as JSON** action. It opens the flow's full definition as JSON, where you can **export** it (copy or download) or **paste an edited definition back**. Applying a pasted definition runs the same server-side validation as the **Validate** button — unparseable or structurally-invalid JSON (missing `nodes`/`edges` arrays) is rejected before it touches the canvas.
### Node palette
| Node | Type | Description |
| ---------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Start** | `control.start` | Flow entry point; exposes run params and uploaded files. |
| **End** | `control.end` | Flow exit point; collects bound inputs as the flow result. |
| **Condition** | `control.condition` | Evaluate a predicate over flow state and route on the result. |
| **Merge** | `control.merge` | Converge conditional branches; outputs the branch that ran. |
| **Map** | `control.map` | Fan a list out over a body node in parallel (one branch per item). |
| **Join** | `control.join` | Collect map branch results after every branch settles. |
| **Loop** | `control.loop` | Bounded loop guard; every cycle passes through it. |
| **Knowledge Graph Query** | `knowledge_graph` | Scope and retrieve from the knowledge graph. Returns rows in `data` mode, or additionally synthesizes a cited narrative in `report` mode. |
| **Knowledge Graph data** | `kg.data` | Scope and query the knowledge graph and return the structured results directly — no report writer. The simpler, data-only surface of Knowledge Graph Query. |
| **Knowledge Graph Display** | `kg.display` | Write a cited narrative report from an upstream `kg.data` node's rows (the deep agent's report writer, no re-query). Wire `kg.data → kg.display`. |
| **MCP action** | `mcp.action` | Run a task with the tools of one MCP server. |
| **Orchestrator agent** | `agent.orchestrator` | Give the agent a goal plus a set of allowed blocks and a budget; the LLM picks the path. |
| **Knowledge research agent** | `agent.deep_research` | A bounded research agent over the internal knowledge graph + documents (and optionally one MCP server). |
| **Web research agent** | `agent.web_research` | A bounded research agent over the live web (Tavily), with cited sources. |
| **LLM generate** | `llm.generate` | Generate text from a templated prompt plus optional context. |
| **LLM extract** | `llm.extract` | Extract structured fields from context against a per-node schema. |
| **LLM classify** | `llm.classify` | Classify the input into one of the configured categories. |
| **Generate DOCX** | `data.generate_docx` | Produce a Word document artifact from structured sections, optionally branded with a registered Document Template. |
| **Generate PPTX** | `data.generate_pptx` | Produce a PowerPoint presentation artifact (one slide per section), optionally branded with a registered PPTX template. |
| **Generate file** | `data.generate_file` | Produce a document artifact in a chosen format — Word (`.docx`), PowerPoint (`.pptx`), PDF, or HTML — reusing the agent's existing generators. |
| **Index uploads** | `data.index_files` | Parse, chunk, and embed uploaded files so downstream nodes can use them. |
| **Read conversation** | `data.channel_history` | Read the recent messages of the conversation the run was launched from — a plain transcript plus the structured turns — so the flow can act on what the user and the agent already established. |
| **Human review** | `human.review` | Pause the flow for a reviewer; resume when approved or edited. |
The **Knowledge Graph Query** and **Knowledge Graph data** nodes share an `on_clarify` policy that controls what happens when scope needs clarification: **fail** the node, **pause** for human input (interrupt), or **continue** with empty rows. To get a cited narrative from a data-only **Knowledge Graph data** node, wire it into a **Knowledge Graph Display** node (`kg.data → kg.display`) and bind `end.result` to the Display node's `text` output — its `evidence` citations render as source chips in the run output.
The **Read conversation** node only works on a run launched **from a conversation**. A canvas or API run has no conversation to read, so the node fails and says so rather than handing an empty transcript to the rest of the flow, and it only ever reads a conversation the person who started the run can open — their own, or one shared with them — and fails with an authorization error on anything else rather than reading it. Its config sets how many of the most recent messages to read (**Messages to read**, default 20, returned oldest-first), **whose messages to include** (all, only the user's, or only the assistant's), a per-message character cap (default 40,000 characters; 0 disables truncation), and whether an empty conversation **fails the node** — left on by default, so a downstream step never quietly drafts a document out of an empty transcript.
The **Generate DOCX** and **Generate PPTX** nodes each expose a **Document Template** selector — pick a registered template by name (managed under [Document Templates](/admin-guide/document-templates)) to brand the output. The **Generate file** node adds a **format chooser** (Word, PowerPoint, PDF, or HTML); the template selector is shown only for the `docx` and `pptx` formats. Markdown in generated content (`##` headings, `**bold**`, bullets) is rendered as real Word/PPTX styling instead of literal markers.
### Budgets
Agent nodes (orchestrator, knowledge research, web research, MCP action) run a model-driven loop and must be **bounded**. Each exposes a budget so a single node can never run unchecked:
| Limit | What it counts |
| -------------------- | ---------------------------------- |
| **Max model calls** | Times the agent may call the model |
| **Max tool calls** | Tool invocations it may make |
| **Max tokens** | Total tokens across the whole loop |
| **Max wall seconds** | Total time the step may run |
Each box shows that node's own default — leave it empty to use it. Reaching the call or token limits **ends the agent's loop and returns what it has so far**; it does not fail the run, so a budget set too low shows up as truncated work rather than an error.
**Max wall seconds** is a hard deadline rather than a soft one. The knowledge research, web research and Orchestrator agent steps stop at it and hand on the work they already have; the MCP action step reports a timeout through its own error policy. Every other step that has a time limit is **failed** when it overruns: the step is cut off, whatever it had produced by then is **discarded**, and the failure is reported like any other step error, so an unresponsive step can no longer hold a run open indefinitely.
Steps that carry a built-in time limit — the knowledge graph steps, for example — are now actually held to it; a step with no time limit of its own and no built-in default still runs unbounded. Those built-in limits also apply to flow versions you published before the limit existed — a knowledge-graph step that used to take as long as it needed is now failed at its cap. Raise a step's **Max wall seconds** if it legitimately needs longer — from the **Budget** editor on an agent node, or through **Edit as JSON** (`budget.max_wall_seconds` on the node) for a knowledge-graph step, whose Budget editor the canvas doesn't render.
## Publishing and Versioning
A flow you are editing is a **draft**. Publishing snapshots the current draft into an immutable **version**:
1. Click **Publish**. The draft is validated first — publishing is blocked if validation reports errors. Validation also checks **MCP action** nodes: an unknown or disabled MCP server, or **Allow writes** enabled on a server that doesn't support writes, blocks publishing; whitelisted write tools that would be stripped because **Allow writes** is off are surfaced as a warning. A **Join** is checked against the map body it collects: a **Body output port** set explicitly to a port that body does not produce blocks publishing, because every branch would then count as failed, and a **Minimum body length** set on a Join whose body produces no default `text` port blocks publishing too, because the floor could never fire on any branch. Both are errors rather than warnings, and both are fixable only in the draft — a published version is immutable.
2. A new `AgentFlowVersion` is created with an incrementing version number. Published versions are immutable.
3. The flow's **latest published version** pointer advances to the new version.
### The live version
Every run of a flow uses its **live** version — the version the chat agent launches, and the default for a canvas or API run. The **Versions** panel marks the current one with a **Live** badge.
By default a flow **follows the latest published version**: each time you publish, the newest version automatically becomes live. To roll back or hold on an older version, click **Set as live** on any validated version in the panel — this **pins** the live version there, and publishing no longer changes what's live. Click **Follow latest** to clear the pin and return to auto-advancing on publish. Either action also loads the now-live version onto the canvas, so the editor shows what actually runs — it arrives as an **unsaved change**, and your saved draft is untouched until you save. If your canvas already holds unsaved changes, you are asked first: loading replaces them and there is no undo, so you can keep editing instead. The pin is applied either way — declining only leaves the canvas alone. While live is pinned to something other than the latest published version, the editor header carries a **Live** badge naming the pinned version, as a reminder that the newest published version is not the one running.
Only a version that passes validation can be set as live. Existing runs always stay on the version they started with; changing the live version only affects **new** runs.
You can review prior versions from the **Versions** panel. Use **Validate** at any time to run save-time validation on the draft without publishing.
## Letting the chat agent run a flow
You don't wire a flow to a dedicated assistant. Instead, you let the **normal chat agent** discover and launch it as a background **sub-agent**:
1. Open the flow and turn on the **Sub-agent** toggle in the editor header. The flow becomes available to the chat agent.
2. Write a clear flow **description** in the editor next to the toggle. The chat agent reads this description to decide **when** to run the flow, so describe what the flow does and the kind of request it handles. A sub-agent flow with no description shows an amber nudge — the agent can't route to a flow it can't recognize.
Once enabled, the chat agent can launch the flow during a normal conversation. When a user asks for something a flow covers — for example, "prepare a response for this RFP" — the agent confirms once ("I can run *RFP Response Pipeline* — proceed?"), then launches the flow **in the background**. The conversation stays fully usable while the flow runs. Users can also launch a sub-agent flow directly from the chat composer's **Launch a flow** menu, which skips the matching step and starts the chosen flow immediately.
The agent maps the user's request onto the flow's parameters automatically (flows read them as `$.params.` / `{{ params. }}`, and `$.params.message` is typically the user's core request forwarded at launch) and binds the conversation's uploaded files to the flow's file slots.
File slots the agent doesn't fill explicitly fall back to **what the user actually attached** in that conversation, newest first — a single slot takes the most recent upload; several slots are matched by label and order. So "here's the RFP, draft a response" fills the flow's `rfp_document` slot from the attachment on that very message. Only **user uploads** are eligible for this fallback: a document produced by an earlier run in the same conversation is bindable when something names it, but is never picked up implicitly as the next run's input.
If a required file slot is still unfilled, **no run is created**, and a message is posted into the conversation naming each missing input by its **readable label** (plus its description, and whether it takes more than one file) and asking the user to attach it and send again. That message is written by the platform, not left to the model to relay, so the user is always told why the flow didn't start — it is posted at most once per turn.
While a sub-agent flow runs, a **live flow-run view** opens in the chat's right-hand panel. It is deliberately minimal: a single status line naming the step in flight ("Working on *Draft the response*", by the step's **Display name**) and a **Stop** control; the conversation pulses in the sidebar until the run finishes. Closing the panel leaves a **View progress** control (above the message box) to reopen it while the run is still going. When the flow pauses for a human, the panel shows the review card there instead — see the note below. **When the run finishes the panel closes itself** — unless the user has a file the run produced open in the panel's viewer, in which case it stays until they close that document. There is no result view: the flow posts a message into the conversation with the full result and any files it produced, and that message arrives on its own (no manual refresh), so a panel could only duplicate it. Users can also just ask the agent for the status or results of a flow it launched — including questions about a document the run produced. The agent is not limited to the excerpt of that document carried automatically in its context: it can list the documents produced in the conversation, search inside one for a phrase, and read any part of it on demand, so a question about a passage buried deep in a long report is answerable. Each document is access-checked for the person asking. The full technical detail of any run — step log, per-node outputs, state and token usage — lives in the flow editor's **Run** panel, where a run links back to the conversation it ran in.
A paused sub-agent run surfaces an **Awaiting review** card in the conversation's live flow-run panel, where the user can approve or reject it (with a comment) — or **edit** what they were shown and approve that instead — without leaving chat; the panel's reopen control turns amber ("Review needed") and the conversation's sidebar row shows an amber review badge until the pause is resolved. The same pending review is also listed in the **Agent Inbox** (**Admin > AI & Agents > Agent Inbox**) for a reviewer to act on, but the conversation is a complete path in its own right — a reviewer never has to go to the inbox. Either path resumes the flow, which then posts both the decision and the run's outcome back to the conversation. See [Human-in-the-Loop](#human-in-the-loop-hitl).
## Running a Flow
### From the canvas
Use the **run panel** in the editor to start a run with parameters and uploaded files. The run executes in the background — progress, per-node status, and produced artifacts stream back to the panel. The **usage panel** shows per-node model/tool call counts and token usage — including, for **MCP action** nodes, the names of the tools the run actually called, so you can confirm it used the tool you intended, and, for any node, which **model** spent that node's tokens, with its call count and token total. A node's totals on their own cannot be attributed to either.
### From chat
When a flow is **Sub-agent enabled**, the chat agent can launch it during a normal conversation — see [Letting the chat agent run a flow](#letting-the-chat-agent-run-a-flow). The agent confirms, runs the flow in the background, maps the user's request onto the flow's parameters, binds uploaded files to the flow's input slots, and posts the result and any produced files back into the conversation when the run finishes.
### Asking to continue an earlier run
**No run can be continued once it has ended — including one that finished successfully.** Running the flow again starts a **new** run from the first step: nothing carries over, no partial output is kept, the new run does not read or revise the earlier run's output, and every approval is asked again.
So when a user refers back to an earlier run — "continue the last RFP", "pick up where it stopped", "what happened to that one" — the agent looks that run up before doing anything else. If it is still going, the agent reports its state instead of launching a second one. If it has ended, the agent says how — failed (naming the step it stopped at, where there is one), cancelled, or completed — and says plainly that what it is starting now is a fresh run, rather than describing it as a continuation. The completed case matters as much as the failed one: asked to "continue" a run that had finished, the agent was observed announcing a new run as producing an *updated* document, which is the same defect one status over. The lookup only ever returns the asking user's own runs in that conversation.
### Reading a run's progress
The two surfaces show a run at very different resolutions, on purpose.
The editor's **Run** panel is the debug surface: it lists the raw **step log** — one entry per node execution, branch suffixes and all — alongside per-node outputs, the run's state and its token usage. A **Map**/**Join** fan-out appears there as it actually executed, one entry per branch, so "which of the nine sections failed?" is answerable.
The chat flow-run panel shows only the step in flight, by name, while the run is going. A fan-out is named by the plan node driving it rather than by branch. It reports nothing about a finished run — it closes itself instead — because the run's result and its files are posted into the conversation as messages. The single exception is a document the user already has open in the panel's viewer: a run that finishes mid-read never yanks the file away, so the close waits until they close the document.
### When a run's executor stops
A run executes inside the API process; there is no worker queue behind it. If that process stops mid-run — a deploy, a pod restart, a dev-server reload — nothing is left to finish the run.
Such runs are reconciled — on the next API start, or on demand from Startup Health — and marked **Failed** with *"Run was interrupted — its executor stopped before the run finished"*, so a conversation stops looking permanently busy and its re-open control stops offering a run that nothing will ever complete. **A run parked at a Human review gate is never reconciled this way** — it is waiting for a reviewer, not orphaned, and resumes as soon as someone answers it.
Operators do not have to wait for a restart: **Admin > Monitoring > Startup Health** has an [Orphaned agent flow runs](/admin-guide/startup-health#orphaned-agent-flow-runs) check that reports how many stale runs exist and repairs them in place.
### Transient errors and retried steps
A network-level failure inside a node — a dropped connection, a read timeout — is **retried once** after a short pause, so a single blip does not have to end the step that hit it. The retry is spent inside the node's own budget: a node that has already used up its declared wall budget is not retried. An attempt the retry absorbed is recorded against that node in the run's step log, so a retry that worked leaves a trace rather than disappearing.
**The agent steps are never retried, in any pass.** *Orchestrator agent*, *Knowledge research agent*, *Web research agent* and *MCP action* each run a loop that can act on an outside system — send the mail, create the record, write through a connected integration. A retry restarts that loop from its goal, with no memory of what the failed attempt had already done, so an action that succeeded before the connection dropped would simply be performed a second time. These steps fail on the first blip instead, which is what they did before the retry existed. What that buys is worth stating exactly: it stops a whole agent loop being replayed, and nothing more. It does **not** make a step that writes exactly-once — inside the loop a tool call that failed is still retried there, and a call that failed *after* the far end had already committed looks no different from one that never landed. A flow that writes through an integration still depends on that integration to tolerate the same request twice.
**Nothing is retried in a pass that resumed from a human gate.** Re-running a step after a resume could re-ask a reviewer who has already answered, so the exclusion is deliberately blunt: it covers the whole resumed pass, not just the node holding the gate. What the retry actually covers, then, is the non-agent steps that run **before a flow's first gate** — a run that never pauses is covered end to end, and a run that pauses is covered only up to that point. Read your own flow's shape before relying on it: everything past the first gate is uncovered, so a gate drawn early leaves almost the whole run outside the retry. In the built-in **RFP Response Pipeline**, *Review the compliance matrix* sits ahead of the section fan-out, so the parallel section authoring — the part with the most branches, and the likeliest place for a blip — runs on the far side of the gate and is not retried (it is an agent step, so it would not be retried in any case).
A failure a later pass genuinely fixed no longer sinks the run either. When a **Map** branch fails and **the same Map, running the same step over the same item**, succeeds on a later pass — a loop that re-enters that map — the earlier failure is superseded and the run can settle **Completed**. The match is on the *item*, not on its position in the list, so a later pass succeeding at a different section can never clear an earlier section's failure. It is on the Map and its body step as well: a **separate revise Map, with its own body step, is different work** and supersedes nothing. That is the shape the **RFP Response Pipeline** has — re-authoring runs through a second Map rather than a second pass of the first — so a section that failed on the first authoring pass still fails the run there, however well the revise pass rewrites it. Any failure that nothing superseded still fails the run exactly as before.
### A step can be red on a run that completed
A step that **ran to completion but reported that it fell short** is recorded as **failed** and shown red, even though it produced output. Two independent signals do this, both volunteered by the step itself: a status outside the success set (a model-declared `partial`, a budget cut-off, an error envelope), or a non-empty list of block failures — a tool the agent called died, which counts regardless of what the model then claimed about its own work.
This is deliberate. Before it, the step log was seeded `success` before a node ran and only ever downgraded if the node **raised**, so an agent that returned normally while reporting its own failure logged green on every surface a human approves from — and runs shipped documents with empty and stubbed sections that nothing anywhere marked. A green run that delivered a hollow section was the defect; a red step on a run that produced a file is the fix, not a regression.
Two consequences to hold onto:
* **The run status is unchanged.** A degraded step does not raise, does not alter routing, and does not fail the run — a run whose steps degraded still settles **Completed**. Instead, the completion message posted back to the conversation appends a note naming the step that stopped early and warning that parts of the document may be incomplete. (A run that genuinely fails still reports **Failed**, and the admin **Run** panel's step log names the failing step and shows which steps never ran.) When the flow fanned out over a **Map**/**Join**, that failure no longer reports only the headline of the node that raised: the run's error message — posted back into the conversation for a sub-agent run, shown on the **Run** panel for a canvas one — also names, per **Join**, how many branches were rejected or held back for review in the last pass, how many sections are recorded, and each named section with the underlying cause, plus a separate line for sections condemned by an earlier pass that no later pass re-produced cleanly. The wording carries the distinction that tells you what to do next: *rejected* means nothing of that section survived, *held back for review* means its text is recorded and was not cleared for use. The message is budgeted to fit the chat preview and the held-back sections are named first, so what gets truncated is the failing node's own error — its full text is still on the run's step log and error list in the **Run** panel.
* **A degraded step's output is still shown.** It is not withheld the way a crashed node's is, because that content is exactly what a reviewer needs in order to judge how bad the shortfall actually is.
Step status is **last-wins**, so a branch that a later revise pass re-ran successfully clears; a step does not stay red for the rest of the run because of one earlier attempt.
Output length is deliberately **not** a degradation signal. On the runs this was built from, good sections spanned 2,844–18,339 characters and degraded ones 400–15,782 — the two classes overlap completely, so no threshold separates them and any value chosen would reject good work. A length floor is available as an opt-in per-flow setting on **Join** instead, and is a stub floor rather than a quality test.
## Human-in-the-Loop (HITL)
Flows pause whenever they reach a **Human review** node (or a Knowledge Graph node configured to interrupt on clarification). A paused run waits for a reviewer to **approve** or **reject** (with a comment) before it resumes.
Paused runs surface in the **Agent Inbox** at **Admin > AI & Agents > Agent Inbox**, which lists every run awaiting human input — newest pause first — with the paused node and a click-through to act on it. Runs you start from the canvas also surface their pause in the editor's **Run** panel.
Resuming a run is atomic: two reviewers cannot both resume the same pause.
A gate can fail without the work it guards failing. When an agent step calls **Human review** as one of its allowed blocks and that block is the step's *only* failure — the step returned normally, reported no problem of its own, and nothing but the gate died — the step is still recorded as failed and its branch still counts as failed at the **Join**, but its text is **kept** rather than thrown away, and is reported as held back for review rather than rejected. An unapproved section is not an untrustworthy one, so a flow with a revise lane gets another pass at it while the words already written stay recorded. If a content block failed in the same step, that is the whole story and the branch is treated as unusable exactly as before.
A gate that cannot reach the store where it normally parks its payload still stops the run and shows the reviewer the full payload inline, rather than dying quietly while the flow authors on.
### Editing before approving
A reviewer is not limited to a yes/no on what the node proposed. Where the review payload is a document or a block of text, the card offers **Edit**: the reviewer rewrites the text — or a document's title and each of its sections — and then approves. **The edited version is what the rest of the flow receives**, so a downstream **Generate DOCX** node renders the reviewer's wording rather than the draft they were shown. An edited card is marked **edited**, and **Revert** restores the original.
A very large payload is stored in the run record in elided form and fetched in full when the card opens; it is not editable until that full content has arrived, so nobody can overwrite a document they have not actually seen.
### What the conversation records
For a run launched from a conversation, each decision is written back as a message in that conversation: which gate it was, whether the reviewer approved or rejected it, and the note they left (a rejection with no note is recorded as having none). When the reviewer edited before approving, the message says so and carries **what was approved** — the edited version — so reading the thread back shows what actually went downstream, not the superseded draft. Answers to a mid-run question are recorded the same way. Each decision message is tied to the review it answers, so a partial resume or a re-settle cannot post it twice.
**The question and its answer are shown as one card.** Both messages are still recorded — nothing is deleted — but the transcript renders them together: the answer appears underneath the question it answers, and the question's "answer it in the run panel" call to action drops away once it has been answered, because it no longer applies. A long answer — a reviewer who rewrote a whole document before approving, say — is shown as a preview with a **View full response** control that opens the rest in the side panel. One case deliberately stays as two separate cards: an answer given *after* the user has already sent another chat message, so a verdict never appears above a later user turn.
### Several questions at once
A run can pause on more than one question at the same time — for example two **Human review** nodes that sit on parallel branches, or a review plus a Knowledge Graph clarification. When that happens:
* Every pending question is listed together, and the reviewer can answer them in any order.
* **A reviewer is never made to wait.** An answer given while the run is still applying a previous one is held and submitted automatically as soon as the run frees up — it is not refused, and there is nothing to retry. Only the card being submitted right now is briefly unavailable; questions the run still lists as pending stay answerable throughout.
* An answered card clears, and the same question does not come back. A gate the flow re-raises carrying **new** content — a revise pass returning to the same **Human review** node — is a fresh question and is shown again: a card is suppressed on *what* it asked as well as on *which* gate asked it, so a re-settle of an unchanged question stays hidden while a genuinely re-asked one comes back and has to be answered again. The second answer is recorded in the conversation as its own decision rather than folded onto the first. Unanswered questions stay on screen, and the conversation's amber review marker stays lit until the last one has been answered.
* Each question is answered on its own — there is no bulk sign-off. Approving or rejecting a card submits that card's answer along with any edits made on it, and each decision carries its own note; a free-text question with an empty answer box has no defensible default and is never answered on the reviewer's behalf.
Because each answer is applied to a specific question, an API caller resuming a run with several pending questions must say which one it is answering (see [REST API](#rest-api)); a bare value is rejected.
The review payload shown to a reviewer (and any generated documents) is cleaned of raw `[evidence:…]` citation markers, so reviewers see readable text. Inline citations are still preserved in the assistant's chat answers.
## Templates
Experio seeds starter templates you can duplicate into your own draft:
* **RFP Response Pipeline** — extracts requirements from an uploaded RFP, drafts sections, and produces a formatted response document.
* **Grunley Scope Merge** — merges uploaded scope-of-work documents into a single document.
* **Knowledge Graph Report** — answers a question from the knowledge graph as a cited narrative report.
* **RFQ Generation** — turns a scope of work into a supplier Request For Quotation, filling in an uploaded Excel RFQ template and returning the same workbook with its branding, formatting and formulas intact.
Duplicate a template from the flows list, then edit and publish your copy.
### Delivering a template fix to flows already in use
A template is a **starting point, not a live link**. "Use template" copies the template's definition into a **new, independent flow**; nothing records where that copy came from. This matters when a fix ships in a template, because the obvious assumptions are both wrong:
* **Re-seeding the templates does not change flows already created from them.** It updates the template flow only.
* **Re-publishing the derived flow does not pull the fix in either.** Publishing snapshots *that flow's own draft*, which still holds the definition copied when it was created. Versions are immutable, so nothing rewrites it in place.
The consequence worth planning for: a deploy can report success while the fix has not reached the flow that actually runs.
#### Re-seeding the templates
Templates are seeded by a management command, run from `server/`:
```bash theme={null}
pipenv run python manage.py seed_agentflow_templates
```
It is idempotent — a template whose definition is unchanged publishes nothing and logs `Unchanged template (v)`. A changed one updates the template's draft and publishes a new immutable version, logging `Updated template and published v`. A template that fails save-time validation aborts the command rather than seeding a broken definition.
Templates are matched **by name**. If a workspace somehow has two templates with the same name, the seeder updates the oldest (the one it originally created) and leaves the duplicate alone rather than guessing.
#### The seeder skips templates a person has edited
To protect a tenant's own authoring, the seeder **refuses to overwrite a template it did not write**, and says so in the log: `Skipping : staff-published v` or `Skipping : edited draft`. A template counts as staff-authored on either of two signals:
* any version of it was **published by a person** (versions the seeder creates have no publisher), or
* it has an **unpublished draft that differs** from its published snapshot — the seeder's own draft is always identical to what it published, so any divergence is a human edit.
The second signal is **sticky**. A template hand-edited once — during a demo or a review, and never published — is treated as staff-authored from then on, so it silently stops receiving template updates on every subsequent deploy. The log line is the only place this is visible. **Read the seeder's output on deploy**; a `Skipping` line means that template is now frozen for that tenant.
To override the skip, pass `--force`:
```bash theme={null}
pipenv run python manage.py seed_agentflow_templates --force
```
This **destroys** the staff edits to that template — it does not merge them. It logs loudly (`OVERWRITING staff-authored (--force): their template edits are being discarded`) so the choice is never invisible in a deploy log. Export the tenant's definition from the flow's **Edit as JSON** dialog before running it.
#### Getting the fix into the flow that runs
Re-seeding leaves you with an updated template and an unchanged flow. The direct way to close that gap is the delivery command, run from `server/`:
```bash theme={null}
pipenv run python manage.py deliver_agentflow_template --template "RFP Response Pipeline"
```
It **reports what it would do and writes nothing** until you add `--write`, and says so on the last line (`DRY RUN — nothing was written. Re-run with --write to publish.`). `--template` is repeatable and defaults to every built-in template. Derived flows are found **by name prefix**, which cannot reach a copy somebody renamed — name that one with `--flow ` instead (repeatable, and it needs exactly one `--template`). It changes which flows are considered, not what the command is willing to write to — the checks below still apply to a flow you named. A `--flow` id that matches nothing, or that names a template row, is called out as `NOT DELIVERABLE` rather than passing silently.
What it reads matters as much as what it writes. The definition comes from the **shipping code**, not from the library template row, so a template the seeder has frozen as staff-authored (the Warning above) still delivers. It **skips** a flow whose latest version a person published, or whose draft has diverged from that published snapshot, because delivery would revert that work; `--include-human-authored` overrides both, and reverting is then exactly what it does. It also skips a flow whose **live version is pinned**, and no flag overrides that one — a pin does not advance on publish, so the new version would never run. Unpin it first. A skip of either kind is logged as `SKIPPED` with the reason, so a deploy log says which flows were left alone.
Delivery validates the definition, names the nodes it would rewrite, publishes a **new immutable version** — runs in flight and paused runs keep the definition they started on — and advances the flow's draft to match, so a later publish from the canvas cannot quietly undo it. Unlike pasting a definition into **Edit as JSON**, it keeps the flow's own name and re-pins the internal id itself.
If you would rather not run a command, or the flow is one the command skips, the two manual routes still work:
* **Create a fresh flow from the updated template** (Use template), re-apply any customization, publish it, and switch the **Sub-agent** toggle and description over from the old flow; or
* **Carry the definition across by hand** — export the updated template's definition from its **Edit as JSON** dialog, paste it into the existing flow's, apply, and **publish**.
The second route keeps the flow's identity and anything already pointing at it. Two things to expect: applying a pasted definition **renames the flow** to the name inside that definition (rename it back if you don't want the template's name), and the definition's internal id is re-pinned to the flow's own automatically, so you don't need to edit it.
Either way, **publishing is the step that matters**. A flow's draft is not what runs — the live version is. Verify afterwards on the **Versions** panel that the version you just published is the one carrying the **Live** badge, and remember that runs already in flight stay on the version they started with.
## REST API
Agent Flows are also available over REST under `/api/agent-flows/` for programmatic use:
| Endpoint | Purpose |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET/POST /api/agent-flows/` | List and create flows (writes are staff-only). Add `?subagent_enabled=true` to list only sub-agent-enabled flows. |
| `PATCH /api/agent-flows/{id}/` | Update flow fields, including `subagent_enabled` and `description` (staff-only) |
| `POST /api/agent-flows/{id}/publish/` | Validate and publish the draft as a new version |
| `POST /api/agent-flows/{id}/set-live/` | Set the live version: body `{"version": }` pins live to a validated version; `{"version": null}` clears the pin so the flow follows the latest published version again (staff-only) |
| `GET /api/agent-flows/{id}/versions/` | List published versions |
| `POST /api/agent-flows/{id}/runs/` | Start a run (returns immediately; executes in the background) |
| `GET /api/agent-flows/runs/` | List runs; filter with `?surface=` (e.g. `chat`, `canvas`, `subagent`) and `?channel=` |
| `GET /api/agent-flows/runs/{run_id}/` | Poll run status, state, step log, and usage |
| `POST /api/agent-flows/runs/{run_id}/resume/` | Resume a paused (HITL) run. With several questions pending, send `{"resume": {"": }}` naming the one you are answering — a bare value returns `400`. Returns `409` while another resume is still in flight: the lease is momentarily busy and the answer is worth re-sending. Returns `410` with a detail naming them when the answer names only questions that are no longer pending — already answered, or the run has moved on — and nothing about that request can succeed on a retry. A map naming several questions of which only some are still pending applies the ones that matched; the rest are dropped and recorded only in the server log, not on the response. |
| `GET /api/agent-flows/pending-hitl/` | List runs awaiting human input |
| `GET /api/agent-flows/node-types/` | Node registry metadata for the canvas palette |
A flow's JSON carries `subagent_enabled` (whether the chat agent can launch it) and its live-version pointers: `live_version` (the pinned version id, or `null` when the flow follows the latest published version) and `live_version_number` (the effective live version number).
Reads are available to authenticated users; creating, editing, and publishing flows require staff permissions.
# Flow Nodes Reference
Source: https://docs.experio.cloud/admin-guide/agent-flows-blocks
Every Agent Flows node explained in plain language — what each node does, when to reach for it, its key settings, and the data it takes in and gives out.
This is the field guide to the **nodes** you drag onto the Agent Flows canvas. Each node does one job; you wire them together into a graph that compiles to a real LangGraph agent. Everything below is generated from the live node registry, so the names, settings, and ports match exactly what you see in the canvas.
New to Agent Flows? Read [Agent Flows](/admin-guide/agent-flows) for the canvas basics (creating, publishing, enabling a flow as a chat sub-agent). For **end-to-end example flows you can copy**, see the [Agent Flows Cookbook](/admin-guide/agent-flows-cookbook). This page is the reference for the individual nodes those recipes use.
## How data flows between nodes
Two different things connect your nodes, and it helps to keep them separate in your head:
* **Edges = order.** An edge (the line you draw from one node to the next) decides *when* a node runs. `start → A → B → end` means run A, then B. Reading another node's value counts toward that order too (see the note below), but the edge is what you should rely on.
* **Bindings = data.** A binding decides *what value* feeds a node's input port. You bind an input to one of these sources:
| Bind an input to… | Path you'll see | What it is |
| ------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Chat message** | `$.params.message` | The user's core request, forwarded by the chat agent when it launches the flow (or the message you supply in the Run tab). |
| **A run parameter** | `$.params.` | Any value provided when the flow is run — from the Run tab, or mapped from the user's request by the chat agent when it launches the flow. |
| **Another node's output** | `$.nodes..outputs.` | A value produced earlier in the flow (e.g. `$.nodes.kg.outputs.text`). |
| **An uploaded file slot** | `$.files.` | Files attached to the run, or the conversation's uploads bound in when the chat agent launches the flow. |
**You rarely type these paths by hand.** When you connect two nodes, the canvas **auto-wires** compatible ports for you. To send one specific value, **drag from an output port to an input port**. Prompt fields also accept inline templates — `{{ params.message }}`, `{{ inputs.context }}`, `{{ nodes..outputs.text }}` — so you can weave values straight into the text.
A node with **two incoming edges waits for both** before it runs. That's how you fan two parallel branches back into one node (e.g. a Knowledge Graph branch and a Web research branch both feeding one `LLM generate`). You only need the explicit **Join** node when you fanned out with **Map**.
**Reading another node's output counts as a dependency too** — whether you bound it (`$.nodes..outputs.`) or spliced it into a prompt (`{{ nodes..outputs. }}`) — which pushes the reading step later in the run. Treat that as a safety net, not a guarantee: when the reader and the node it reads are *both* waiting on other branches, they can still end up running together, and the reader gets nothing. **Draw the edge as well** — it's the only reliable way to express the order, and the canvas won't flag the missing one for you when you validate or publish.
If a step fails, the steps that read its output **don't run on empty input** — the run stops with an error naming the source that failed, instead of carrying on and producing a hollow answer that still reports success.
Nodes fall into five categories in the palette: **Control**, **Data**, **LLM**, **Agents**, and **Human**. Here's every one.
***
## Control nodes
The plumbing: where a flow starts and ends, and how it branches, repeats, and fans out. Control nodes don't call a model — they're fast and deterministic.
| Node (`type`) | What it does | When to use | Inputs → Outputs |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Start** (`control.start`) | The entry point. Exposes the run's params and uploaded files. | Every flow has exactly one. | — → `params`, `files` |
| **End** (`control.end`) | The exit point. Collects the flow's final result, and can refuse to end empty-handed. | Every flow has exactly one; bind its `result`. | `result` → `result` |
| **Condition** (`control.condition`) | Tests one value and produces true/false to route on. | Send the run down different paths based on a value. | (reads a state path) → `result` (bool), `value` |
| **Merge** (`control.merge`) | Rejoins branches after a Condition; forwards the branch that actually ran. | After a Condition fan-out, to get back to one path. | one input per branch → `value`, `values` |
| **Map** (`control.map`) | Fans a **list** out to parallel branches — one per item. | Do the same step over many items at once. | `items` (required) → `items`, `count`, `item`, `index` |
| **Join** (`control.join`) | Collects the results of every Map branch once they all finish, and reports whether the fan-out actually produced anything. | Always paired with a Map. | — → `results`, `results_by_name`, `count`, `ok`, `failed_indexes`, `failure_reasons`, and (once the Join has held something back) `condemned_items`, `discarded_items` |
| **Loop** (`control.loop`) | A bounded counter that lets a cycle repeat a fixed number of times, then stops. | Sequential repeat (e.g. revise-until-approved). | — → `iteration`, `exhausted` |
**Condition** (`control.condition`) — set **Value path** to the value you're testing (e.g. `$.nodes.kg.outputs.row_count`), an **Operator** (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`, `truthy`, `falsy`), and a **Comparison value** when the operator needs one. The edges leaving a Condition decide which branch runs on true vs false.
**Map** (`control.map`) — set **Body node** to the node that runs once per item, and **Join node** to the `control.join` that collects the results. Inside the body, read the current item with `$.nodes..outputs.item` (and `.index`). The list you bind to **items** is fanned out in parallel.
**Join** (`control.join`) — the other half of a Map (the Map's **Join node** setting is what points at it). It collects every branch once they've all settled, and two settings tune what it counts as a branch that *produced something*: **Body output port** (default `text`) names the port on the map body that carries the branch's content, so the Join knows where to look when it judges whether a branch came back empty; **Minimum body length** (default `0`, off) puts a floor on that port's length.
`results` is one entry per branch in item order and `count` is how many branches there were. `results_by_name` keys the usable results by the map *item* they came from and accumulates across loop passes, so a second, narrower pass that re-authors three sections doesn't throw away the twelve an earlier pass got right. Only branches that came back with usable content are recorded there — so a degraded re-run never evicts a good earlier result, and `results_by_name` and `ok` can legitimately disagree. Failing a branch and dropping its content are two different decisions: a branch that is failed for how it *finished* keeps its content in `results_by_name` — for an item nothing has recorded yet — because evicting a complete section replaces a defect you can see with a hole you cannot. Where an earlier pass already recorded that item, the condemned re-author is thrown away instead and the earlier result stands: the map holds the best known good result per item, so a condemned second attempt must never replace work an earlier pass got right. A branch the Join cannot key (the Map's item list doesn't name it, or two items share a name) is counted as failed rather than quietly left out: a result that never reaches `results_by_name` is a section missing from whatever you assemble from it.
**Route on `ok`, not on `failed_indexes`.** `failed_indexes` is built from the branch count, so a Map that fanned out over an *empty* list produces an empty `failed_indexes` that reads exactly like "every branch succeeded" — and a Condition testing it sends the run on to assemble a document with no sections in it. `ok` is true only when at least one branch ran, none of them failed, **and** nothing is still held back on `condemned_items` or `discarded_items` — which is the check you actually want. Those last two terms are folded in because `count` and `failed_indexes` are rebuilt from the branch count on every execution and so describe the *current* pass only, while `condemned_items` and `discarded_items` survive it: a later, narrower pass that re-authors three sections and comes back clean must not flip `ok` true while an earlier section is still unapproved or missing. `failure_reasons` maps a branch index to why that branch counts as failed, for a message or a log.
`condemned_items` and `discarded_items` name *which planned sections* went wrong, where `failed_indexes` only gives you positions. `condemned_items` names the map items whose own branch failed for routing — a skipped sign-off, a dead approval gate, meta-commentary — and whose text that branch did record, so the section can still be assembled but is unapproved. `discarded_items` names planned items this Join failed and recorded **nothing** for, so there is no text at all: wording that blames a discarded section's approval gate is false, it was degraded and dropped. Both survive the pass that produced them, and both are released the same way — a later pass that re-produces the item with no failure signal drops it from `condemned_items`, and any pass that records something under the name drops it from `discarded_items`. Feed both back into whatever chooses the sections to re-author, and bind both anywhere you bind one, each with a `default` of `[]`, because the ports are absent until the Join first holds something back — which is what the **RFP Response Pipeline** template does.
**What counts as a failed branch.** A branch that crashed, one whose step reported degraded work (including an agent that never called `finish` — see below), one whose body port came back empty, one whose agent had a block tool fail *and never got it working*, one whose body falls under **Minimum body length**, and one whose body carries meta-commentary — the agent narrating its own tooling inside the deliverable (a grounding disclaimer, a raw traceback, a leftover TODO marker). This is simply how a Join behaves: there is no per-flow strict switch and no deployment-wide setting to turn it on, so `ok` means every branch came back with real content in it, not just that nothing crashed.
**An agent that skipped its sign-off keeps its work.** An orchestrator is told it must call `finish` when it's done. When the loop ends without that call the branch is reported as failed and goes back through your revise lane — that really is off-policy and you should see it — but what the agent *wrote* is still recorded in `results_by_name`, because a missing tool call says nothing about the section. It is judged on the same signals as any other branch: an empty body, a body under **Minimum body length**, or a block tool that died still drops it. This is only about the sign-off; an agent that reports `partial` is telling you the work itself fell short, and that branch is dropped as before.
**A dead approval gate holds a section back rather than deleting it.** A branch whose *only* unresolved failing block is a `human.review` gate is routed as failed, but it keeps its content in `results_by_name`: the gate authors no prose, so its death means the section is **unapproved**, not untrustworthy, and — when nothing else in the body is at fault — the reason recorded for that branch in `failure_reasons` reads `approval gate did not complete (N block failure(s)); section unapproved`. The carve-out is deliberately narrow — it applies only when the step was degraded rather than raised, only when *every* unresolved failing block is that gate, and only when the branch's own status is clean. A branch reporting `partial`, or one that also skipped `finish`, is content-fatal and dropped, as a `partial` branch always was, and a failed knowledge or content block still evicts.
**A tool the agent retried successfully doesn't count against it.** A transient error — a graph database blip mid-run — makes the agent's tool call come back as a failure, and the agent's documented next move is to try again. When the retry works, the branch is a branch that recovered, not a degraded one, so it isn't held against the section or the run. The blip is still recorded and still visible on the step; it just doesn't fail the work that came out correct.
**Text your flow quoted isn't your agent's own words.** A `[TODO: ...]` or `[placeholder: ...]` marker inside quotation marks, a blockquote, or a code fence reads as material quoted from the customer's own document — quoting an RFP's boilerplate back at it is what a compliance response *does* — so it doesn't fail the branch. The agent narrating its own tooling (a grounding note, an "as an AI" hedge, a traceback) is never excused this way, however it's wrapped. Note the carve-out only covers *marked* quotation: a marker spliced into the agent's own sentence still counts as the agent's own.
**A body port that isn't there is judged on intent.** If you *set* **Body output port**, you're declaring where the content lives, so a branch that comes back without that port is a failed branch. If you leave it at the default `text`, a body node that produces no `text` port at all is not held against the branch — that body simply doesn't use it, and the branch is judged on its other signals (step status, block failures). This is what lets a Map whose body is, say, a Data block work normally; publish-time validation rejects the two definitions where this can only be a mistake — a body that cannot produce the port you named, and a **Minimum body length** on a port the body never produces (the floor could never fire).
**Minimum body length is a stub floor, not a quality bar.** In the runs this was measured against, good sections and degraded ones overlapped completely in length (2,844–18,339 characters against 400–15,782), so no threshold cleanly separates them. Set it low enough to catch an obvious stub, and expect a high value to reject good work.
**Loop** (`control.loop`) — set **Max iterations** (1–1000). Every cycle must pass through the Loop node; once the count hits the max, `exhausted` flips to true so you can route out of the loop.
**End** (`control.end`) — **End** is where the flow's answer comes from, so always bind its `result` to whatever produced the final value. Two optional settings decide what happens when nothing did: **Require a result** (default off) fails the run when every bound input resolves to nothing, and **Failure message** is what the run reports when it does.
Turn **Require a result** on for any flow whose whole point is a deliverable. A route that skips the producer — an exhausted revise loop, a rejected approval — reaches the exit with its binding unresolved, and because nothing *crashed* the run would otherwise settle **Completed** with no document attached. "We gave up after three revision cycles" and "here is your document" should not look the same on the runs list. Leave it off for a flow whose job is a side effect (post a message, write a row) and for anything that can legitimately end on `0`, `false` or an empty list — the check is "the binding resolved to nothing", not "the value is falsy", so those still pass either way.
**Start** and **Merge** need no settings in the simple case — just wiring. **Merge** has two optional ones for a merge a loop can revisit; see below.
**A Merge a loop can re-enter needs a selector.** By default a Merge forwards the first bound input that isn't empty, which is correct when the merge is reached once. Node outputs accumulate, so on a second pass through a Condition the branch you took the *first* time is still sitting there — and it keeps winning. Set **Selector path** to the Condition's own `result` (e.g. `$.nodes.route.outputs.result`) and **Selector map** to `{"true": "", "false": ""}`, and the branch that ran *this* pass is the one that's forwarded. If the branch it names produced nothing, the Merge forwards nothing rather than falling back to the abandoned branch: an empty value is visible downstream, a stale one is not.
***
## Data nodes
These work with your content: query the knowledge graph, read the conversation a run came from, index uploaded files, and render documents.
| Node (`type`) | What it does | When to use | Inputs → Outputs |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Knowledge Graph Query** (`knowledge_graph`) | Asks your organization's knowledge graph a question; returns rows and (optionally) a cited written answer. | Grounded Q\&A over your own data (projects, deliverables, contracts, people…). | `query`, `custom_instructions` → `rows`, `row_count`, `entities`, `cypher`, `text`, `evidence`, … |
| **Knowledge Graph data** (`kg.data`) | Scopes and queries the knowledge graph and returns the structured results — no written report. The simpler sibling of Knowledge Graph Query. | Feed graph rows and resolved entities into downstream nodes without any report machinery. | `query`, `custom_instructions` → `rows`, `row_count`, `resolved_entities`, `intent`, `validated_cypher`, `graph_context`, `retrieval_quality`, `scope_recommendation`, `clarification_question` |
| **Knowledge Graph Display** (`kg.display`) | Turns a `kg.data` node's rows into a cited written report — the same report writer as Knowledge Graph Query, but over rows an upstream `kg.data` already retrieved (no re-querying). | Pair with `kg.data` when you want the graph data and its narrative as two wired steps you can branch or inspect between. | `rows` (required), `resolved_entities`, `intent`, … → `text`, `evidence` |
| **Index uploads** (`data.index_files`) | Parses, chunks, and embeds uploaded files so later nodes can use their content. | When a flow starts from uploaded documents. | `message_file_ids` (required) → `indexed`, `skipped`, `indexed_count`, `documents` |
| **Read conversation** (`data.channel_history`) | Reads the recent messages of the chat the run was launched from, as a transcript. | When the flow needs what the user and the agent already established earlier in the conversation. | — → `transcript`, `messages`, `message_count` |
| **Generate DOCX** (`data.generate_docx`) | Renders text or structured sections into a downloadable Word document. | Turn an answer into a polished `.docx` deliverable. | `title`, `sections`, `text` → `file_path`, `filename`, `size_bytes`, `artifact_key` |
| **Generate PPTX** (`data.generate_pptx`) | Renders sections into a downloadable PowerPoint presentation (one slide per section). | Turn an answer into a `.pptx` deck. | `title`, `sections`, `text` → `file_path`, `filename`, `size_bytes`, `artifact_key` |
| **Generate file** (`data.generate_file`) | Renders content into a chosen format — Word, PowerPoint, PDF, or HTML — reusing the same generators. | One node when the output format is configurable (or PDF/HTML). | `title`, `sections`, `content`, `text` → `file_path`, `filename`, `size_bytes`, `artifact_key` |
| **Read Excel template** (`data.inspect_xlsx`) | Describes an uploaded Excel template — the fields it prints, its line-item table, and the values any looked-up column will accept. | Before filling a spreadsheet, so an LLM node can draft against the form's own vocabulary. | — → `text`, `schema`, `field_count`, `row_capacity` |
| **Fill Excel template** (`data.generate_xlsx`) | Writes values into an uploaded Excel template and returns **the same workbook** — branding, formatting and formulas intact. | Produce a filled-in copy of a client's own spreadsheet (an RFQ, a pricing sheet, a return form). | `fields`, `blocks`, `rows`, `cells` → `file_path`, `filename`, `size_bytes`, `artifact_key`, `filled_count`, `rows_written`, `skipped` |
**Knowledge Graph Query** (`knowledge_graph`) — the workhorse for answering from your own data. Key settings:
* **Output mode** — `data` returns rows only; **`report`** additionally synthesizes a cited narrative on the `text` output (with `evidence`). Pick `report` when you want a written answer, not just a table.
* **Query** — the question. Leave it templated or bind the **`query`** input to **Chat message** so the user's question drives it.
* **On clarify** — what to do when the question is too vague to scope: `fail` (stop), `interrupt` (pause for a human), or `skip` (continue with empty rows).
* **Fail on empty**, **Include graph context**, **Custom instructions**, and report-tuning options (**Output instructions**, preview/token caps) are available for finer control.
Large result sets (over \~1,000 rows) are automatically exported to a CSV file artifact and surfaced on the `rows_file` output, so the run stays lightweight.
**Knowledge Graph data** (`kg.data`) — the same scope-and-query as Knowledge Graph Query, minus the report writer. Set the **Query** and optional **Custom instructions**, and choose an **On clarify** policy (`fail` / `interrupt` / `skip`) exactly as above; **Fail on empty**, **Include graph context**, and **Document search K** (how many parallel document searches run during scope — default 3, `0` to disable) tune the retrieval. It exposes the scope and retrieval outputs directly — `resolved_entities`, `intent`, `validated_cypher`, `graph_context`, `scope_recommendation`, `clarification_question`, plus `rows`/`row_count`/`retrieval_quality` — with the same automatic CSV export on `rows_file` for large results. Reach for it when a downstream node consumes the graph data itself. When you also want a written narrative, you have two routes: keep the data step and wire a **Knowledge Graph Display** node after it (below), or collapse both into a single **Knowledge Graph Query** in `report` mode.
**Knowledge Graph Display** (`kg.display`) — the report half of the pair. Wire **`kg.data → kg.display`** and it runs the deep agent's report writer over the rows `kg.data` already retrieved — no second query — to synthesize a main-agent-quality, cited narrative on `text` (with an `evidence` index). The canvas auto-wires the ports it reads from `kg.data` (`rows`, `resolved_entities`, `intent`, `validated_cypher`, `graph_context`, `scope_recommendation`, `clarification_question`); **`rows` is the only required input**. Configure the **Query** it answers (falls back to the upstream `intent` when left blank), optional **Output instructions** (formatting and guidance for the report writer — the report-only Display node has no scope phase, so it takes no separate custom instructions), the report's **row budget** and **token budget** (`report_data_preview_limit`, default 300; `report_max_data_tokens`, default 50,000), and an optional **model override** for the writer. To surface the answer, finish the pattern **`start → kg.data → kg.display → end`** and bind **`end.result ← kg.display.text`** — the `evidence` index rides along with the text, so the grounded `[evidence:…]` citations render as **clickable source chips** in the run output panel (document chips link out to the source; graph-entity chips show the entity by name).
**One node or two?** Use **Knowledge Graph Query** (`report` mode) for a straight question → cited answer in a single block. Split it into **`kg.data → kg.display`** only when you need the rows *between* the two steps — to branch on `row_count`, inspect or transform the data, or feed it somewhere else as well as report on it.
**Time limits.** The knowledge-graph nodes are capped on wall-clock time, and a node that runs past its cap fails with a timeout instead of returning partial results: **Knowledge Graph Query** and **Knowledge Graph data** get **600s**, **Knowledge Graph Display** **300s**. The cap applies to every run of the flow, including versions published before it existed. You can't see or change these from the Inspect panel — its **Budget** editor only appears on the agent nodes — but you can raise one through **Edit as JSON** by setting `budget.max_wall_seconds` on the node.
**Index uploads** (`data.index_files`) — bind **message file ids** to an uploaded file slot (`$.files.`). It indexes the files and also emits short text previews on `documents` that downstream LLM nodes (like `llm.extract`) can read directly.
**Read conversation** (`data.channel_history`) — nothing to bind: it reads the chat the run was launched from. Set **Messages to read** (how many of the most recent messages to take — default 20, up to 200; they come back oldest-first), **Whose messages to include** (`all`, `user` for only what the person typed, or `assistant` for only the agent's replies), a **Per-message character cap** (default 40,000; `0` disables truncation), and **Fail when the conversation is empty** (on by default — leave it on when a downstream node depends on the transcript, since an empty transcript renders as an empty prompt and yields a hollow document that still reports success). `transcript` is the plain-text `User: … / Assistant: …` version to feed an LLM node; `messages` is the same turns structured as `{message_id, role, text, truncated, created}`, with `message_count` alongside.
**Read conversation** only works on a run **launched from a chat**. A run started from the canvas **Run** tab (or the REST API) has no conversation to read, so the node fails with a "requires a channel-scoped run" error. It also only ever reads a conversation the person who started the run can open — one they own, or one shared with them directly or through a folder. Anything else fails with an authorization error rather than being read — distinct from the empty-conversation failure above, so you can tell the two apart in the run's step log.
**Generate DOCX** (`data.generate_docx`) — bind **sections** (a list of structured `{heading, content}` objects) for a multi-section document, or bind **text** for a single-section body (e.g. a Knowledge Graph report). Set the **Document title**, optionally **Include table of contents**, and choose a **Theme** from the eight built-in ones. To brand the output, pick a **Document Template** by name (one of your registered [Document Templates](/admin-guide/document-templates)); the document then inherits that template's styles, headers/footers, and logo.
**Generate PPTX** (`data.generate_pptx`) — the slide equivalent of Generate DOCX: bind **sections** (one slide each) or **text**, set the **Title**, and optionally pick a **PPTX template** by name for branded slide masters. (A **Theme** is accepted for parity with DOCX but does not currently affect slide styling — use a template to brand slides.)
**Read Excel template** (`data.inspect_xlsx`) and **Fill Excel template** (`data.generate_xlsx`) — a pair for filling a spreadsheet you already have. Unlike the Generate nodes, which build a document from scratch, these edit an existing workbook in place: only the cells you fill change, and the file keeps its images, styles, conditional formatting and formulas.
Point both at the same **Template name** (an [Excel Document Template](/admin-guide/document-templates)) and, optionally, list the **Sheets** to limit them to.
*Read Excel template* produces a plain-English description of the form on `text` — the labels it prints, the columns of its line-item table, and, for any column the workbook looks up (a commodity code, say), the exact set of values that will resolve. Bind that to an LLM node's **context** and the model drafts using the form's own wording instead of guessing.
*Fill Excel template* takes that draft back:
* **fields** — single-value fields, as `{label, value}` entries or a `{label: value}` object. Matched against the labels the form prints, so `E-Mail Address` and `email address` both land.
* **blocks** — multi-line boxes, as `{label, lines}`. For a caption printed *inside* its own box (a `TO:` address block), the lines go in the rows beneath it and the caption is left alone.
* **rows** — line items, each keyed by the table's own column headers.
* **cells** — exact overrides as `{sheet: {A1: value}}`, when you already know where a value goes.
Dates land as dates, lookup keys keep their leading zeros, and columns the workbook computes for itself are never overwritten — their cached results are refreshed instead, so the file reads correctly everywhere and still recalculates in Excel.
Both blocks stop the run rather than hand back a workbook that only looks filled. **Fail if nothing was filled** is checked per region — filling the header is not evidence the line items filled — and a value outside the workbook's allowed set is an error, not a silent skip, because a blank there leaves every computed column empty.
**Generate file** (`data.generate_file`) — a single node with a **format chooser**: **Word (`.docx`)**, **PowerPoint (`.pptx`)**, **PDF**, or **HTML**. It reuses the same generators as the dedicated DOCX/PPTX nodes. The **Document Template** selector appears only for the `docx` and `pptx` formats. For `pdf`/`html`, bind **content** to the format-native source (LaTeX for PDF, HTML for HTML); for `docx`/`pptx`, bind **sections** (or **text**) as you would for the dedicated nodes.
Markdown that shows up in generated content — `#`/`##`/`###` headings, `**bold**`/`*italic*`, `-`/`1.` lists, and **pipe tables** (`| Col | Col |` with a `|---|---|` separator row) — is rendered as **real Word/PPTX styling** rather than left as literal markers in the document. A pipe table becomes a proper Word table with a header row and borders; in PPTX, where slides have no table layout, each row is rendered as a line.
**A document node won't hand back an empty deliverable.** **Generate DOCX**, **Generate PPTX**, and **Generate file** each have **Fail when there is nothing to write**, on by default. With it on, a node given no **sections** and no **text** — or a list of sections with nothing in any of them — stops the run with an error saying it *was given nothing to write … so the result would be a title-only document reporting success*, instead of writing a title-only file and reporting success anyway. Untick it only when a shell with no body is genuinely the output you want.
The check knows what each format can actually render. A section carrying only a **table** counts as content for `.docx`, where the writer renders it as a real Word table, but not for `.pptx`, where a slide has no table to fill and the section would come out blank. (A markdown table written inside a section's **content** does render on a slide, so it counts either way.) For the **PDF** and **HTML** formats of Generate file — which take one source string rather than sections — the same setting refuses an empty body.
The hidden phases of Knowledge Graph Query — `kg.scope`, `kg.retrieve`, `kg.report` — are covered under [Advanced / hidden nodes](#advanced--hidden-nodes) below. You don't need them for normal flows.
***
## LLM nodes
Single-call language-model steps. Each makes **one** model call from a prompt (plus optional `context` input) — predictable cost, no looping. All three accept `{{ params.* }}`, `{{ inputs.* }}`, and `{{ nodes..outputs.* }}` templates in their prompts.
| Node (`type`) | What it does | When to use | Inputs → Outputs |
| --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------- |
| **LLM generate** (`llm.generate`) | Writes free-form text from a prompt. The general-purpose writer. | Summarize, fuse sources, draft prose, rewrite. | `context` → `text` |
| **LLM extract** (`llm.extract`) | Pulls structured, typed fields out of content into a schema you define. | When downstream needs machine-readable data (lists, fields). | `context` → `data` (object) |
| **LLM classify** (`llm.classify`) | Picks exactly one label from a list you define. | Routing and tagging. | `context` → `category`, `reasoning` |
**LLM generate** (`llm.generate`) — set the **Prompt** (required) and optionally a **System prompt**. Bind **context** to whatever you want appended to the prompt (e.g. a Knowledge Graph report). Output lands on `text`.
**LLM extract** (`llm.extract`) — set the **Prompt** plus an **Output schema**: a map of `{name: {type, description, required}}` where `type` is `string | number | integer | boolean | array | object`. The model is forced to return exactly that shape on the `data` output. Example schema:
```json theme={null}
{ "requirements": { "type": "array", "description": "All RFP requirements", "required": true } }
```
**LLM classify** (`llm.classify`) — set the **Prompt** and a list of **Categories** (at least two, unique). The output `category` is guaranteed to be one of your labels, with a one-line `reasoning`. Wire `category` into a **Condition** to route.
All three take an optional **Model** (defaults to the global reasoning model), an **Output format** hint, and **Include graph schema** (adds a compact knowledge-graph schema to the prompt when the step reasons over graph data).
***
## Agent nodes
These run a **bounded, model-driven loop** — the model decides its own steps using tools until it finishes or hits its budget. Because they loop, every agent node has a **budget** (max model calls, tool calls, tokens, wall-seconds) so it can never run unchecked. Unset budget fields fall back to sensible defaults.
| Node (`type`) | What it does | When to use | Inputs → Outputs |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| **Knowledge research agent** (`agent.deep_research`) | Self-directed, multi-step investigation over your **internal** knowledge (graph + ingested documents). No web. | Deep questions over internal data needing several queries. | `context` → `status`, `text`, `structured`, `usage` |
| **Web research agent** (`agent.web_research`) | Self-directed investigation over the **live web** (Tavily), with cited source URLs. | Current or external information. | `context` → `status`, `text`, `structured`, `usage` |
| **Orchestrator agent** (`agent.orchestrator`) | A coordinator that calls **other nodes as tools** and decides the path at runtime. | Open-ended tasks where you can't predict the steps. | `context` → `status`, `text`, `structured`, `plan`, `evidence`, `usage` |
| **MCP action** (`mcp.action`) | Runs a bounded loop over the tools of **one MCP server** (Gmail, HubSpot, time, …). | Take actions or fetch data through a connected integration. | `context` → `status`, `error`, `result`, `usage` |
**Knowledge research agent** (`agent.deep_research`) — set the **Research goal** (templatable; use `{{ params.message }}` to use the chat message). By default it uses the proven internal-knowledge tool set (knowledge graph + Cypher); you can override **Tool groups** or add one **MCP** server. **Report format** `structured` returns a typed `{title, summary, sections}` report. Defaults: 40 model calls / 80 tool calls / 1800s.
**Web research agent** (`agent.web_research`) — set the **Research goal**. It runs live web searches via Tavily and grounds every claim in cited URLs. Leave **Tavily API key** blank to use the org-level `TAVILY_API_KEY` configuration setting (recommended). Defaults are lighter than internal research: 30 model calls / 60 tool calls / 900s.
**Orchestrator agent** (`agent.orchestrator`) — set the **Goal** and **Allowed nodes** (the registry node types it may call as tools, e.g. `knowledge_graph`, `agent.web_research`, `data.generate_docx`). At runtime it plans with a todo list and chooses which nodes to call. Outputs include the final `text`, the `plan` it followed, and `evidence` (citations from report-mode node tools). Optional **Require plan approval** pauses the run the first time the agent proposes its todo list, before that plan is applied: a reviewer sees the proposed steps and either approves them — the agent then works through that plan — or **rejects them with feedback**, which goes back to the agent so it re-plans (a rejection does not end the run). The pause travels the same path as a **Human review** — it surfaces wherever the run is being watched, and is picked up and resolved the same way (see **Human review** below) — and, like Human review, it needs a **checkpointed run**, which is the default for a published flow. Defaults: 20 model calls / 30 tool calls / 900s.
**MCP action** (`mcp.action`) — set the **MCP server** and a **Task instruction**. Choose **Auth source** (`user` = the run-user's connection, `org` = organization integration, `auto` = user first then org), optionally **whitelist tools**, and set **Allow writes** if the task should mutate external systems. Write-capable tools are flagged in the tool list; when **Allow writes** is off, the builder warns which selected write tools would be **dropped at run time** — they are stripped so a read-only step can never mutate anything, and the run is reported as a failure rather than a false success if the task then can't complete. **On error** controls failure handling: `fail` (halt), `error_port` (emit the error envelope for conditional routing on `status`), or `continue`. Defaults: 10 model calls / 15 tool calls / 300s.
MCP servers that need OAuth (Google, Slack, HubSpot…) must be **connected first** under Admin → MCP Servers. No-auth servers (e.g. time, sequential-thinking) work immediately.
***
## Human nodes
| Node (`type`) | What it does | When to use | Inputs → Outputs |
| --------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
| **Human review** (`human.review`) | Pauses the run so a person can approve, edit, or reject, then resumes. | Anything that needs sign-off before it finalizes. | `payload` → `approved`, `decision`, `content`, `edited`, `comment`, `response` |
**Human review** (`human.review`) — bind **payload** to the data you want the reviewer to see (e.g. a draft). When the run reaches this node it pauses and surfaces the item wherever the run is being watched: for a sub-agent run that means the **flow-run panel in the conversation itself** (the reviewer never has to leave chat), and for every run it is also listed in the **Agent Inbox** for staff. On approval the flow resumes; on rejection you can route back to a revise step. Set a **Review title** and **Reviewer message** for context, and choose the **Review layout** (`inline_summary` or `artifact_download`).
**Reviewer note.** On the review card — whether it appears in the canvas **Run** panel, the in-chat flow-run panel, or the **Agent Inbox** (it's one shared card) — the reviewer can add a **note**. The note is **optional when approving** and **required when rejecting**, so a rejection always comes back with a reason.
**Editing, not just approving.** The review card is not read-only. Where the payload is something the card can render as fields, an **Edit** button turns it into an editable form: a single text value becomes a text box, and an assembled document (`{title, sections}`) becomes a title field plus a heading and a body box for every section. An edited card carries an **edited** badge and a **Revert** button, and the edit travels with the verdict on **Approve and continue** *or* on **Reject**.
**Outputs.** Alongside `approved` (boolean) and `response` (object), the node exposes first-class ports for the reviewer's decision *and* for what they actually reviewed:
| Output | Type | Value |
| ---------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
| `approved` | boolean | `true` on approve, `false` on reject |
| `decision` | string | `"approved"` or `"rejected"` |
| `content` | any | The reviewed payload — the reviewer's edited version when they edited it, otherwise exactly what they were shown |
| `edited` | boolean | Whether the reviewer changed the payload |
| `comment` | string | The reviewer's note (empty if they approved without one) |
| `response` | object | The full review payload (decision + edits) |
Downstream nodes consume the note by binding `{{ nodes..outputs.comment }}` into a prompt — e.g. a revise / LLM step that applies the feedback — and can route on `{{ nodes..outputs.decision }}` (or the existing `approved`). This feeds the reviewer's feedback straight into the revise loop or the next LLM node.
**Bind the next node to `content`, not to the draft the reviewer was shown.** This is the one thing that is easy to get wrong with this block. If the DOCX node after your review gate still binds the upstream draft (`$.nodes..outputs.text`), the reviewer's edit is accepted and then silently ignored: the run reports success and delivers the unedited document.
Bind `$.nodes..outputs.content` instead — and you can reach into it. The **RFP Response Pipeline** template does exactly that: its DOCX node binds **title** to `$.nodes.review_final.outputs.content.title` and **sections** to `$.nodes.review_final.outputs.content.sections`. On the un-edited path `content` is simply what the reviewer saw, so binding it is always correct — edit or no edit.
**Not every payload can be edited.** The card offers **Edit** only when the payload is a document (`{title, sections}`) or a **single** input value that is plain text. Bind **payload** to a multi-field object and the reviewer gets a read-only card; `content` then just echoes what they were shown. Editing is also unavailable while the full payload is still loading, so nobody can save a truncated preview over the real content.
**A resume must carry a verdict.** An answer to a Human review gate that is an object but carries no approve/reject verdict — a hand-rolled REST call with only a note, say — is refused rather than guessed at: the resume comes back `400` naming the gate, the review stays parked and the run stays paused. It does not take the reject branch. To reject on purpose, send `approved: false`, or send a bare `false` as the whole resume value; both are explicit verdicts and both are accepted (the bare form only when that gate is the one thing the run is waiting on). The review card always sends the verdict explicitly, so nothing changes for a reviewer working in the UI. The node itself fails closed for the same reason: an answer that reaches it with no `approved` key resolves to not approved.
Human review requires a **checkpointed run** (the engine has to be able to pause and resume) — this is the default when a flow is published and run.
***
## Choosing the right node
A few decisions come up again and again. Here's how to pick.
### Which LLM node — classify, extract, or generate?
| You want… | Use | Output |
| ------------------------------------------------------------- | ---------------- | -------------------------------------- |
| To pick **one label** from a fixed set (then route on it) | **LLM classify** | `category` (always one of your labels) |
| **Structured data** (fields, lists) the next node can consume | **LLM extract** | `data` (matches your schema) |
| **Free text** — a summary, a fused answer, a draft | **LLM generate** | `text` |
Rule of thumb: *classify* when you'll branch on the result, *extract* when a downstream node (Map, Generate DOCX, a table) needs typed values, and *generate* for anything that should read as prose.
### Map (parallel) vs Loop (sequential)
* **Map** runs the same step over **many items at the same time** — fast, and each branch is independent (e.g. author every section of a document in parallel). Always pair it with **Join** to collect the results.
* **Loop** repeats a step **one pass at a time**, up to a max count, where each pass can depend on the last (e.g. revise-until-approved, or a fixed number of retries).
Use Map for throughput over a list; use Loop for an iterative cycle that has to happen in order.
### Condition + Merge (branch, then rejoin)
Use them as a pair: a **Condition** evaluates a value and routes the run down one of several branches; each branch does its own work; a **Merge** converges them back to a single path (it forwards the one branch that actually ran). Bind one input on the Merge per upstream branch. A common shape: classify → Condition → (branch A | branch B) → Merge → end.
### Knowledge Graph Query vs Knowledge research vs Web research vs Orchestrator
All four can "answer a question," but they trade off determinism, depth, and source:
| Node | Source | Style | Reach for it when… |
| ---------------------------------------------------- | -------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Knowledge Graph Query** (`knowledge_graph`) | Internal graph | One deterministic scope→retrieve(→report) pass | You want a predictable, single-hop grounded answer over your own data. **Best default.** |
| **Knowledge research agent** (`agent.deep_research`) | Internal graph + documents | Self-directed, multi-step, plans and branches | The question needs several queries / a deep dig over internal data — no web. |
| **Web research agent** (`agent.web_research`) | Live web (Tavily) | Self-directed, cited URLs | You need current or external information. |
| **Orchestrator agent** (`agent.orchestrator`) | Whatever nodes you allow | Decides the path at runtime, calls other nodes as tools | The task is open-ended and you can't predict which sources/steps are needed (it can combine graph + web + docx). |
Rule of thumb: **deterministic single question → Knowledge Graph Query**; **deep internal investigation → Knowledge research agent**; **external/current → Web research agent**; **unpredictable, multi-tool → Orchestrator agent**. For a *fixed* "graph **and** web" answer that always runs both, prefer two parallel nodes feeding one `LLM generate` (deterministic, predictable cost) over an Orchestrator — see [Cookbook Recipe 3](/admin-guide/agent-flows-cookbook).
***
## Advanced / hidden nodes
`kg.scope`, `kg.retrieve`, and `kg.report` are the **internal phases** of Knowledge Graph Query, split into separate nodes. They're **hidden from the palette** — new flows can't add them — because the single **Knowledge Graph Query** node does all three phases for you. They remain registered and runnable only so older published flows that referenced them keep working. You won't need them: use **Knowledge Graph Query** instead.
***
## Where to next
* **[Agent Flows Cookbook](/admin-guide/agent-flows-cookbook)** — validated, end-to-end example flows (and the patterns behind them) built from these nodes.
* **[Agent Flows](/admin-guide/agent-flows)** — the canvas basics: creating, validating, publishing, and enabling a flow as a chat sub-agent.
# Agent Flows Cookbook
Source: https://docs.experio.cloud/admin-guide/agent-flows-cookbook
What you can build with Agent Flows — validated example flows, the patterns behind them, and how to build each one in the admin canvas.
Agent Flows let you compose **typed nodes** into a graph that runs as a real LangGraph agent: pull from the knowledge graph, research the web, call MCP tools, classify and route, fan out work in parallel, pause for human review, and generate documents — then let the chat agent launch the whole thing as a background sub-agent, or run it from the canvas.
This cookbook is a set of **example flows that have been run end-to-end and validated**, the reusable patterns behind them, and step-by-step instructions for building each in the admin canvas (**AI & Agents → Agent Flows**).
Every example below was built, published, run, and checked against its real output (knowledge-graph rows, live web results, generated `.docx` files, MCP calls). Where a flow uses a question, point it at data you actually have — the demo knowledge graph contains `Project` and `Deliverable` data, so "list our projects" / "what deliverables exist" return real results.
Looking for what a specific block does, its settings, and which one to pick? See the [Flow Blocks Reference](/admin-guide/agent-flows-blocks) — a plain-language guide to every node type, with decision helpers (classify vs extract vs generate, Map vs Loop, KG Query vs research agents, and more).
## The node palette at a glance
| Category | Node | What it does |
| ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Control** | Start / End | Entry and exit. `End` collects the flow's result (bind `result`). |
| | Condition | Branch on a predicate (route to one of several next nodes). |
| | Map / Join | Fan a list out to parallel branches, then collect them. |
| | Loop | Bounded repeat (e.g. a revise-until-approved loop). |
| | Merge | Converge branches from a Condition fan-out. |
| **Data** | Knowledge Graph Query | Ask the knowledge graph; returns rows + a synthesized, cited answer. |
| | Index uploads | Index uploaded files so later nodes can use their content. |
| | Read conversation | Read the recent messages of the chat the flow was launched from (a plain transcript, the individual turns, and a count). Only works when the flow is launched **from a chat** — a run started from the canvas or the Run tab has no conversation to read and the node fails. |
| | Generate DOCX | Render text/sections into a downloadable Word document (optionally branded with a Document Template). |
| | Generate PPTX | Render sections into a downloadable PowerPoint deck (one slide per section). |
| | Generate file | Render into a chosen format — Word, PowerPoint, PDF, or HTML — with one block. |
| **LLM** | LLM generate | Free-form generation from a prompt (the general "writer"). |
| | LLM extract | Structured extraction into a typed schema. |
| | LLM classify | Classify input into one of a set of labels. |
| **Agents** | Knowledge research agent | Deep, self-directed investigation over the **internal** knowledge graph + documents. |
| | Web research agent | Deep investigation over the **web** (Tavily), with cited sources. |
| | Orchestrator agent | A coordinator that calls other nodes as **tools** and decides the path at runtime. |
| | MCP action | Calls tools on a configured MCP server. |
| **Human** | Human review | Pause the run for a person to approve / edit / reject, then resume. |
**How data flows:** an edge sets execution order; the **data** flows through each node's input bindings. Connecting two nodes auto-wires compatible ports, and you can drag port-to-port to wire a specific value. To feed the user's request into a node, bind the input to **Chat message** (`$.params.message`) — the chat agent forwards it when it launches the flow. To pass one node's output to another, bind to `$.nodes..outputs.`.
Execution order also takes those reads into account: a value read from another node — whether through an input binding or a `{{ nodes..outputs. }}` reference in a prompt — counts as a dependency, not just the edges you drew. That's a safety net, not a substitute for wiring: when a node reads from a node that isn't upstream of it, publishing isn't blocked — and the read can resolve to nothing, so the node runs on an empty input and still reports success. Adding that edge is the reliable fix.
***
## Recipe 1 — Answer from the knowledge graph
**Use it for:** grounded Q\&A over your own data (projects, deliverables, contracts, people…).
```
start → Knowledge Graph Query → end
```
* **Knowledge Graph Query**: set **Output mode = report** (returns a written, cited answer, not just rows). Bind its **`query`** input to **Chat message** so the user's question drives it.
* **end**: bind `result` ← `Knowledge Graph Query.text`.
**How to build:** New flow → drag `Knowledge Graph Query` from **DATA** → connect `start → KG → end` → select the KG node → Output mode = report → wire its `query` input to **Chat message** → wire `end.result` to the KG `text` output → **Publish** → enable the **Sub-agent** toggle and write a description so the chat agent can launch it (or use the **Run** tab).
**Validated:** returns real `Project`/`Deliverable` rows with an evidence-cited narrative.
***
## Recipe 2 — Research the web
**Use it for:** current, externally-sourced answers with citations.
```
start → Web research agent → end
```
* **Web research agent**: set the **goal** to your question (it supports `{{ params.message }}` to use the chat message). It runs multiple live searches and returns a sourced report on its `text` output.
* **end**: `result` ← `Web research agent.text`.
**Validated:** ran live Tavily searches and produced a sourced brief (e.g. a Python 3.13 summary citing the official "What's New" page).
***
## Recipe 3 — Combine the graph **and** the web into one cited answer
**Use it for:** "answer using what we know internally **plus** what's current on the web," with `[KG]` / `[Web]` attribution.
```
start → Knowledge Graph Query (report) ┐
├→ LLM generate (synthesize) → end
start → Web research agent ┘
```
* The two sources run **in parallel**; `LLM generate` waits for both (a node with two incoming edges auto-defers until both finish).
* **LLM generate** prompt fuses both: reference `{{ nodes.web_research.outputs.text }}` and bind its `context` input to the KG node's `text`; instruct it to tag claims `[KG]` vs `[Web](url)`, note agreements/conflicts, and end with a Sources list.
* **end**: `result` ← `LLM generate.text`.
Don't use `Merge` + `Knowledge Graph Query` (report mode) to "combine" the two sources — `Merge` is for Condition fan-outs (it forwards one branch), and `Knowledge Graph Query` (report mode) only writes from a knowledge-graph retrieval handoff. The general writer for fusing arbitrary sources is **LLM generate**.
***
## Recipe 4 — Classify, then route
**Use it for:** sending different question types down different paths.
```
start → LLM classify → Condition → ┌ LLM generate (technical) ┐
└ LLM generate (general) ┘ → Merge → end
```
* **LLM classify** labels the input (e.g. `technical` | `general`); **Condition** routes on that label; the unused branch is skipped; **Merge** converges the chosen branch to `end`.
**Validated:** a technical question classified `technical`, routed to the technical branch (general branch correctly skipped), and produced a grounded answer.
***
## Recipe 5 — Research → write a report → Word document
**Use it for:** turning a research request into a polished, downloadable deliverable.
```
start → Web research agent → LLM generate (report) → Generate DOCX → end
```
* **Web research agent** gathers sourced findings → **LLM generate** writes a structured report from them → **Generate DOCX** renders it to a `.docx` artifact you can download.
**Validated:** produced a 40 KB Word document (54 paragraphs) from a live web research pass.
Give the Web research agent a reasonable budget (≈12+ model calls). Very small budgets can exhaust mid-investigation.
***
## Recipe 6 — Knowledge graph → structured extraction → document
```
start → Knowledge Graph Query (report) → LLM extract → Generate DOCX → end
```
* Pull grounded data from the graph, **LLM extract** it into a typed structure (e.g. a list of `{name, …}` objects), then render a document.
**Validated:** KG returned 39 rows (`MATCH (p:Project)-[:HAS_DELIVERABLE]->(d:Deliverable)`), extracted structured names, and produced a 90-paragraph `.docx`.
***
## Recipe 7 — Let an orchestrator decide
**Use it for:** open-ended requests where you can't predict which sources are needed; the agent picks and iterates.
```
start → Orchestrator agent (tools: Knowledge Graph Query, Web research, Generate DOCX) → end
```
* Give the **Orchestrator** a goal and a set of **allowed tools** (other nodes become its tools). At runtime it decides whether to query the graph, research the web, generate a document, or combine them — and writes the final answer itself.
**When to prefer it:** open-ended/iterative tasks. For always-run-both-in-parallel with a fixed shape, prefer Recipe 3 (deterministic, predictable cost).
***
## Recipe 8 — MCP action (call external tools)
**Use it for:** taking actions or fetching data through a configured MCP server.
```
start → MCP action → end
```
* Point **MCP action** at a configured MCP server (admin **MCP Servers**) and whitelist the tools it may use.
**Validated:** a live run against the `time-mcp` server returned the real current UTC time.
Servers that need OAuth (Google, Slack, HubSpot…) must be connected first. No-auth servers (e.g. time, sequential-thinking) work immediately.
***
## Recipe 9 — Human-in-the-loop (review & approve)
**Use it for:** anything that needs sign-off before it finalizes.
```
start → LLM generate → Human review → LLM generate (revise) → end
```
* **Human review** pauses the run and surfaces the draft in the conversation's flow-run panel (and, for staff, the **Agent Inbox**); a reviewer approves (or rejects with feedback). On approval the flow resumes; on rejection you can loop back to revise.
If your flow can pause on two reviews at once (parallel branches), every pending question is listed together and the reviewer can answer them in any order, without waiting between them — an answer given while the run is still applying the previous one is held and submitted automatically. See [Several questions at once](/admin-guide/agent-flows#several-questions-at-once).
**Feed the reviewer's note into the revise step.** The review card lets the reviewer add a **note** — optional on approve, required on reject. Bind it into the revise step's prompt so the feedback actually drives the rewrite:
```
Revise the draft using this reviewer feedback:
{{ nodes..outputs.comment }}
```
Route the loop on `{{ nodes..outputs.decision }}` (`"approved"` / `"rejected"`) — or the boolean `{{ nodes..outputs.approved }}` — so a rejection goes back to **LLM generate (revise)** with the note applied, and an approval finishes the run. See the [Human review node](/admin-guide/agent-flows-blocks#human-nodes) for the full list of outputs.
**If a document step follows the review, bind it to the review's `content`, not to the draft.** The reviewer can edit what they were shown, and `content` is the port that carries the edited version (it is simply what they saw when they didn't edit, so binding it is always correct). A DOCX step still bound to `$.nodes..outputs.text` accepts the reviewer's edit and then silently ships the unedited draft, reporting success. See [Human review](/admin-guide/agent-flows-blocks#human-nodes).
**Validated:** the run paused at `Human review`, resumed on approval, and the revise step produced the final answer.
***
## Recipe 10 — Fan out work in parallel (map / join)
**Use it for:** doing the same step over many items at once (e.g. author each section of a document).
```
start → LLM extract (a list) → Map → LLM generate (per item) → Join → LLM generate (assemble) → end
```
* **Map** fans the list out to parallel branches (one per item), **Join** collects them, and a final node assembles the pieces. Bind the assembling node to the Join's `results_by_name` rather than `results` — that port keys each branch's result by the map item it came from — and fan the Map out over a list of *distinct* item names so the results key cleanly.
* Put a **Condition** on the Join's `ok` in front of the assembling node and route false into a revise lane: `failed_indexes` is built from the branch count, so a Map that fanned out over an empty list leaves it empty and the assembling node would otherwise produce a document with nothing in it. See [Join](/admin-guide/agent-flows-blocks#control-nodes) for the port detail.
**Validated:** a 3-item list fanned to 3 parallel branches; Join collected all three; the summary combined them coherently. (This is the same engine the **RFP Response Pipeline** template uses to author 17 sections in parallel.)
***
## Flagship templates
Two ready-made templates combine many of the above (start them from **Agent Flows → Templates → Use template**):
* **Grunley Scope Merge** — upload scope documents → index → LLM merge of the uploaded documents → human review → **Merged Scope of Work** `.docx` (the DOCX binds the review's `content`, so an edit made at the gate reaches the delivered file, and rejecting the merged scope ends the run as a failure with no document rather than quietly completing). *(Validated: two uploaded scope docs → a consolidated, cited SOW grouped by project.)*
* **RFP Response Pipeline** — upload an RFP → extract requirements → route (standard/bespoke) → compliance matrix → human review → **parallel section authoring (orchestrator + map)** → assemble → final review → RFP response `.docx` (the DOCX binds the final review's `content`, so a reviewer's edits reach the delivered file). The chain hides two loops: rejecting the compliance matrix sends the run back to re-extract the requirements with the reviewer's note instead of walking on into section authoring, and once those attempts run out it goes ahead with the matrix it has; the drafted sections have to pass a quality gate before assembly, and a rejection at the final review — or a draft that fails that gate — re-authors just the sections that need it through a second authoring lane and re-assembles, and when that loop runs out the run ends with no document and is reported as failed rather than delivering a `.docx`. The assembly step is handed the names of the sections the draft stage held back or could not produce at all, so any of them a later pass didn't re-author surfaces in the assembled document under its planned heading with an explicit not-included note instead of vanishing. *(Validated: a full proposal grounded in the RFP brief.)*
***
## Building a flow in the admin — the short version
1. **AI & Agents → Agent Flows → New flow**, give it a name (the system handles the internal id).
2. Drag nodes from the **palette** onto the canvas; **connect** them (`start → … → end`). Connecting auto-wires compatible data; drag **port-to-port** for a specific value, or use **Fix wiring** to complete required inputs.
3. **Select a node** to configure it (Inspect panel); bind inputs to **Chat message** (`$.params.message`), to another node's output (`$.nodes..outputs.`), or to an uploaded file slot.
4. **Validate**, then **Publish** (publishing snapshots an immutable version).
5. **Run it** from the **Run** tab (provide params/files), or enable the **Sub-agent** toggle (and write a good description) so the chat agent can launch it during a conversation.
**Edit as JSON:** the canvas toolbar has an **Edit as JSON** action to export the flow definition (copy/download) and paste an edited definition back. Applying runs the same validation as the canvas, so a malformed definition is rejected before it loads — handy for copying a flow between environments or making bulk edits.
## Tips
* **Feed the user's question in** by binding the entry node's `query`/`context` to **Chat message** — don't rely on a hard-coded value.
* **Parallel + converge:** a node with two incoming edges waits for both — no explicit Join needed for simple fan-in (use Join when you fanned out with Map).
* **Pick the right writer:** `LLM generate` for fusing arbitrary sources; `Knowledge Graph Query` (report mode) for graph-grounded answers; `LLM extract` for structured data.
* **Budgets:** give research/orchestrator nodes enough model-call budget for the task.
## On the roadmap
A **natural-language flow builder** — describe what you want in the admin and have an agent assemble the flow for you — is planned. Until then, this cookbook + the templates are the fastest way to start.
# AI Instructions
Source: https://docs.experio.cloud/admin-guide/ai-instructions
Manage system-wide and assistant-specific AI behavior instructions
## Overview
AI instructions are directives that shape how AI assistants behave. They can be scoped globally (affecting all assistants) or targeted to specific assistants or clients. Instructions are applied in order, allowing layered control over AI behavior.
Navigate to **Admin > Settings > AI Instructions**.
## Viewing Instructions
The instructions page lists all configured instructions with:
* Instruction text preview (first 50 characters)
* Scope (System, Global, Client, Assistant)
* Client ID (if client-scoped)
* Active status
* Display order
* Created and modified dates
## Instruction Scopes
| Scope | Applied To |
| ------------- | ------------------------------------------------- |
| **System** | All assistants, always applied first |
| **Global** | All assistants, applied after system instructions |
| **Client** | All assistants for a specific client organization |
| **Assistant** | A specific assistant only |
Instructions are applied in scope order: System > Global > Client > Assistant. Within each scope, they are applied in their configured display order.
## Creating Instructions
Click **Create New** to add an instruction:
| Field | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| **Instruction** | The instruction text (multi-line). This is the actual directive sent to the AI model. |
| **Scope** | Where this instruction applies (System, Global, Client, Assistant) |
| **Client ID** | Required for Client scope — specifies which client organization |
| **Active** | Whether this instruction is currently applied |
| **Order** | Display and application order within its scope |
## Managing Instructions
### Editing
Click any instruction to edit its text, scope, or configuration.
### Ordering
Instructions within the same scope are applied in their configured order. Use the order field to control which instructions are applied first.
### Activating/Deactivating
Toggle the **Active** status to temporarily disable an instruction without deleting it. Deactivated instructions are not sent to the AI model.
### Searching
Use the search field to find instructions by text content.
Use instructions to enforce organizational policies, guide response tone, restrict topics, or provide domain-specific context that all assistants should follow.
An instruction that *requires information from the user* ("before answering, establish which business unit they belong to") makes a deep-agent assistant ask for it with a question card rather than writing the question into its reply. The instruction reaches the report writer, so the question comes after the data has been fetched; to have the assistant ask *before* it retrieves, put the same requirement in its **Intent Resolver Prompt Extension**. See [Clarifying Questions](/admin-guide/assistants#clarifying-questions).
# Assistants
Source: https://docs.experio.cloud/admin-guide/assistants
Configure AI assistant personas, models, tools, and agent behavior
## Overview
Assistants are the AI personas users interact with in Experio. Each assistant has its own configuration including model assignments, tools, behavior settings, and agent architecture. You can create multiple assistants tailored to different use cases such as data exploration, conflict analysis, or knowledge transition.
Navigate to **Admin Panel** > **Settings** > **Agent Configuration**.
The interface provides a full-page CRUD experience with a tabbed form for managing all assistant settings. From the list view you can search, create, edit, and delete assistants. Clicking an assistant or the **Add Assistant** button opens the detail page with the following tabs.
## Basic Tab
| Field | Description |
| ------------------- | ------------------------------------------------------------------------------------ |
| **Title** | The assistant's display name shown to users |
| **Subtitle** | A brief description shown on the assistant card |
| **Icon** | Select from a dropdown of available icons (e.g., robot, brain, lightbulb, analytics) |
| **Welcome Message** | The greeting shown when a user starts a new conversation with this assistant |
| **Display Order** | Display order on the Agents page (lower numbers appear first) |
| **Enabled** | Whether the assistant is available to users |
| **Staff Only** | Restrict this assistant to staff users only |
### Gating
Gating requires users to provide specific context (for example: company size, industry, strategic focus) before the assistant will process their queries. When enabled, the first message of every conversation is evaluated against the gating prompt; if the required context is missing, the assistant returns a clarification asking for it instead of running the normal pipeline. Once a conversation provides complete context, gating is satisfied for the rest of that conversation.
| Field | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Enable Gating** | Toggle gating on or off for this assistant. When off, no gating evaluation runs and there is no overhead. |
| **Gating Prompt** | Free-form text describing what context the user must provide. Shown to users in an amber banner on the welcome page and used by the LLM to decide whether the user's message is complete. Only visible when **Enable Gating** is on; clearing the toggle clears the prompt. |
Write the gating prompt as a checklist of required information, e.g.:
```
User must provide:
- Company size and industry
- Strategic objectives
- Key challenges they want to address
```
Users can amend the context they originally provided at any time by clicking the clipboard icon next to the message input — the panel shows the original context plus any updates.
## Configuration Tab
| Field | Description |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **System Prompt** | Custom system prompt for this assistant. Leave empty to use the default prompt for the assistant type. |
| **Use Document Context** | Include document context from vector search in responses |
| **Allow Per-Question Model Switch** | When enabled, users can select a report-writer model per message; for LangGraph deep agents this applies to the report phase only |
## Model Configuration Tab
Each assistant is assigned models that control how it generates responses. All model dropdowns are populated from the configured Model Configurations.
| Field | Description |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reasoning model (default pipeline)** | Default for LangGraph scope, retrieval, router, and summarization when no per-step override is set. Uses a **Chat** model config. Also the chain used when the report-writer model is unset. |
| **Report writer model** | Used for the LangGraph **report** phase (the streamed answer users see), and for final responses in native/planning agents. Same **Chat** model type as other assistant slots. If unset, falls back to the reasoning model. |
For **LangGraph** deep agents, the reasoning model drives the pipeline; the report writer model is used only for the final report step. Per-question model selection, when enabled, overrides the report writer model for that message.
The Model Configuration tab also includes per-tool model overrides:
| Field | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Cypher Reasoning Model** | Model used for generating Cypher graph database queries. Falls back to the reasoning model if not set. |
| **Resolve Entities Model** | Model used for entity resolution in knowledge graph lookups. Falls back to the reasoning model if not set. |
| **Intent Resolver Model** | Model used for classifying user intent and routing queries. Falls back to the reasoning model if not set. |
| **Orchestrator Model** | Model used for orchestrator checkpoint nodes (think/write\_todos). These nodes only plan tasks and show progress to the user — they do not generate the final answer. Use a fast, inexpensive model here. Falls back to the reasoning model if not set. |
| **Router Model** | Model used for the router — the first model call of every turn, which decides how the question should be handled. It only classifies the request, so it does not need a large model. **Must support function calling.** Falls back to the reasoning model if not set. |
| **Template Matcher Model** | Model used for the template matcher, which runs immediately after the router on every turn and checks whether the message matches a document template. Like the router it only classifies, so a smaller model is usually enough. **Must support function calling.** Falls back to the reasoning model if not set. |
Use a smaller, faster model for the router, template matcher, Cypher generation, entity resolution, and orchestrator checkpoints to reduce latency and cost, while keeping a more capable model for the final answering step.
The router and the template matcher both run before any progress is shown, one after the other, so the user waits on both before the assistant appears to start. Because each only classifies the message rather than composing an answer, they are usually the safest places to trade model size for speed.
Both of these steps request **structured output using function calling**, so whichever model you pick must support it. A model that does not will fail the very first step of every conversation, not just the step you assigned it to. The template matcher additionally decides template **clarification**, so an underpowered model there can send a document request down the wrong path rather than simply costing a few seconds.
## Custom Tool Configuration Tab
Custom prompt instructions for specific tools. These are appended to the default prompts.
| Field | Description |
| --------------------------------- | ------------------------------------------------------------ |
| **Cypher Instructions** | Custom instructions appended to the Cypher generation prompt |
| **Resolve Entities Instructions** | Custom instructions for the entity resolution step |
## Agent Configuration Tab
Control the agent architecture and capabilities available to the assistant.
### Agent Mode
Select one agent mode at a time using the card-based selector. New assistants default to **Deep Agent**.
| Mode | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| **Deep Agent** | LangGraph-based 3-phase workflow (scope, retrieval, report) with TODO tracking, file offloading, and subagent delegation |
| **Planning Agent** | Multi-turn planning agent that creates a step-by-step plan before execution |
| **Native Agent** | Native LLM tool calling (ReAct loop) |
| **None** | No agent orchestration |
### Tool Groups
Select which tool groups the assistant has access to using the pill-based multi-select. Click a pill to toggle it on/off. Available groups include Knowledge Base, Employee Analysis, OCI Analysis, Graph Query, Document Processing, Reference Documents, and more.
If no tool groups are selected, defaults are applied based on the assistant type.
Only one agent mode can be active at a time. The interface enforces this automatically. **Deep Agent** provides the most thorough analysis but takes longer to respond. For faster, simpler interactions, use **Native Agent** instead.
When **Deep Agent** is selected, users see a **Deep Research** toggle in the chat interface. When activated, this enables parallel graph context queries (GCQ) alongside document search during the scope phase, producing more thorough but slower results. This is a per-message toggle — users can enable it for complex queries and disable it for quick lookups.
## Deep Agent Settings Tab
When **Deep Agent** is selected as the agent mode, a **Deep Agent Settings** tab appears with additional configuration. This tab is hidden when other agent modes are selected.
### Architecture Settings
| Field | Default | Description |
| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Use LangGraph architecture** | `false` | Enable pure LangGraph 3-phase workflow |
| **Enable subagents** | `true` | Enable focused subagents for parallel retrieval tasks |
| **Recursion limit** | `30` | Max recursion depth for agent execution |
| **Stream recursion limit** | `100` | Max streaming iterations before stopping |
| **Enable checkpointer** | `true` | Enable state persistence across conversation turns |
| **Enable store** | `true` | Enable PostgresStore for agent memory and context sharing |
| **Checkpoint durability** | `exit` | Controls checkpoint frequency (`exit` saves on completion) |
| **Enable orchestrator** | `true` | Enable orchestrator checkpoint nodes that provide user-visible progress updates (thinking steps, task tracking). When disabled, these nodes become pass-through — the pipeline still runs but skips the orchestrator LLM calls, reducing latency and cost. |
Disabling the orchestrator does not change the agent's retrieval or answer quality. It only removes the intermediate "thinking" and "task planning" steps that are shown to the user during processing. This is useful for simpler queries where the overhead of orchestrator LLM calls is not needed.
### Retrieval Settings
| Field | Default | Description |
| ----------------------------------- | ------- | ---------------------------------------------------------- |
| **Document search K** | `3` | Number of similar documents to retrieve per search |
| **Document search score threshold** | `0.7` | Minimum similarity score for document results (0.0 to 1.0) |
### Scope Settings
| Field | Default | Description |
| ----------------------------- | ------- | ---------------------------------------------------------------------------------------- |
| **Scope max count threshold** | `1000` | Max results before the agent asks the user to narrow their query |
| **Min path confidence** | `0.5` | Minimum confidence score for intent resolver routing paths |
| **Use resolved intent** | `true` | Use the router-rephrased intent for all downstream nodes instead of the raw user message |
### Clarifying Questions
When the deep agent needs information before it can answer, it asks with a **question card** instead of writing the question into its reply: a few questions, each with plain-language options the user can multi-select or extend with their own answer. The user's answers arrive as the next message and the agent resumes — re-running retrieval when the answers change *what* must be fetched, or only rewriting the answer when they change *how* it is presented. Follow-up suggestions and filters are suppressed while a question is open. See [Questions from the Assistant](/user-guide/chat-interface#questions-from-the-assistant) for the user's side.
The agent asks when the request is genuinely ambiguous, or when nothing in the knowledge base matches it — the card then offers the closest topics it did recognize.
To make an assistant gather specific information before it answers, put that in the **Intent Resolver Prompt Extension** on the Deep Agent tab. An extension such as *"Before resolving the query, establish which business unit the user belongs to (Retail, Wholesale, Manufacturing) and whether they want a summary or a detailed report"* produces a card **before** retrieval, so the answer is scoped correctly the first time. An [AI instruction](/admin-guide/ai-instructions) that demands information still works, but it reaches the report writer rather than the scope phase, so the question is asked after the data has been fetched.
How much the agent may ask is configurable per assistant:
| Setting | Default | Description |
| ---------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
| **Max clarifying questions** | `5` | How many questions may appear on one card before the agent must proceed (1-10) |
| **Max options per question** | `6` | How many suggested answers each question offers; the user can always add their own (2-10) |
| **Max clarification rounds** | `2` | How many times the agent may come back and ask again before it must answer with what it has (1-5) |
An instruction that demands information makes the assistant open with a question on **every** request that doesn't already supply it. Keep the list of required details short, and word each one so the agent can turn it into two to six options — an instruction that asks for something open-ended ("find out the user's objectives") produces a card with nothing to pick from, only a free-text box.
The agent will not interrogate a user indefinitely: after two answered rounds of questions for the same request it stops asking, answers with what it has, and explains what it could not determine.
### Report Settings
| Field | Default | Description |
| --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Report data preview limit** | `300` | Max rows of retrieved data included in the report writer prompt |
| **Report max data tokens** | `50000` | Max tokens allocated for retrieved data in the report context. Rows are dropped from the end until the content fits within this budget, and the report writer is then told it is seeing a partial sample and must say the results were truncated rather than state totals or completeness. If not even one row fits after an aggressive per-field retry, the writer is told the retrieved data could not be included at all and must not describe or count it. |
| **Report writer fallback models** | *(none)* | Fallback models for the report writer phase. On 429 rate-limit errors, these models are tried in order. |
Configure fallback models from different providers (e.g., primary: Azure GPT-5.1, fallback: Google Gemini) to ensure the report phase can complete even when one provider hits rate limits. The primary answering model is always tried first.
### Graph Context Settings
| Field | Default | Description |
| --------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Auto-include graph context if under** | `30000` | Token threshold for auto-including graph context in the report. If the total graph context is under this limit, it is included automatically without requiring a store lookup. |
| **Per-entity auto-include threshold** | `20000` | Max tokens per individual entity for selective packing. Entities exceeding this limit are summarized instead of included in full. |
Entities that fit these thresholds are delivered directly in the report context; the rest are held in the agent store and listed for the report writer to retrieve on demand. If an entity's store write fails or is never attempted (for example when **Enable store** is off), it is withdrawn from that list and the report writer is instructed to state in the answer that part of the graph context was unavailable, rather than present ungrounded relationship claims.
### LangGraph Prompt Extensions
The Deep Agent Settings tab also includes a **LangGraph Prompt Extensions** section. These are custom instructions appended to each LangGraph node prompt, allowing fine-tuning of agent behavior at each stage of the pipeline without code changes.
| Prompt Extension | Applied To |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| **Retrieval** | The retrieval node that searches documents and knowledge graph |
| **Report Writer** | The final report generation node that synthesizes findings |
| **Intent Resolver** | The router that classifies user intent and selects the execution path |
| **Orchestrator Pre-Scope** | The orchestrator before the scoping phase begins |
| **Orchestrator Pre-Retrieval** | The orchestrator before the retrieval phase begins |
| **Orchestrator Pre-Report** | The orchestrator before the report generation phase begins |
| **Router** | The initial routing node that determines the query type |
| **Retrieval Fallback** | The fallback retrieval strategy when primary retrieval returns insufficient results |
| **Summarization** | The summarization node used for condensing large result sets |
Use prompt extensions to add domain-specific instructions like "Always cite contract numbers" or "Format financial data as tables" without modifying the underlying agent code.
## Agent Flows
Assistants are no longer wired to a single Agent Flow. Instead, a flow opts into chat by turning on its **Sub-agent** toggle, and the assistant's chat agent discovers and launches it as a background sub-agent when a user's request matches the flow's description. See [Agent Flows](/admin-guide/agent-flows#letting-the-chat-agent-run-a-flow) for how to enable a flow and how launching works.
# Client Configuration
Source: https://docs.experio.cloud/admin-guide/client-configuration
Configure your organization's identity and branding
## Overview
Client configuration defines how your organization appears in the Experio knowledge graph and user interface. This is typically the first step when setting up a new deployment.
Navigate to **Admin > Graph > Client Configuration**.
## Organization Identity
The identity section configures how your organization is recognized within the knowledge graph.
| Field | Description |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Organization Name** | The canonical name used to identify your firm in the knowledge graph. This name is used when building entity relationships. |
| **Synonyms** | Alternative names, abbreviations, or variations of your organization name. The system recognizes these as referring to the same entity. |
Add common abbreviations, former names, and informal names as synonyms. For example, if your organization is "Acme Consulting Group", add "ACG", "Acme Consulting", and "Acme" as synonyms.
## Branding
The branding section controls how your organization appears in the user interface.
| Field | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| **Display Name** | The name shown in the Experio UI header and branding elements. This can differ from the graph identity name. |
| **Logo** | Your organization's logo displayed in the application header. Upload via drag-and-drop or file selection. |
### Logo Upload
* Drag and drop an image file onto the upload area, or click to select a file
* Supported formats: PNG, JPG, and JPEG (SVG is not accepted)
* A preview of the uploaded logo is displayed
* Click **Delete** to remove the current logo and upload a new one
# Conflict Resolution
Source: https://docs.experio.cloud/admin-guide/conflict-resolution
Review and resolve low-confidence classifications and entity matches
## Overview
During document ingestion, the AI classifies documents and extracts entities. When confidence is low or conflicts arise, items are queued for manual review. The conflict resolution interface lets administrators review these cases and provide corrections.
Navigate to **Admin > Graph > Conflict Resolution**.
## Classification Reviews
When the AI is uncertain about how to classify a document or which content type it belongs to, the document enters the review queue.
For each review item, you can see:
* **Document preview** — A text excerpt from the document
* **AI classification** — The AI's suggested classification with confidence score
* **Alternative classifications** — Other possible classifications the AI considered
* **Resolution status** — Pending, approved, or rejected
### Resolving Classifications
1. Review the document preview and AI suggestions
2. Select the correct classification
3. Submit your decision
Your corrections improve future AI accuracy by providing feedback to the classification model.
## Entity Match Reviews
When the matching system identifies potential duplicate entities but isn't confident enough to auto-merge them, they appear in the match review queue.
Each match review shows:
* **Entity data** — The entity in question with its properties
* **Candidate matches** — Potential duplicate entities ranked by match score
* **Match score** — Confidence level for each candidate
### Review Actions
| Action | Description |
| ----------------- | ---------------------------------------------------- |
| **Merge** | Confirm the entities are the same and merge them |
| **Confirm New** | The entity is distinct — keep it as a separate entry |
| **Skip / Reject** | Set aside for later review |
### Status Indicators
Match reviews display color-coded status:
* **Pending** (orange) — Awaiting review
* **Approved** (green) — Merged with a candidate
* **Merged** (blue) — Successfully combined
* **Rejected** (red) — Confirmed as distinct entities
## Version Toggle
The conflict resolution interface offers both **v1** and **v2** editors. The v2 editor provides an updated interface with improved usability for reviewing and resolving conflicts.
Review conflicts regularly, especially during initial ingestion. Early corrections significantly improve the AI's accuracy for subsequent document processing.
# Connectors
Source: https://docs.experio.cloud/admin-guide/connectors
Authorize connections to cloud storage providers
## Overview
Connectors establish authenticated connections to your cloud storage providers. Once a connector is authorized, you can create data sources that scan specific folders within that provider.
Navigate to **Admin > Data Sources > Connectors**.
## Supported Providers
Experio supports three cloud storage providers:
Enterprise content management with OAuth2 authentication.
Google Workspace file storage with OAuth2 authentication.
Microsoft 365 document management with OAuth2 authentication.
## Authentication Types
Depending on your connector configuration, the following authentication methods are available:
| Method | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
| **OAuth2 (Authorization Code)** | Redirects to the provider's login page for user consent. Used by Box, Google Drive, and SharePoint. |
| **OAuth2 (Client Credentials)** | Server-to-server authentication using client ID and secret. |
| **API Key** | Simple key-based authentication. |
| **Bearer Token** | Token-based authentication. |
| **Basic Authentication** | Username and password combination. |
## Creating a Connector
1. Click **Create New Connection** on the Connectors page
2. Select the authentication type
3. Fill in the required credentials:
* For OAuth2: Client ID, Client Secret, and authorization scopes
* For API Key: The API key value
* For Basic Auth: Username and password
4. Click **Save** to create the connector
5. For OAuth2 connectors, click **Authorize** to complete the OAuth flow
## OAuth Flow
For Box, Google Drive, and SharePoint:
1. Click **Authorize** on the connector
2. You'll be redirected to the provider's login page
3. Sign in with an account that has access to the content you want to index
4. Grant Experio the requested permissions
5. You'll be redirected back to Experio with the connection active
## Managing Connectors
### Connection Status
Each connector shows its status:
* **Active** — Connection is authorized and working
* **Pending** — OAuth authorization has not been completed
* **Error** — Connection has encountered an issue (re-authorize to fix)
### Actions
| Action | Description |
| ---------- | ----------------------------------------------------------- |
| **Edit** | Modify connector credentials or settings |
| **Test** | Verify the connection is working |
| **Delete** | Remove the connector (also removes associated data sources) |
Deleting a connector will affect all data sources that use it. Ensure no active data sources depend on the connector before deleting.
## Help & Documentation
The Connectors page includes a **Help & Documentation** tab with detailed setup guides for each provider, including required OAuth scopes and configuration steps.
# Content Types
Source: https://docs.experio.cloud/admin-guide/content-types
Define document types and control how they are processed during ingestion
## Overview
Content types define the categories of documents your system processes and how the AI should classify and extract information from them. Each content type includes instructions for the AI on what entities and relationships to look for.
Navigate to **Admin > Data Sources > Content Types**.
## Viewing Content Types
The content types page lists all configured types with:
* Content type name
* Number of entity types associated
* Number of relationship types associated
* **Compatibility status** — valid, stale, invalid, or pending review against the current ontology
revision (see [Ontology Compatibility](/admin-guide/ontology-compatibility))
* Created and last modified dates
## Creating a Content Type
Click **Create New** to define a new content type:
1. **Name** — A descriptive name for the document category (e.g., "Contracts", "Proposals", "Meeting Notes")
2. **Classification Instructions** — Guidance for the AI on how to identify documents of this type
3. **Entity Types** — Which entities to extract from these documents
4. **Relationship Types** — Which relationships to identify between extracted entities
5. **Metadata** — Additional configuration for processing
### Ingestion extraction policy
On the **Basic Information** tab, the **Ingestion extraction** section controls how deeply ingestion
uses LLMs for this content type:
* **Default mode** — `full`, `metadata_and_snippet`, or `metadata_only`
* **Primary model tier** — `large`, `medium`, or `small` for primary extraction
* **Validation pass** — optional secondary LLM pass (off by default for metadata-only Excel)
* **Excel overrides** — separate mode for spreadsheets
See [Extraction Policy](/admin-guide/extraction-policy) for mode behavior, system limits, and filter
overrides.
### Entity and relationship attributes
**Entity attributes** are chosen from the ontology per entity type. You select which attributes apply,
edit extraction instructions (defaults come from the ontology unless you override them), and set
whether an attribute is required.
**Relationship attributes** follow the same idea:
* Attributes available on a relationship come from the **ontology definition for that triple**
(source entity type, relationship type, target entity type).
* You pick attributes from a dropdown — you do **not** type arbitrary names; **types** come from the
ontology (shown read-only).
* **Extraction instructions** default from the ontology and can be overridden per content type for
document-specific wording.
Relationship attributes are included in extraction prompts so the model can fill an `attributes`
object on each extracted edge when the document supports values.
See also: [Relationship attributes (architecture)](/architecture/relationship-attributes).
To preview which ontology nodes, relationships, and attributes a content type uses, open
[Ontology](/admin-guide/ontology) and select that type from the toolbar dropdown. Unused nodes and
relationships are hidden; unused attributes are greyed out. That filter is view-only and does not
change the content type or the ontology.
## Test Ingestion
The test ingestion feature lets you validate your content type configuration before processing real data.
1. Click **Test Ingestion** on any content type
2. Configure test parameters:
* **Mode** — Choose between:
* **Classify & Extract** — Full processing (classification + entity extraction)
* **Classify Only** — Test document classification without extraction
* **Extract Only** — Test entity extraction without classification
* **Max Pages** — Limit pages to process (default: 3, use -1 for full document)
3. Upload a sample file
4. Review the results:
### Test Results
The test results display:
* **Classification Scores** — Confidence scores for each content type, with the selected type highlighted
* **Entities Table** — Extracted entities showing Type, Name, and Attributes
* **Relationships Table** — Extracted relationships showing Source, Type, Target, and **Attributes**
(key/value pairs returned by extraction when present)
* **Raw JSON** — Full raw output for debugging
Run test ingestion with representative documents before activating a content type in production. This helps fine-tune classification instructions and entity extraction rules.
## Editing Content Types
Click any content type to open its editor. Changes to a content type affect how future documents are processed — previously processed documents are not retroactively re-processed.
The editor shows a **compatibility** panel when this type drifted from the ontology. Fix the entities,
attributes, or relationships so they match the current schema, then **Re-validate**. After an ontology
rename that already looks correct, **Mark as reviewed** returns the type to valid.
An **invalid** content type blocks scans for every data source that uses it. Stale types warn in the
admin UI but do not block ingestion.
The admin sidebar labels this page **Artifact Types**; this guide uses **Content Types** for the same
feature.
# Data Mapping
Source: https://docs.experio.cloud/admin-guide/data-mapping
Map external data fields to your knowledge graph ontology
## Overview
Data mapping defines how raw fields from your external data sources are transformed into entities and relationships in the knowledge graph. Mappings tell Experio how to interpret structured data (CSV, Excel, databases) and create the corresponding graph nodes and edges.
Navigate to **Admin > Data Sources > Data Mapping**.
## Viewing Mappings
The data mapping page shows a table of all configured mappings with server-side pagination. Each entry
displays the mapping name, configuration details, and **compatibility status** against the current
ontology revision. Filter to invalid or stale mappings when you need to clean up after an ontology
change. See [Ontology Compatibility](/admin-guide/ontology-compatibility).
## Creating a Mapping
Click **Create New Mapping** to open the mapping editor.
The mapping editor provides two modes:
### Form Builder
A step-by-step form interface for defining:
1. **Source Fields** — Select which fields from your data source to use
2. **Node Mappings** — Define how source fields map to entity types in your ontology
3. **Relationship Mappings** — Define how relationships between entities are derived from the data
### Visual Canvas
An interactive drag-and-drop canvas (powered by React Flow) for visually designing mappings:
* Drag source fields onto the canvas
* Connect fields to entity types and properties
* Draw relationship lines between entities
* Zoom and pan to navigate complex mappings
## Mapping Components
### Source Fields
Define the raw fields available from your data source. Each field has:
* **Field name** — The column or property name in the source data
* **Data type** — String, number, date, etc.
### Node Mappings
Map source fields to entity properties:
| Configuration | Description |
| -------------------- | -------------------------------------------------- |
| **Entity Type** | Which ontology entity this maps to |
| **Property Mapping** | Which source field populates which entity property |
| **Identifier** | Which field uniquely identifies this entity |
Each mapped property displays its **ontology type** (text, number, date, boolean, list, enum). You do
not configure per-field transforms in the mapping UI — types come from the ontology definition for
that entity.
### Property types and graph writes
At ingestion time, Experio coerces mapped values to the ontology type before writing to the graph:
| Ontology type | Graph write behavior |
| ------------------- | --------------------------------------- |
| **text** / **enum** | String |
| **number** | Float |
| **date** | Datetime (native on Neo4j and FalkorDB) |
| **boolean** | Boolean |
| **list** | List of primitives |
Structured and unstructured ingestion share the same coercion path. Ensure source columns are
parseable for the target type (for example, ISO or common date formats for date fields).
### Relationship Mappings
Define how entities are connected:
| Configuration | Description |
| --------------------- | --------------------------------------- |
| **Source Entity** | The starting node of the relationship |
| **Target Entity** | The ending node of the relationship |
| **Relationship Type** | The type of connection between entities |
## Editing and Deleting
* Click any mapping row to edit its configuration
* Use the delete action to remove a mapping
The mapping editor includes a **compatibility** panel. If node or relationship mappings point at
ontology elements that no longer exist, the mapping is **invalid** until you update it and
**Re-validate**. After a rename that already looks correct, **Mark as reviewed** clears stale status.
Deleting a mapping does not remove entities or relationships already created in the knowledge graph. It only prevents future data from being mapped using that configuration.
An ontology cannot be deleted while mappings still point at it.
# Data Sources
Source: https://docs.experio.cloud/admin-guide/data-sources
Configure which folders and files to scan from your connected providers
## Overview
Data sources define which folders and files Experio should scan and process from your connected cloud storage providers. Each data source is linked to a connector and specifies folder paths, scanning behavior, and filtering rules.
Navigate to **Admin > Data Sources > Data Sources**.
## Creating a Data Source
Click **Add New Data Source** to start a multi-step configuration wizard:
Select the type of data source:
* **Box** — Scan folders from a Box account
* **Google Drive** — Scan folders from Google Drive
* **SharePoint** — Scan folders from a SharePoint site
* **File Upload** — Upload files directly to Experio
Enter the connection details and validate that Experio can access the specified location. The system verifies credentials and folder access.
Set up folder hierarchy and filtering rules:
* **Folder paths** — Specify which folders to scan
* **Recursive scanning** — Include subfolders
* **Filter expressions** — Include or exclude files based on patterns
* **Ingest Excel files** — On by default. Uncheck for filters that should exclude spreadsheets
from graph ingestion (see [Extraction Policy](/admin-guide/extraction-policy))
* **Excel extraction mode override** — Optional per-filter Excel policy override
* **Excel max sheet characters** — Optional per-sheet character cap override
Configure ingestion settings for the data source:
* **Days to sync** — How far back to scan for files
* **Use OCR** — Enable optical character recognition for scanned documents
* **Classification max pages** — Limit pages sent to the classifier
* **Ingestion type** — Choose **Full ingestion** (default) for the complete pipeline, or **Parse only** to stop after parsing (useful when a downstream system handles classification and embedding)
For API data sources, these options appear in the source configuration step instead.
Preview which files match your filter configuration before saving. This ensures only the intended files will be processed.
## Data Source Properties
| Property | Description |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Display name for identifying the data source |
| **Connector** | The authorized connection to use |
| **Folder Path** | Root folder to scan |
| **Recursive** | Whether to scan subfolders |
| **Filter Expression** | Pattern to include/exclude files |
| **Ingestion Type** | Pipeline mode: **Full ingestion** (default) runs the complete pipeline (download → parse → classify → graph → embed). **Parse only** stops after parsing — files are downloaded and parsed, but not classified, added to the knowledge graph, or embedded. Parsed artifacts are stored in Minio for downstream consumption. |
| **Excel ingestion** | Per filter: **Ingest Excel files** (default off). When off, matched spreadsheets skip graph ingestion. See [Extraction Policy](/admin-guide/extraction-policy). |
| **Status** | Active, paused, or error |
| **Compatibility** | Computed from linked content types and mappings: valid, stale, or invalid. Invalid linked configs **block Start Job**. See [Ontology Compatibility](/admin-guide/ontology-compatibility). |
## Managing Data Sources
### Editing
Click on any data source to open its configuration. Modify settings and save to apply changes. Changes take effect on the next scan cycle.
### Monitoring
Each data source shows:
* **Last scan time** — When the source was last scanned
* **Files found** — Number of files discovered
* **Files processed** — Number of files successfully ingested
* **Errors** — Any files that failed processing
* **Compatibility** — Whether linked content types and mappings still match the ontology
If you start a job while a linked config is **invalid**, the request fails immediately with links to
the blocking items. Fix those configs and re-validate, then start the job again. **Stale** configs
warn only and do not block the scan.
### OAuth Callbacks
For Box and SharePoint data sources, OAuth callback handling is built in. If a token expires, you'll be prompted to re-authorize through the connector.
## Parse-Only Mode
When a data source has **Ingestion Type** set to **Parse only**, the ingestion pipeline stops after downloading and parsing files. Specifically:
* Files are downloaded from the cloud provider and parsed using the standard parser
* Parsed artifacts are stored in Minio (under `parsed/{file_id}/...`) with the same retention policy as full ingestion
* **No classification, graph ingestion, or embedding occurs**
* Files reach a terminal status of `parsed_only` instead of `ingested`
This mode is useful when an external system (such as a partner pipeline) needs to consume the parsed output and handle classification and embedding independently.
**Ingestion Type** can only be changed when the data source has no files currently processing. If you try to switch modes while a scan is in flight, the update is rejected with a validation error. Wait for the current scan to complete (or stop it) before changing the mode. The new mode takes effect on the next scan.
## File Upload
The **File Upload** data source type allows direct file uploads:
* Drag and drop files onto the upload area
* Track upload progress with visual indicators
* Files are queued for processing automatically after upload
# Document Templates
Source: https://docs.experio.cloud/admin-guide/document-templates
Upload and manage templates for AI-generated documents including presentations, reports, and more
## Overview
Document Templates let you upload branded files that the AI uses as a base for generated documents. When a template is active, generated output inherits its branding, structure, and formatting. Templates are organized by **categories** (e.g., Case Study, Proposal, Resume) and support multiple output formats.
Navigate to **Settings > Document Templates** in the admin panel.
## Template Categories
Categories group templates by purpose. When users generate a document, the **Generate button** shows templates organized by category for easy discovery.
### Default Categories
The system ships with six seeded categories:
| Category | Description |
| --------------------- | ----------------------------------------- |
| **General** | General-purpose document templates |
| **Case Study** | Templates for case study documents |
| **Resume** | Templates for resume and CV documents |
| **Proposal** | Templates for business proposals |
| **Executive Summary** | Templates for executive summary documents |
| **List of Projects** | Templates for project listing documents |
### Managing Categories
Navigate to **Settings > Template Categories** in the admin panel.
* **Create** — Add a new category with a name and optional description. A URL-friendly slug is auto-generated from the name.
* **Edit** — Update the name, description, or active status.
* **Deactivate** — Inactive categories and their templates are hidden from users.
* **Delete** — Permanently removes the category. Templates in a deleted category become uncategorized.
To seed the default categories after a fresh deployment, run:
```bash theme={null}
cd server && pipenv run python manage.py seed_template_categories
```
## Supported Template Types
| Type | Extension | Description |
| ----------- | ---------------- | ------------------------------------------------------------------------------------------------ |
| **pptx** | `.pptx` | PowerPoint presentation with slide layouts and branding |
| **docx** | `.docx` | Word document with custom styles, headers/footers, and branding |
| **xlsx** | `.xlsx`, `.xlsm` | Excel workbook filled in place by an agent flow — the form is returned with its blanks completed |
| **message** | — | Formatted chat response (no file download — output appears directly in chat) |
The **message** type produces a formatted response in the chat window instead of a downloadable file. Use it for structured text outputs like summaries or formatted lists.
## Adding a Template
1. Click **Add Template** — this opens a full-page form
2. Fill in:
* **Name** — identifier used in the `/generate` command (must be unique within the selected category)
* **Type** — select the output format (`pptx`, `docx`, `xlsx`, or `message`)
* **Category** — select from available categories (e.g., General, Case Study, Proposal)
* **Description** — optional notes about the template
* **Retrieval Instructions** — guides what data the AI should retrieve (see below)
* **Output Instructions** — guides how the AI should format the output (see below)
* **File** — upload a template file (required for pptx, docx and xlsx; not applicable for message)
3. Click **Create**
For PPTX templates, the system parses the file and extracts slide layout names. Templates must contain at least one recognized layout name to be accepted.
For XLSX templates, the system reads the workbook and reports how many fillable fields and tables it found — shown on the template card as *"N fillable fields and tables detected"*. A workbook with none is rejected at upload rather than at run time, so a flow never quietly returns an untouched copy.
**Legacy binary files are auto-converted.** If you upload an old binary `.doc`/`.ppt` file — even one saved or renamed with a `.docx`/`.pptx` extension — Experio detects it and converts it to true OOXML server-side before validation. The conversion is transparent and preserves your branding, so legacy templates no longer break downstream generation.
### Retrieval Instructions
This field tells the AI **what data to look for** during the retrieval/scope phase of generation. It is injected into the pipeline before the AI begins gathering context.
**Examples:**
* `"Find all projects for this client including project name, client name, dates, and budget"`
* `"Retrieve the candidate's work history, education, skills, and certifications"`
* `"Gather financial metrics, market analysis, and competitive positioning data"`
### Output Instructions
This field tells the AI **how to structure and format** the final output. It is injected into the report writer phase.
**Examples:**
* `"Format as a professional case study with Executive Summary, Challenge, Solution, and Results sections"`
* `"Create a one-page executive summary with key findings, recommendations, and next steps"`
* `"Organize as a bulleted project list grouped by client, with dates and status for each project"`
Retrieval and output instructions work together: retrieval instructions ensure the right data is gathered, and output instructions ensure it is presented in the right format. Both are optional — when left blank, the AI uses conversation context and its default formatting.
## PPTX-Specific: Supported Layout Names
PPTX templates must include slide layouts named after the following supported types (case-insensitive):
| Layout Name | Purpose |
| ---------------- | -------------------------------------------- |
| `section_header` | Section divider slide with accent background |
| `bullets` | Bullet point content slide |
| `quote` | Centered quotation with attribution |
| `two_column` | Side-by-side content columns |
| `table` | Data table with styled header row |
| `key_metric` | Big number/KPI with supporting text |
| `timeline` | Horizontal timeline or roadmap |
| `blank` | Minimal spacer slide (footer only) |
| `comparison` | Structured VS comparison layout |
| `closing` | Thank you / end slide |
Layout matching is **case-insensitive**: `SECTION_HEADER`, `Section_Header`, and `section_header` all match.
### The `blank` Layout
The `blank` layout is **critical** — it serves as the fallback for any slide whose layout name doesn't match the supported list. Templates missing a `blank` layout will show a warning in the admin UI.
If your template has no layout named "blank", slides using unsupported layout names will fall back to the last available layout, which may produce unexpected results.
## What Templates Provide
Templates control the **visual branding** of generated documents:
* Slide master backgrounds (gradients, images, shapes) — for PPTX
* Custom Word styles (Title, Heading 1–4, Normal, List Bullet) — for DOCX
* Logo placement and footer design
* Font families and default text styling
* Color schemes defined in the slide master
* Retrieval guidance (what data to gather)
* Output formatting guidance (how to structure the result)
**Templates do NOT control content placement.** For PPTX, the AI uses programmatic builders to position titles, bullet points, tables, and charts at precise coordinates on each slide.
**XLSX templates are the exception.** They are not branding applied to generated content — they *are* the content. The [Fill Excel template](/admin-guide/agent-flows-blocks) block edits the uploaded workbook in place and hands back that same file, so its images, merged cells, conditional formatting and formulas all survive untouched and only the blanks you fill change.
## What Works
* Multiple templates can be active simultaneously
* Users select a template from the **Generate button** (wand icon) in the chat input
* Templates are grouped by category in the Generate dropdown
* The `/generate` command supports `--type`, `--template`, and `--category` flags
* Each supported PPTX layout type uses the matching template layout for its background/styling
* Layout badges in the admin show green for supported layouts, gray for unsupported
## Known Limitations
| Limitation | Detail |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Only 10 PPTX layout types** | Custom layout names (e.g., `TITLE_AND_BODY`, `BIG_NUMBER`, `MAIN_POINT`) are not supported. Slides requesting these fall back to the `blank` layout. |
| **No per-layout content placement** | Template placeholder positions are not used for content. All content is placed programmatically at fixed coordinates. |
| **No image placeholders** | Picture/image placeholders in templates are ignored. |
| **Title slide uses blank layout** | The title slide does not use a template's `title` layout — it's drawn programmatically. |
| **No template preview** | There is no visual preview of the template in the admin UI — download and open in PowerPoint/Keynote to inspect. |
## Managing Templates
### Activate / Deactivate
* Only **active** templates appear in the Generate dropdown for chat users
* Multiple templates can be active at the same time
* Click **Deactivate** to hide a template from users without deleting it
### Edit
Click **Edit** to open the full-page form where you can update:
* Name, description, category, and type
* Retrieval instructions and output instructions
* Upload a replacement file (re-parses layouts for PPTX)
### Download
Click **Download** to get the original template file for inspection or editing.
### Delete
Permanently removes the template and its file data. This cannot be undone.
## Creating a Compatible PPTX Template
To create a PPTX template that works with Experio:
1. Open PowerPoint or Google Slides
2. Go to **View > Slide Master**
3. Create or rename layouts using the supported names listed above
4. Design each layout with your branding (backgrounds, logos, fonts)
5. Ensure a layout named `blank` exists
6. Save as `.pptx`
7. Upload to Experio
Start with an existing Experio template (download one from the admin) and customize it to match your brand. This ensures all required layout names are present.
## Creating a Compatible DOCX Template
To create a Word template that works with Experio:
1. Open Microsoft Word or Google Docs (export as `.docx`)
2. Customize **Styles** (Home > Styles) for Title, Heading 1–4, Normal, and List Bullet with your brand fonts and colors
3. Add headers/footers with your logo and page numbers
4. Save as `.docx` — the body content is replaced at generation time; styles and headers/footers are preserved
5. Upload to Experio as type **Word (DOCX)**
Experio maps content to template styles using best-effort matching. Standard style names (`Heading 1`, `Normal`, `List Bullet`, etc.) work best.
## Using Templates in Chat
### Generate Button
A **Generate button** (wand icon) appears in two places when at least one template is active:
* **Chat input toolbar** — next to the model selector
* **Artifact Panel header** — shown when a long-form response is open on the right
From either location:
1. Click the **wand icon**
2. Browse categories in the dropdown (e.g., General, Case Study, Proposal)
3. Hover over a category to see its templates — each shows a type badge (`pptx`, `docx`, or `msg`)
4. Click a template name
Behavior differs slightly by entry point:
* **From the chat input toolbar** — the `/generate` command is filled into the chat input with the appropriate flags so you can review or edit it before pressing **Enter**.
* **From the Artifact Panel** — the `/generate` command is sent immediately using the current conversation context, so the agent can reformat the long response into the chosen template without a separate prompt step.
### Manual Command
You can also type the command directly:
```
/generate --type pptx --template "blue"
```
With a category:
```
/generate --type pptx --category "Case Study" --template "Standard"
```
#### Command Flags
| Flag | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------------------------- |
| `--type` | Yes | Output format: `pptx` or `message` |
| `--template` | No | Template name (case-sensitive match) |
| `--category` | No | Category name (case-insensitive match). When omitted, lookup is not filtered by category |
#### Template Lookup Priority
When resolving which template to use, the system checks in this order:
1. **Category + Template name** — both `--category` and `--template` specified
2. **Template name + Type** — only `--template` specified (backward-compatible)
3. **Category + Type** — only `--category` specified (uses first active template in that category)
4. **Type only** — neither specified (uses first active template matching the type)
### What the User Sees
1. The AI analyzes the conversation and composes the document
2. For **pptx**: a downloadable PowerPoint file appears in the chat as a source attachment
3. For **message**: formatted output appears directly in the chat
### Tips for Best Results
* Have a conversation with analysis/data **before** generating — the AI uses conversation context to build the document
* Use specific requests: "generate a case study summarizing the project above"
* Configure **retrieval instructions** on templates to guide what data the AI gathers
* Configure **output instructions** to control the structure and formatting of the result
* For PPTX, the AI automatically selects appropriate layout types (bullets for lists, tables for data, timelines for chronological content)
## Natural Language Template Detection
Experio automatically detects when a chat message matches an available template category and applies the corresponding template behind the scenes — no special commands needed.
* When a user types something like **"create a case study for Acme Corp"**, the system recognizes the intent matches the **Case Study** category and auto-applies that template's retrieval and output instructions to the generation pipeline.
* If **multiple templates** match the detected category, the assistant asks a follow-up question so the user can choose which template to use.
* If **no category** matches, the message is processed normally without any template behavior.
* This works alongside the **Generate button** — users can either type naturally or use the wand icon. Both paths produce the same template-powered output.
* **No configuration is required.** The system automatically checks all active template categories against incoming messages.
* **PPTX template by name:** "create a pptx using our project strategies template" matches the specific PPTX file by name, even without mentioning the category.
* **New generation in conversation:** Saying "now create a case study from that" starts a new generation request — the system treats it as a fresh query and matches the appropriate template. Follow-up messages that reformat existing output (e.g., "make it shorter") might not trigger template matching.
* **External data + template:** "create a case study based on my latest emails about ManTech" combines template matching with external tool retrieval in a single request.
Natural language detection only considers **active** categories and templates. Deactivate a category or template to exclude it from automatic matching.
## How PPTX Generation Works
When a user sends `/generate --type pptx --template "name"`:
1. The AI composes slides using the 10 supported layout types
2. For each slide, the system finds the matching layout in the template (case-insensitive)
3. A slide is created from that layout — inheriting its background and master styling
4. Content (title, bullets, tables, charts) is placed programmatically
5. If no matching layout is found, the `blank` layout is used as fallback
6. The final PPTX is returned as a downloadable file
# Enrichment Rules
Source: https://docs.experio.cloud/admin-guide/enrichment-rules
Enrich the knowledge graph with LLM-powered post-processing rules
## Overview
Enrichment rules let you enrich your knowledge graph **after** document ingestion is complete. Using prompt-based, LLM-powered rules, you can:
* **Add or update attributes** on existing nodes (e.g., tag projects with domains from a taxonomy)
* **Create new nodes** inferred from existing data (e.g., generate Obligation nodes from ContractClauses)
* **Create relationships** between nodes (e.g., link Employees to Skills based on resume content)
* **Create nodes and relationships** in one step (e.g., create Obligation and link it to the source)
Navigate to **Admin > Graph > Rules**.
## How It Works
Each enrichment rule has three parts:
| Part | Description |
| ---------- | ------------------------------------------------------------------------------- |
| **Input** | What to pull from the graph — target nodes and their attributes or related data |
| **Prompt** | Natural language instructions for the LLM, with placeholders for node data |
| **Output** | What to create — new attributes, nodes, relationships, or combinations |
The system processes each target node through the LLM and applies the results back to the graph.
## Creating a Rule
1. Click **Create Rule**
2. Enter a **name** and optional **description**
3. Configure the three sections below
### Target Configuration
| Field | Description |
| --------------------- | ------------------------------------------------------------------------------------------------- |
| **Target node label** | The entity type to process (e.g., Project, Employee, ContractClause) |
| **Target filter** | Optional Cypher filter to limit which nodes are processed (e.g., only nodes missing an attribute) |
### Input Configuration
Choose what data to send to the LLM for each node:
| Input mode | Description |
| ----------------------- | ----------------------------------------------------------------------------------- |
| **Full node** | All properties of the target node (except embeddings) |
| **Selected attributes** | Only the attributes you specify (e.g., `name`, `description`, `scope`) |
| **Neighborhood** | Include related nodes via relationships (e.g., Project → Status, Employee → Resume) |
### Prompt
Write a natural language prompt that guides the LLM. Placeholders are replaced with actual graph data before the prompt is sent.
#### Target node placeholders
| Placeholder | When available | Description |
| ---------------------- | ------------------------ | --------------------------------------------------------------------------------------- |
| **`{node}`** | Full node input mode | The entire target node as formatted text (all attributes except embeddings) |
| **`{attribute_name}`** | Selected attributes mode | Individual attributes from the target node (e.g., `{name}`, `{description}`, `{scope}`) |
Placeholder names must match the attribute names in your **Input configuration**. Use snake\_case if your graph uses it.
#### Related data placeholders
When **Neighborhood** is configured, you can reference related nodes:
| Placeholder | Description | Example |
| ------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- |
| **`{neighborhood}`** | All 1-hop neighbors (when using a wildcard neighborhood config) | Formatted list of every related node |
| **`{related.Label}`** | All related nodes of a specific label | `{related.Resume}` — all Resume nodes linked to the target |
| **`{related.Label.attribute}`** | A specific attribute from related nodes of that label | `{related.Resume.experience}` — experience text from each Resume |
The `Label` in `{related.Label}` must match the **target label** from your neighborhood configuration (e.g., Resume, Skill, Status).
#### Taxonomy expansion
Use **`@TaxonomyName`** to inject taxonomy values into the prompt. The tag is replaced with a list of active leaf values from that taxonomy type.
| Tag | Expands to |
| ------------- | ------------------------------------------ |
| `@Domain` | All leaf values in the Domain taxonomy |
| `@Industries` | All leaf values in the Industries taxonomy |
| `@Skill` | All leaf values in the Skill taxonomy |
The LLM receives the full list, which helps it choose from valid options. Define taxonomies in **Admin > Graph > Taxonomies** before creating rules.
#### Example prompt
```
Given this project:
Name: {name}
Description: {description}
Scope: {scope}
From the resume experience: {related.Resume.experience}
Classify this project into exactly one domain from: @Domain
Return only the domain name, nothing else.
```
#### Output format
The system automatically appends format instructions based on your **Output type**. For attributes, nodes, and relationships, the LLM is instructed to return valid JSON only — no markdown, prose, or bullet points. You can add **Output instructions** per attribute to give the LLM extra guidance (e.g., "Use title case" or "Pick the most specific match").
Prompts support the same `@TaxonomyName` syntax used in ingestion. Define your taxonomies before creating rules for best results.
### Output Configuration
| Output type | Description | Example |
| ----------------------- | --------------------------------- | ---------------------------------------- |
| **New attribute** | Add a property to the source node | Set `domain` on Project |
| **Updated attribute** | Modify an existing property | Update `risk_level` |
| **New node** | Create a new entity | Create `Obligation` node |
| **Relationship** | Link source to an existing node | `Employee -[:HAS_SKILL]-> Skill` |
| **Node + relationship** | Create node and link to source | `ContractClause -[:GRANTS]-> Obligation` |
For relationships, specify the relationship type and target node label. The LLM will match or create target nodes based on your prompt.
## Running Inference Jobs
1. Save your rule
2. Click **Run Job** (or use the dropdown on the rules list)
3. Optionally enable **Overwrite existing** to re-process nodes that already have results
4. Monitor progress in the **Jobs** tab
The rules list shows **compatibility status**. An **invalid** rule (for example, its target label or
output attribute is no longer in the ontology) cannot run until you fix it and **Re-validate**. See
[Ontology Compatibility](/admin-guide/ontology-compatibility).
### Job Status
| Status | Description |
| ------------- | ------------------------- |
| **Queued** | Job is waiting to start |
| **Running** | Job is processing nodes |
| **Completed** | Job finished successfully |
| **Failed** | Job encountered an error |
| **Cancelled** | Job was stopped by user |
### Job Actions
* **View** — See job details and results
* **Cancel** — Stop a running job
* **Resume** — Continue a failed or cancelled job from where it left off
## Viewing Results
* **Per rule:** Open a rule and go to the **Jobs** tab to see execution history
* **All jobs:** Use the **Executions** tab on the Rules page to see all enrichment jobs across rules
* **Job details:** Click any job to view processed nodes, errors, created/updated entities, and
**lineage record ids** linking to graph audit entries when present
### Graph lineage
Each successful enrichment write also creates a **graph lineage** record on the target node. Open the
entity in the graph explorer to see which rule and model produced each attribute change. See
[Graph Lineage](/admin-guide/graph-lineage) for details.
## Flow Integration
Enrichment rules can be used as steps in **Flows**. Add an **Enrichment** node to your flow and select the rule to run. The enrichment step runs when the flow executes, either manually or on a schedule.
The flow editor warns when the selected rule is **invalid** against the current ontology. Fix the rule
before you rely on that flow.
## Configuration
Settings that affect enrichment (available in **Admin > System Settings**):
| Setting | Description | Default |
| --------------------------------------- | ------------------------------------------------------------------ | ------- |
| **ENRICHMENT\_NODE\_CONCURRENCY** | Number of nodes processed in parallel per job | 4 |
| **ENRICHMENT\_RESULT\_RETENTION\_DAYS** | Days to keep EnrichmentResult rows before purge (0 = keep forever) | 90 |
## Best Practices
* **Start with taxonomy** — Define taxonomies before creating rules; `@TaxonomyName` expansion improves classification accuracy
* **Test on a subset** — Use a target filter to limit nodes when testing a new rule
* **Review results** — Check job results after the first run to ensure the prompt produces expected output
* **Use specific prompts** — Clear, specific prompts yield better results than vague instructions
Enrichment runs asynchronously via RabbitMQ. Ensure the enrichment worker is running (e.g., in Docker Compose) for jobs to process.
# Extraction Policy
Source: https://docs.experio.cloud/admin-guide/extraction-policy
Control ingestion depth, Excel handling, and model tiers per content type
## Overview
Extraction policy controls how deeply ingestion runs LLM entity extraction for each content type.
Use it to reduce cost on low-value files (for example large Excel exports) while keeping full
extraction on types that need rich graph data.
Navigate to **Admin > Data Sources > Content Types**, open a type, and scroll to **Ingestion
extraction** on the **Basic Information** tab.
## Extraction modes
| Mode | Behavior |
| ---------------------- | ------------------------------------------------------------------------------- |
| `full` | Normal LLM entity extraction (default) |
| `metadata_and_snippet` | Document shell entity plus a short text preview; no chunked LLM extraction |
| `metadata_only` | Shell entity from filename, path, and classification only; no content LLM calls |
For Excel (`.xlsx`, `.xlsm`, `.xls`), resolution order is:
**filter override → content-type Excel mode → default mode → `full`**
Parsed spreadsheet text is still stored on the Document node for chat retrieval even when
extraction is skipped.
## Content-type settings
Configure in the admin UI or in the content type's JSON metadata under `extraction_policy`:
```json theme={null}
{
"extraction_policy": {
"default": {
"mode": "full",
"model_tier": "large",
"validation_pass": true
},
"excel": {
"mode": "metadata_only",
"validation_pass": false,
"snippet_chars": 2000
}
}
}
```
### UI fields
| Field | Purpose |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| **Default mode** | Extraction depth for most file types |
| **Primary model tier** | `large`, `medium`, or `small` for primary extraction |
| **Run validation pass (default)** | Secondary LLM pass to fill gaps; skipped when policy disables it or heuristics apply |
| **Excel mode** | Override default for spreadsheets, or **Same as default** |
| **Run validation pass (Excel)** | Shown when Excel mode differs from default |
### Model tiers
| Tier | System setting | Used for |
| ----------------- | ------------------------------- | ----------------------------------------------- |
| `large` (default) | `INGESTION_LARGE_MODEL_CONFIG` | Primary extraction |
| `medium` | `INGESTION_MEDIUM_MODEL_CONFIG` | Primary extraction when set on the content type |
| `small` | `INGESTION_SMALL_MODEL_CONFIG` | Primary extraction when set on the content type |
Secondary steps (validation, JSON repair, relationship backfill, entity disambiguation) use
`INGESTION_SMALL_MODEL_CONFIG`, falling back to the large model if unset.
Create **Ingestion - Medium** model configurations under [Model Configurations](/admin-guide/model-configurations)
and assign one in [System Settings](/admin-guide/system-settings) before using the medium tier.
## Excel sheet handling
Spreadsheets are parsed by Kreuzberg. Each sheet becomes a markdown block headed by `## SheetName`.
Ingestion splits on those headers and applies caps per sheet:
| System setting | Default | Purpose |
| -------------------------------------- | ------- | ------------------------------------------------------------------------- |
| `MAX_EXCEL_SHEET_CHARS` | `50000` | Skip LLM extraction on sheets above this size |
| `MAX_EXCEL_INGESTION_CHUNKS_PER_SHEET` | `25` | Cap LLM chunks per sheet in full mode |
| `INGESTION_COST_GUARD_CHUNK_THRESHOLD` | `120` | Estimated chunk count above which full mode falls back to `metadata_only` |
These settings are seeded in [System Settings](/admin-guide/system-settings). The cost guard
threshold is also editable from the dashboard.
## Filter-level Excel controls
When configuring [data source filters](/admin-guide/data-sources), you can control spreadsheet
ingestion per filter:
| Field | Default | Purpose |
| ----------------------- | ------- | ---------------------------------------------------------------------------------- |
| `parse_excel_files` | `true` | Opt in to Excel ingestion for matching files |
| `excel_extraction_mode` | inherit | Override content-type Excel mode (`full`, `metadata_and_snippet`, `metadata_only`) |
| `excel_max_sheet_chars` | inherit | Per-filter per-sheet character cap |
When **Ingest Excel files** is unchecked and a file matches an enabled filter, Excel ingestion is skipped
with reason `excel_ingestion_disabled_by_filter`. Files with **no matched filters** still ingest
Excel (legacy behavior).
Use unchecked **Ingest Excel files** on export-only filters when you want those spreadsheets excluded from the graph.
## Example configurations
| Content type | Excel mode | Typical use |
| ------------------------ | ---------------------- | ------------------------------------------ |
| Requirements / exports | `metadata_only` | Client input spreadsheets, inventory dumps |
| Deliverable | `metadata_and_snippet` | Artifacts where a short preview is enough |
| Structured workbook type | `full` | Sheets where row-level entities matter |
Pair export-style content types with filters that leave **Ingest Excel files** unchecked unless you
explicitly want those files in the graph.
# Flows
Source: https://docs.experio.cloud/admin-guide/flows
Create and manage automated workflows
## Overview
Flows are automated workflows that define sequences of operations for data processing, transformation, or other administrative tasks. They can be run manually, scheduled, or triggered by events.
Navigate to **Admin > Data Sources > Flows**.
## Viewing Flows
The flows page displays a grid of flow cards, each showing:
* Flow name and description
* **Status badge** — Active, Running, Paused, or Draft
* Schedule indicator (if the flow runs on a schedule)
* Last execution time
## Creating a Flow
1. Click **Create New Flow**
2. Define the flow name and description
3. Use the visual flow editor to build your workflow
4. Save and optionally activate the flow
## Flow Editor
The flow editor provides a visual interface for building workflows by connecting steps and defining logic.
### Flow Node Types
| Node type | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Enrichment** | Runs an enrichment rule to enrich the knowledge graph. Configure by selecting an existing rule from **Admin > Graph > Rules**. |
| **Reader** | Scans data sources for new or updated files |
| **Ingestion** | Processes documents and extracts entities into the graph |
See [Enrichment Rules](/admin-guide/enrichment-rules) for how to create and configure rules. The editor
warns if the selected enrichment rule is invalid against the current
[ontology](/admin-guide/ontology-compatibility).
## Running Flows
### Manual Execution
Click **Run Now** on any flow card to execute it immediately. The status changes to **Running** and you can monitor progress in the execution details.
### Scheduled Execution
Flows can be configured to run on a schedule. Scheduled flows show an indicator on their card.
## Monitoring Executions
### Execution History
Each flow maintains a history of executions. View the list at the flow's executions page, which shows:
* Execution start and end times
* Duration
* Status (success, failure, in progress)
* Number of items processed
### Execution Details
Click any execution to see detailed information:
* Step-by-step execution log
* Input and output data for each step
* Error messages for failed steps
* Processing metrics
## Managing Flows
| Action | Description |
| ---------------- | ------------------------------------------------- |
| **Edit** | Modify flow configuration and steps |
| **Run Now** | Execute the flow immediately |
| **Pause/Resume** | Temporarily disable or re-enable a scheduled flow |
| **Delete** | Remove the flow (requires confirmation) |
Deleting a flow also removes its execution history. Export any important execution data before deleting.
# Graph Access Control
Source: https://docs.experio.cloud/admin-guide/graph-access-control
Set up who can see which records in the knowledge graph, in the order the policy compiler expects
## Overview
Graph access control decides **which records in the knowledge graph a given person may see**. It works by
computing, for each person, the set of records they are entitled to, and then adding that set as a filter to
every graph query made on their behalf. A person who is entitled to nothing sees nothing; there is no partial
or best-effort answer.
Policy lives in Postgres, in the **Graph Authorization** tables. It is deliberately **not** seeded: every
deployment's org structure differs, so a new install starts empty and an administrator enters the policy that
matches their own graph.
There are two surfaces, and they do different jobs:
| Surface | Use it to |
| ----------------------------------------------------- | ------------------------------------------------------------------- |
| **Staff admin** (`/staff/`) > **Graph Authorization** | **Create and edit** the policy rows |
| **Admin > Graph > Access control** | **Read and check** the resulting policy — and switch org scoping on |
This page is the order to create the rows in.
**The order matters, and three of the steps fail silently when they are done wrongly.** The policy still
compiles, the admin screens still look correct, and the only symptom is that somebody sees fewer records than
they should — or, in one case, that a manager quietly stops seeing their team's work. Each trap below is
covered by a test in `server/experio/graph_authz/tests/test_manual_bootstrap.py`; if you change this page,
change that file too.
## The four things a policy is made of
| Table | Answers | Example |
| ------------------------ | --------------------------------------------------------------- | ------------------------------------------ |
| **Roll-up axis** | "Which edge describes containment, and which way do I walk it?" | A division contains its engagements |
| **Baseline** | "Is this label restricted, or can everyone read it?" | `Engagement` is private, `Skill` is public |
| **Baseline → axis link** | "Which containment axis applies to this label?" | `Engagement` rolls down the division axis |
| **Grant rule** | "Which edge attaches a person to the graph in the first place?" | `WORKS_ON_PROJECT` |
A working policy needs all four. Three of them on their own grant nobody anything.
## Setting up a new deployment
In **Staff admin > Graph Authorization > Roll-up axes**, add one row per containment relationship in your
ontology — the edges that mean "this thing contains that thing".
For each axis set:
* **Name** — anything descriptive; it is only used for display and ordering.
* **Edge element id** — the relationship type, exactly as it appears in the graph.
* **Direction** — `down` follows the arrow, `up` walks it in reverse. This is **mechanical, not semantic**: it
describes how your graph happens to store the edge, not whether the axis means "upward" in the org chart.
To pick the direction, look up the relationship **in your own ontology** and ask which way its arrow
points — then ask which way you want to walk it. Following the arrow is `down`; walking it in reverse
is `up`.
Worked through on one deployment, whose ontology names these edges `HAS_DIVISION` and
`IS_IN_DIVISION`: the first is stored Firm → Division, so walking firm-to-divisions follows the arrow
and is `down`; the second is stored Engagement → Division, so walking division-to-engagements goes
against it and is `up`.
**Check the names against your own ontology rather than copying these.** They are not fixed across
deployments — the same two relationships ship as `HAS` and `BELONGS_TO` in the default ontology. An
axis naming an edge your graph does not have is not an error: it simply matches nothing, and the
symptom is a flat tree or a count that reads zero.
At this point the axes exist but govern nothing.
In **Graph Authorization > Baselines (OWD)**, add one row per label in your graph. Set **default access** to:
* `private` for labels whose records are access-controlled;
* `public_read` for labels everyone may read.
**A label with no baseline row at all is treated as `private`.** The default is deny, so forgetting a label is
not a no-op — it makes that label invisible to everyone until somebody notices. List every label your graph
actually contains, not just the ones you intend to restrict.
Open each baseline that should inherit down a hierarchy and set its **roll-up axis**.
This is the step that turns an axis into a containment axis, and it is the one with no visible symptom when
it is missed — see [An axis you forgot to attach](#an-axis-you-forgot-to-attach) below.
In **Graph Authorization > Grant rules**, add the edges that attach a person to the graph. There are two
kinds, and they are **not** interchangeable — the difference is where the edge comes from:
| Kind | Attaches a person by | Who writes that edge | Status |
| -------------- | ------------------------------------------------------------------- | ---------------------------------------- | --------------- |
| **Team** | the work they are on — `WORKS_ON_PROJECT`, `IS_MANAGING_ENGAGEMENT` | **Ingestion**, as a fact it discovered | The kind to use |
| **Membership** | where they sit in the org — `IS_PART_OF_DIVISION` | Nothing, unless your ingestion writes it | **Retired** |
**Use team rules. Membership is retired — do not build a policy on it.**
A membership rule only grants something if your graph really contains that edge. On the reference
deployment there are **no** `IS_PART_OF_DIVISION` or `IS_PART_OF_FIRM` edges at all — ingestion never
writes them — so those rules sat in the policy granting nothing, and removing them changed no one's access.
The kind still exists in the data model, and a rule saved with it still compiles and still walks its edge.
What changed is that **no screen offers it, measures from it, or recommends it any more.** Access control
previously derived its coverage numbers from membership rules and, finding none, advised admins to create
them — advice that widens every scope to fix a problem that does not exist.
There is no screen that tells you an edge is missing before you save the rule. Check the relationship in
[Ontology](/admin-guide/ontology) first, or use **Preview as user** afterwards and confirm the number moved.
Until at least one grant rule exists, a completely correct hierarchy still resolves to nothing for everyone,
because nothing connects a person to a starting point.
### What each kind gives a person
**Team rules work bottom-up.** A person reaches the work they are attached to, and the containment axes then
carry that outward — see the engagement a project belongs to, the client that engagement is for, and the
other projects under it. On the reference deployment one consultant is directly attached to 146 projects and
can see 313; the other 167 arrive purely through that fan-out.
**Membership rules were meant to work top-down** — a person placed at a division reaching everything filed
under it, including work they were never staffed on. That mechanism needs `IS_PART_OF_*` edges, and nothing
writes them, so it has never granted anybody anything here.
**A policy built only from team rules is the supported configuration.** Its cost, stated plainly: a person
with no work edges sees **nothing**, and there is no rule that changes that. To give access to somebody who
delivers no work — a new joiner, or a leader who oversees — attach them to the work itself, or add the
[management roll-up](#optional-the-management-roll-up) so they inherit what their reports can see.
Leave **grantee principal**, **grantee selector**, **source principal** and **record filter** empty. They are
not yet implemented, and a rule that sets them would grant access to **everyone** rather than to the principal
named. The admin form disables them and refuses to save a rule that has them set.
## Optional: the management roll-up
If you want managers to inherit what their reports can see, add **one** further roll-up axis for the
management edge — and do **not** attach it to any baseline. An axis with no baseline pointing at it is what
the compiler treats as the management axis.
Its **direction must be `up`**: the edge is stored report → manager, so the roll-up walks it in reverse.
## Traps
### An axis you forgot to attach
Skipping step 3 does not raise an error and does not empty anything. It **reclassifies** the axis: because it
is attached to no baseline, the compiler reads it as the management axis instead. There is no warning to read,
because nothing recorded what the axis was intended for.
**Symptom:** records do not inherit down the hierarchy, and the management roll-up behaves oddly.
**Check:** every axis you meant as containment is named by some baseline's roll-up axis field.
### Two unattached axes
Only one management axis is supported. When there is more than one, the compiler picks the one whose **name
sorts first alphabetically**.
The resulting diagnostic **names the wrong axis**. It is filed against the axis that lost, so it reads as "the
management axis was not applied" when the real cause is that some *other* axis was never attached to a
baseline. If you see that message, check for unattached axes before you touch the management axis itself.
### A management axis pointing `down`
A management axis with direction `down` is not evaluated, and every manager silently stops inheriting their
reports' scope.
This one is especially hard to spot from the outside, because the compiled policy still reports the management
edge as `IS_MANAGED_BY` afterwards — that is a built-in default filling the hole, not your axis being
honoured. The field that actually says whether the roll-up is on is **include report scopes**.
## Checking your work
Use **Admin > Graph > Access control > Preview as user** to check what a named person can see before you
switch enforcement on, and the **Diagnostics** tab for what the compiler dropped. Two results are worth
treating as failures even though neither raises an error:
* **Everyone sees the same thing.** Scoping is not doing anything — usually a missing baseline or a missing
axis attachment.
* **Everyone sees nothing.** Usually a missing grant rule, so nobody has a starting point.
Test with **at least three people at different depths** of the org structure. A single account cannot
distinguish a working policy from either failure above.
### Graph reachability
**Diagnostics > Graph reachability** answers a narrower question than either of the above: *for each private
label, can the policy reach that kind of record at all, and over which relationship?* It starts from the
labels the grant rules land on, follows the compiled roll-down and the propagation rules, and reports a
count per label with the edge it travelled.
```
Proposal held: 1,975 reached: 657 walking SUBMITTED_TO
```
Read it as a **ceiling on the policy, not as anybody's access.** A person still sees only the records they
are linked to; the manager roll-up and superuser accounts start somewhere else and are not counted here.
What it is good for is a label whose count is far below its total, or collapses toward zero: that says the
relationship it depends on is missing from the data, and names the edge to go and look for. What it is *not*
is a target to drive to 100% — plenty of records legitimately have no such edge. For "which specific records
can nobody reach", use **unreachable records** below, which measures real people rather than a walk.
### Seeing the filter that was applied
When a superuser runs a graph query from chat, the tool result carries the query the filter actually produced,
in two forms:
* **`authz_rewritten_query`** — what ran, with the access filter still held as a parameter.
* **`authz_executable_query`** — the same thing with the values filled in, so it can be pasted into a console
and run as-is.
This is the fastest way to answer "did the filter apply, and to what?" — before blaming the policy for an
answer you did not expect. If the number looks wrong, read the query the model wrote before assuming the
filter is at fault; a question the model misread produces a wrong number with a perfectly correct filter.
Both are shown to **superusers only**, and are absent — not blank — for everyone else. The filled-in form
lists every record the person is entitled to, so it describes their access in full.
If the list is too long to show, you get a message saying how many entries were left out rather than a
shortened query. A cut-down filter is still valid and still runs — it just quietly answers a narrower
question, which is worse than showing nothing.
### Citations, and why a scoped answer can have none
Every chat answer that draws on the graph is accompanied by an **evidence index** — the internal list of
records the report writer is allowed to cite, which becomes the citation chips in the answer and the entries
in the **Provenance** panel. Access control applies to that list as well as to the answer, so a person only
ever sees citations for records they may read.
Two consequences are worth knowing before you read them as faults:
* **An answer can be correct and carry no citations.** The list is built from a capped sample of the records
a question touched, and the cap is applied by the graph engine *before* access control. On a broad question
— "list our companies" against several hundred — someone entitled to a small share of them can miss the
sample entirely, and the answer arrives with no chips. The figures in it are still correctly scoped; they
come from the filtered query, not from this list. The server log says so explicitly, on a line beginning
`[EVIDENCE HYDRATION] clearance filter dropped ALL`.
* **A narrow question is the way to get citations back.** Asking about a named company or engagement puts the
relevant records inside the sample, and the chips return.
If citations vanish for **everyone**, including people entitled to almost everything, that is not this — check
the server log for `[EVIDENCE HYDRATION] refusing to hydrate`, which means enforcement is on but the request
carried no identity. That is a fault, and it fails safe: no records are disclosed.
### Records nobody can reach
**Diagnostics > unreachable records** counts records that **no one** can reach, by working out every person's
access and combining it. A record appears there because nothing in the graph connects it to anybody — not
because the policy is wrong.
The fix is upstream, in the data: an [enrichment rule](/admin-guide/enrichment-rules) that gives those records
the missing relationship. Widening the policy to cover them is the wrong instrument — it grants far more than
the records in question.
## A worked policy, end to end
The demo deployment's policy is reproduced below. It is worth reading even if your labels differ, because it
shows the shape a working policy takes: **a small number of private labels, each reachable over exactly one
relationship from something a person already holds.**
Nine labels are private; the remaining 106 are `public_read`.
Read each row in the direction the rule itself is written: **hold the left, and you may also read the right.**
`from_element_id` on an `AuthzPropagationRule` is what you *already have*, and writing the table the other way
round is a good way to end up with rules that look right and grant nothing.
| If you hold… | over | direction | …you may also read |
| ------------ | ------------------- | --------- | --------------------- |
| `Project` | `HAS_PROJECT` | inbound | `Engagement` |
| `Project` | `DEFINES_WORK_FOR` | inbound | `Contract` |
| `Engagement` | `HAS_ENGAGEMENT` | inbound | `Company` |
| `Engagement` | `FOR_ENGAGEMENT` | inbound | `Contract` |
| `Engagement` | `IS_IN_DIVISION` | outbound | `Division` † |
| `Company` | `BINDS` | inbound | `Contract` |
| `Company` | `SUBMITTED_TO` | inbound | `Proposal` |
| `Company` | `EMPLOYED_BY` | inbound | `Contact` |
| `Company` | `IS_FOR` | inbound | `PastPerformance` |
| `Company` | `HAS_OPPORTUNITY` | outbound | `Opportunity` |
| `Company` | `ISSUED` | outbound | `Solicitation` |
| `Division` | `HAS_DIVISION` | inbound | `Firm` † |
| `Division` | `IS_SUBDIVISION_OF` | outbound | `Division` † (3 hops) |
† `Division` and `Firm` are `public_read`, so these three are not security grants — everyone can read all
divisions and the firm regardless of them. They exist to give the clearance objects their values.
Access originates from **work**: a grant rule on `WORKS_ON_PROJECT` / `IS_MANAGING_PROJECT` /
`IS_MANAGING_ENGAGEMENT` gives a person the projects and engagements they are on, and the rules above walk
outward from there. `Project` is private but never appears in the right-hand column — nothing propagates *to*
it, because it is where access starts.
Nothing in the table grants anything on its own. Each row only says "if you already hold the label in the
first column, you may also read the one in the last."
Use a **propagation rule**, never the `controlled_by` field on the baseline. `controlled_by` reads like the
answer and is not evaluated: a label set private with only `controlled_by` compiles to plain `private` with no
derivation at all, which means **invisible to everyone**.
### What making a label private actually costs
Roughly half of each label above has no path to anybody. Those records become unreadable by ordinary clearance,
and that is the correct outcome rather than a defect — nothing in the source data connected them to a person,
so they have no parent in the security graph. See [Records nobody can reach](#records-nobody-can-reach); the
[super-admin bypass](#checking-your-work) is the designed way in when someone needs them.
Measured on the demo dataset when these nine were first applied — before, every person could read 100% of all
of them:
| Label | Records | Readable by a given person, after |
| ---------- | ------- | --------------------------------- |
| `Contract` | 2,298 | 127–197, depending on their work |
| `Proposal` | 1,975 | a per-person slice |
| `Contact` | 836 | a per-person slice |
A person on no projects reads **none** of them, which is the intended answer and not a fault.
### Where to stop
Coverage is the test. `ContractClause` (18,908 records) and `Document` (9,959) hang off documents rather than
off work, so only 29% and 11% of them are reachable at all. Making those private would hide 13,438 and 8,874
records from everybody — closing a disclosure by deleting the data from view, which is not a fix.
These are **database rows**, not code, so they do not travel with a release. A new deployment needs them
entered as part of [Setting up a new deployment](#setting-up-a-new-deployment), and
**Diagnostics > unreachable records** is how you check the result.
## Related
* [Ontology](/admin-guide/ontology) — the labels and relationship types this policy refers to
* [Graph backend (Neo4j & FalkorDB)](/admin-guide/graph-backend)
* [Enrichment rules](/admin-guide/enrichment-rules) — how to connect records that no relationship reaches
# Graph backend (Neo4j & FalkorDB)
Source: https://docs.experio.cloud/admin-guide/graph-backend
Configure the active graph database, run migrations, and understand Neo4j vs FalkorDB behavior
## Overview
Experio stores the **knowledge graph** (ontology instances, document relationships, embeddings on graph nodes)
against either **Neo4j** or **FalkorDB**. Exactly one backend is **active** per deployment. The application routes
reads and writes through a shared abstraction layer so ingestion, chat retrieval, and admin tools stay consistent.
## Choosing the active backend
Set **`GRAPH_PROVIDER`** to `neo4j` or `falkordb` in **Admin > Settings > System Settings** (DB category). Connection
fields for the **inactive** provider remain stored but are ignored until you switch.
See the [DB settings reference](/admin-guide/system-settings#db-settings) for all keys. After changing provider or
connection details, validate connectivity using the dashboard flow (including the reachability probe surfaced when
switching providers).
## Graph migration (staff)
Staff can copy data between backends from **Admin > Settings > Graph migration**:
* **Neo4j → FalkorDB** and **FalkorDB → Neo4j** jobs stream progress over a WebSocket.
* Use prerequisites on that page (backup, connectivity) before starting a long run.
Product and engineering decisions for the reverse direction are documented in
[FalkorDB → Neo4j migration research](/graph-migration/falkordb-to-neo4j-migration-research).
## Behavioral differences
| Topic | Neo4j | FalkorDB |
| ------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Cypher** | Full Neo4j 5.x surface used by Experio | Subset; LLM Cypher generation uses Falkor-specific dialect notes |
| **Date / time properties** | Native temporals via the Bolt driver | Native `Datetime` / `Date` via `localdatetime($param)` / `date($param)` at write time |
| **Relationship fan-out stats** | Portable batch query (same on both providers) | Same portable batch query; refresh schema cache to populate stats |
| **Typical production default** | Common default (`keep_alive`) | Available in cluster (`keep_down` by default) |
## FalkorDB resilience & query limits
* **Per-query memory cap** — FalkorDB is started with `QUERY_MEM_CAPACITY` set to **1 GiB**.
A single query that exceeds the cap is terminated by FalkorDB with
`Query's mem consumption exceeded capacity` instead of pushing the container toward its
memory limit (and an OOM restart). If a legitimate analytical query hits the cap, narrow
the query; the cap is defined in the deployment spec (`FALKORDB_ARGS`).
* **Per-query runtime cap** — `TIMEOUT_DEFAULT` is set to **60 seconds**. FalkorDB's
default is `0` (unlimited). Queries that omit an explicit `TIMEOUT` (including an
unbounded FalkorDB Browser `MATCH (n) RETURN n`) are killed at 60s instead of holding
the graph lock indefinitely while Redis `PING` still succeeds. 60s matches the Experio
client `socket_timeout`. Do not set the deprecated `TIMEOUT` alongside
`TIMEOUT_DEFAULT` — the combination aborts module load. `TIMEOUT_MAX` is left at `0`
so a client may pass a longer per-query `TIMEOUT` when needed.
* **Fail-fast on outages** — graph calls use a 60s read timeout. While FalkorDB is
restarting (it reloads its dataset into memory and answers `LOADING` during that window),
chat and ingestion graph queries fail quickly with a provider-unavailable error instead of
hanging. Expect `/health/` to report `status: degraded` until the reload completes, then
queries recover automatically — no server restart is needed.
## Omnistrate / cluster deployment
Both **Neo4j** and **FalkorDB** run as internal cluster services with **manual
scale-to-zero**. They are independent of platform boot — server, workers, and
`initdb` no longer wait for a graph DB to be healthy.
### Default state
| Service | Scaling mode | Replicas | Notes |
| ---------- | ------------ | -------- | -------------------------------------- |
| `neo4j` | `keep_alive` | 1 | Matches historical production behavior |
| `falkordb` | `keep_down` | 0 | No cost until you enable it |
Seeded connection defaults (via `initdb` → `seed_config`):
* `FALKOR_URI`: `redis://falkordb:6379` (cluster internal DNS)
* `NEO4J_URI`: `neo4j://neo4j:7687`
Passwords come from Omnistrate secrets `neo4jPassword` and `falkordbPassword`.
See [Omnistrate deployment](/omnistrate#omnistrate-secrets).
### Controlling graph DB replicas
Use Django admin **ServiceConfiguration** (same UI as JupyterLab scale-to-zero):
* **`keep_alive`** — maintain 1 replica (use for the active provider)
* **`keep_down`** — maintain 0 replicas (use for the inactive provider to save cost)
* **`auto`** — manual strategy defaults to 1 replica; prefer explicit modes above
Changes are applied by the scale-to-zero sidecar on the server pod (polls every
\~60s). Allow a few minutes after toggling before expecting connectivity.
### Migration window (both providers up)
Graph migration requires **both** backends reachable:
1. Set **`neo4j`** and **`falkordb`** to **`keep_alive`** in ServiceConfiguration
2. Wait until both pass connectivity checks (Admin → System Settings → provider probe,
or inspect `/health/` — graph may show `checks.neo4j` unhealthy until warm)
3. Run migration from **Admin → Settings → Graph migration**
4. Switch **`GRAPH_PROVIDER`** in System Settings
5. Set the **inactive** backend to **`keep_down`**
### Health monitoring
`GET /health/` reports the active graph provider under `checks.neo4j` (legacy key
name; includes `provider: neo4j | falkordb`). Graph downtime does **not** fail the
whole endpoint:
* **`status: degraded`** — platform OK, graph unavailable (expected during scale-to-zero)
* **`status: unhealthy`** — Postgres, Redis, RabbitMQ, or LLM failed
Chat and graph ingestion need the active provider up; other admin features may work
while status is `degraded`.
### Schema cache
On server startup a background thread pre-warms the graph schema cache in Redis
(24h TTL). If the graph is down at boot, warming fails softly and the first chat
query may be slower until cache is populated. With `neo4j` on `keep_alive`, this
is rarely an issue in steady state.
The cache includes **relationship fan-out statistics** (`fan_out_risk`, max distinct
targets per source, max distinct sources per target) computed from live graph data.
After upgrading or switching graph providers, refresh the schema cache so fan-out
stats and aggregated property types reflect the active backend.
For how relationship attributes interact with graph introspection, see
[Relationship attributes](/architecture/relationship-attributes).
## Local development
When using the devcontainer, FalkorDB and its Browser UI are available for ad-hoc Cypher. See
[FalkorDB Browser (local development)](/local-development/falkordb-browser).
# Graph Evaluation
Source: https://docs.experio.cloud/admin-guide/graph-evaluation
Manually audit graph extraction quality for completed document ingestion jobs
## Overview
**Graph evaluation** runs an LLM-as-judge over ingested documents to assess whether extracted
graph facts match the source text and content-type schema. It is **observational only** — it does
not modify the graph or re-trigger ingestion.
Evaluation uses **Graph Lineage** records plus live graph node state (including matched existing
nodes) as input. See [Graph Lineage](/admin-guide/graph-lineage) for how ingestion writes are
recorded.
Open **Admin → Process → Graph Evaluation** for the runs list, filters, and run detail pages. You
can also start a run from a completed job via **Process → Jobs** → job detail → **Run Evaluation**.
## When you can run evaluation
The **Run Evaluation** button is available when:
| Requirement | Details |
| -------------------------- | ---------------------------------------------------------- |
| **Terminal job status** | Job is completed, failed, stopped, or interrupted |
| **Unstructured ingestion** | Structured/API datasources use the statistics view instead |
| **Full ingestion mode** | Parse-only jobs have no graph output to evaluate |
| **Ingested files** | At least one file reached `ingested` status |
Only one evaluation run may be **pending** or **running** per job at a time.
## Running an evaluation
### From the Graph Evaluation page
1. Go to **Process → Graph Evaluation**
2. Click **Run Evaluation** and pick a completed unstructured job
3. Set **samples per content type** (default from `GRAPH_EVALUATION_SAMPLES_PER_CONTENT_TYPE`, max 50)
4. Confirm — the run is queued to the graph evaluation worker
5. Open the run detail page to watch progress and review results
### From a job detail page
1. Open **Process → Jobs** and select a completed unstructured job
2. In the **Graph Evaluation** card, click **Run Evaluation**
3. Confirm samples per content type — you are redirected to the run detail page when the run starts
Use **View Evaluations** on the job card to filter the runs list to that job.
While a run is active, the UI polls every few seconds until it completes or fails.
### Automatic runs after job completion
When **`GRAPH_EVALUATION_AUTO_RUN_AFTER_JOB`** is enabled in **Admin → System Settings**, Experio
queues an evaluation automatically once **every file** in the job has reached a terminal processing
status (including `ingested`, `failed`, `skipped`, and other finalized states). The run uses the
configured default sample count (`GRAPH_EVALUATION_SAMPLES_PER_CONTENT_TYPE`).
Auto-run uses the same eligibility rules as manual runs (unstructured, full ingestion, at least one
ingested file, terminal job status, no other pending/running evaluation). Parse-only jobs and jobs
with no ingested files are skipped silently.
## Reading results
### Run summary
Each run shows:
* **Status** — pending, running, completed, failed, or cancelled
* **Sample count** and **issue count** aggregated across samples
* **Summary line** — evaluated / skipped / failed counts
### Runs list
The **Graph Evaluation** page supports search, status filters, data source filters, and pagination.
Filter by job using the job link or `scanOrderId` query parameter from a job detail page.
### Samples list
On a run detail page, the left column lists sampled files with:
* **Status** — evaluated, skipped (no lineage), or failed
* **Issue count** for evaluated samples
* **Overall quality** — good, acceptable, or poor
Click a sample to open its details. The panel auto-selects the sample with the most issues.
### Sample details and issues
The right column shows:
* File name, overall quality badge, and content type
* Judge summary for the document
* **Issues (N)** — each issue includes category, severity, description, and document evidence
* **Lineage links** when the issue references a graph element with lineage records
### Issue categories
| Category | Meaning |
| --------------------------------------------------- | ------------------------------------------------------- |
| `missed_entity` / `missed_relationship` | Supported by the document and schema, absent from graph |
| `hallucinated_entity` / `hallucinated_relationship` | Present in graph but not supported by document text |
| `incorrect_attribute` | Entity exists but a property contradicts the document |
| `incorrect_relationship_endpoint` | Relationship type plausible but wrong source/target |
| `schema_violation` | Label or relationship not defined in the content type |
## Skipped samples (no lineage)
Files ingested **before Graph Lineage** was enabled may have no lineage records. Those samples
are marked **skipped (no lineage)** with guidance to re-ingest the file before evaluating.
## Matched existing nodes
When ingestion links a document to a **pre-existing graph node** (entity resolution / matching),
lineage may only record a new relationship — not a full node create. Evaluation enriches the
judge input with **live graph properties** for relationship endpoints so matched entities are not
falsely flagged as missing.
## API
Authenticated admin REST endpoints (prefix `/api/graph-evaluation/`):
| Endpoint | Purpose |
| --------------------------- | -------------------------------------------------------------- |
| `POST /preview/` | Estimate sample counts before starting a run |
| `POST /runs/` | Create and queue an evaluation run |
| `GET /runs/` | Paginated runs list (search, status, data source, job filters) |
| `GET /runs/{id}/` | Run detail and aggregates |
| `POST /runs/{id}/cancel/` | Cancel a pending or running evaluation |
| `GET /runs/{id}/samples/` | Sample list for a run |
| `GET /samples/{id}/` | Sample detail with issues |
| `GET /samples/{id}/issues/` | Issues only |
## Worker and infrastructure
The **graph-evaluation** job service consumes `graph.evaluation.queue` via RabbitMQ. It uses the
**Ingestion-Large** model configuration for judge calls (with JSON-mode response parsing).
Tune worker concurrency, shutdown, sampling defaults, and auto-run in **Admin → System Settings**
(JOBS category): `GRAPH_EVALUATION_THREADS` (default `2`),
`GRAPH_EVALUATION_SHUTDOWN_TIMEOUT_SECONDS` (default `600`),
`GRAPH_EVALUATION_SAMPLES_PER_CONTENT_TYPE` (default `3`), and
`GRAPH_EVALUATION_AUTO_RUN_AFTER_JOB` (default `False`). See
[System Settings](/admin-guide/system-settings) for details.
In production (Omnistrate), deploy the `graph-evaluation` service built from
`Dockerfile.graph_evaluation`. For local development:
```bash theme={null}
cd jobs && docker compose up -d graph-evaluation
```
Apply Django migrations if needed:
```bash theme={null}
cd server && pipenv run python manage.py migrate graph_evaluation
```
## Related documentation
* [Jobs & Monitoring](/admin-guide/jobs-monitoring) — job list and detail pages
* [Graph Lineage](/admin-guide/graph-lineage) — provenance records used as judge input
* [Content Types](/admin-guide/content-types) — schemas compared during evaluation
* Architecture plan: `docs/plans/graph-evaluation-architecture.md`
# Graph Lineage
Source: https://docs.experio.cloud/admin-guide/graph-lineage
Audit trail for how knowledge graph entities and relationships were created or changed
## Overview
**Graph lineage** records how every meaningful change to the knowledge graph was made — which document,
data source, enrichment rule, or manual review produced each property update or relationship.
Each change is stored as a `:LineageRecord` node in Neo4j, linked to the affected entity via
`:HAS_LINEAGE`. Lineage is **additive**: legacy `source` lists on nodes continue to work during rollout.
Lineage is **not** included in routine AI context or graph visualizations. It is available on demand in
the graph explorer, provenance panel, and read API.
## What gets recorded
| Source | When | Example |
| ---------------------- | ------------------------------------------------------ | ------------------------------------------ |
| **Document ingestion** | New/updated entities or relationships from a file | Contract attribute extracted from a PDF |
| **Structured data** | Scan order row creates or updates a node/relationship | CRM import updates `annual_revenue` |
| **Enrichment** | An enrichment rule adds or updates graph data | Company `headquarter_region` from LLM rule |
| **Manual review** | A human resolves a match review (merge, confirm, skip) | Reviewer merges duplicate Engagement nodes |
Each record includes:
* **Operation** — `node_created`, `node_updated`, `relationship_created`, etc.
* **Changes** — property-level `{property, old, new}` transitions
* **Source metadata** — path, data source name, rule name, job id
* **Model** — LLM model name when applicable (ingestion, enrichment)
* **Reviewer** — email address for manual match-review actions
* **Timestamp** — when the write occurred
Document-sourced records may link to the `:Document` node via `:DERIVED_FROM` for one-click file access.
## Viewing lineage in the UI
### Graph explorer (provenance)
When exploring entities cited in chat:
1. Open the **provenance graph explorer** for a response
2. Select an entity node
3. Scroll to the **Graph Lineage** section in the inspector
The timeline shows operations newest-first, with source links, model name, reviewer (if manual), and
property changes. Relationship operations show type and from → to endpoints.
### Provenance panel — View lineage
In the provenance entity cards, click **View lineage** to open the graph explorer focused on that
entity's lineage history.
### Chat — asking about origin
Assistants can call the `get_entity_lineage` tool when you ask where a fact came from (e.g. *"How do we
know this company's revenue?"*). Lineage is never injected proactively into every response.
## Enrichment jobs and Postgres audit
Enrichment rules write graph lineage for each successful apply. The Django **EnrichmentResult** row
also stores an optional **`lineage_record_id`** linking to the graph record for admin cross-reference.
See [Enrichment Rules](/admin-guide/enrichment-rules) for running jobs; open a job's results to see
which nodes were updated and the linked lineage id when present.
## Read API
Authenticated REST endpoints (prefix `/api/graph-lineage/`):
| Endpoint | Purpose |
| -------------------------------- | ---------------------------------------------------------------- |
| `GET /nodes/{element_id}/` | Lineage history for an entity (optional `?property=` filter) |
| `GET /records/{lineage_id}/` | Single record with resolved document link |
| `GET /jobs/{job_type}/{job_id}/` | All records for an ingestion file, scan order, or enrichment job |
Relationship lineage appears when querying **either** endpoint node (`from_id` / `to_id` matching).
## Matched document entities
When document ingestion **matches** an existing node and only adds the file to `source` (no new
attributes), a node-level lineage record is **not** created. Relationship lineage from the same
ingest still records links to related entities (e.g. Contract → Company).
## Limitations (v1)
* No automatic backfill for data ingested before lineage shipped
* `relationship_deleted` not recorded (no delete paths wired yet)
* Legacy `client` UI does not include lineage; use the current admin/chat UI (`client-cn`)
* Deprecation of `n.source` in favor of lineage-only provenance is a follow-up ticket
## Related
* [Enrichment Rules](/admin-guide/enrichment-rules) — post-ingestion LLM enrichment
* [Sources & Citations](/user-guide/sources-and-citations) — document citations in chat
* [Matching Strategies](/admin-guide/matching-strategies) — entity matching during ingestion
* [Startup Health](/admin-guide/startup-health) — verify and Force ensure lineage indexes
* Architecture plan: `docs/plans/graph-lineage-architecture.md` (repository)
# Integrations
Source: https://docs.experio.cloud/admin-guide/integrations
Configure per-user OAuth integrations (Google Workspace, Slack) for external tools in chat
## Overview
Integrations let each user connect their own external accounts to Experio. Connected services are
available in **Deep Agent** chat via the MCP retrieval path (`mcp_action`).
As an administrator you configure OAuth credentials once in System Settings. Each user completes
OAuth on the **Integrations** page.
| Integration | Admin configures | User connects |
| ---------------- | -------------------------------- | ------------------------------- |
| Google Workspace | Google Cloud OAuth client + APIs | Gmail, Drive, Calendar, Docs |
| Slack | Slack app OAuth client | Workspace messages and channels |
Organization-wide connectors (e.g. HubSpot) are configured under **Admin > MCP > Organization
integrations**, not on this page.
## Prerequisites
* Admin access to **Admin > Settings > System Settings**
* Provider admin access (Google Cloud Console and/or [Slack API](https://api.slack.com/apps))
* Your Experio backend URL (`BACKEND_URL`), e.g. `https://your-domain` or `http://localhost:8000`
OAuth redirect URLs always follow:
```
{BACKEND_URL}/api/mcp/connections/{provider}/callback
```
Replace `{provider}` with `google` or `slack`. The URL must match exactly (protocol, host, port).
***
## Google Workspace
### 1. Create OAuth credentials
Go to [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials).
**Create Credentials** → **OAuth 2.0 Client ID** → **Web application**.
**Authorized redirect URIs:**
```
https://your-domain/api/mcp/connections/google/callback
http://localhost:8000/api/mcp/connections/google/callback
```
Copy the **Client ID** and **Client Secret**.
Configure the OAuth consent screen first. Add the scopes from step 2 before users connect.
### 2. Enable Google APIs
Enable each API in [APIs & Services > Library](https://console.cloud.google.com/apis/library):
| API | Used for |
| ------------------- | ----------------- |
| Gmail API | Email read/search |
| Google Drive API | Files and search |
| Google Calendar API | Events |
| Google Docs API | Documents |
| Google Sheets API | Spreadsheets |
### 3. Experio settings
| Setting | Category |
| -------------------------- | -------- |
| `GOOGLE_MCP_CLIENT_ID` | AUTH |
| `GOOGLE_MCP_CLIENT_SECRET` | AUTH |
### 4. Test
User: **Integrations** → **Connect** on Google Workspace → chat: *"List my recent emails"*.
***
## Slack
Experio uses Slack **user** OAuth (token type `xoxp`). Add scopes under **User Token Scopes**, not
bot scopes. Posting messages still requires per-action approval in chat.
### 1. Create Slack app
At [api.slack.com/apps](https://api.slack.com/apps), choose **Create New App** → **From scratch**.
Name the app (e.g. Experio) and select a development workspace.
Open **OAuth & Permissions** → **Redirect URLs**. Add:
```
https://your-domain/api/mcp/connections/slack/callback
http://localhost:8000/api/mcp/connections/slack/callback
```
Save URLs.
Under **Scopes** → **User Token Scopes**, add:
| Scope | Purpose |
| ----------------------------------- | ------------------------------------------------ |
| `channels:history`, `channels:read` | Public channels |
| `groups:history`, `groups:read` | Private channels |
| `im:history`, `im:read` | DMs |
| `mpim:history`, `mpim:read` | Group DMs |
| `users:read`, `users:read.email` | User identity |
| `search:read` | Message search |
| `chat:write` | Post messages (optional; requires chat approval) |
Omit `chat:write` for read-only search and history.
**Settings** → **Basic Information** → **App Credentials**. Copy **Client ID** and
**Client Secret**.
### 2. Experio settings
| Setting | Category |
| ------------------------- | -------- |
| `SLACK_MCP_CLIENT_ID` | AUTH |
| `SLACK_MCP_CLIENT_SECRET` | AUTH |
On new deployments, run `npm run config:seed` so these keys appear in System Settings. Ensure the
**Slack** MCP server is enabled in admin (seeded by migration).
### 3. Test
User: **Integrations** → **Connect** on Slack → chat: *"Search Slack for messages about launch"*.
***
## How it works
1. User starts OAuth from **Integrations**; Experio stores encrypted tokens on `UserMCPConnection`.
2. The Deep Agent router sends external-service questions to `mcp_action` → MCP retrieval.
3. MCP tools run with that user's credentials (Google session home directory or Slack `xoxp` token).
4. Write tools (e.g. Slack post) trigger an in-chat approval step before execution.
If the user has not connected a service, the agent directs them to **Integrations**.
***
## Troubleshooting
### Google
| Issue | Resolution |
| --------------------------- | --------------------------------------------------------- |
| OAuth fails immediately | Redirect URI must match exactly in Google Cloud |
| Missing `client_id` | Set `GOOGLE_MCP_CLIENT_ID` in System Settings |
| Permission denied on a tool | Enable the matching Google API; user reconnects |
| Agent ignores Google data | Use **Deep Agent**; start a **new chat** after connecting |
### Slack
| Issue | Resolution |
| -------------------------------- | ---------------------------------------------------------- |
| `redirect_uri did not match` | Add the exact callback URL under Slack **Redirect URLs** |
| `invalid_scope` | Add the scope under **User Token Scopes**; user reconnects |
| Connect works but no Slack tools | Enable Slack MCP server; confirm Pass 3 migration applied |
| Search works, post fails | Add `chat:write` in Slack app; user reconnects |
# Jobs & Monitoring
Source: https://docs.experio.cloud/admin-guide/jobs-monitoring
Monitor ingestion jobs, flow executions, system logs, and service scaling
## Jobs
Navigate to **Admin > Monitoring > Jobs** to monitor document processing and ingestion jobs.
### Enrichment Jobs
Enrichment jobs (post-processing rules that enrich the knowledge graph) are managed separately. Navigate to **Admin > Graph > Rules** and use the **Executions** tab to view all enrichment jobs, or open a specific rule and go to its **Jobs** tab. Enrichment jobs support cancel and resume.
### Job List
The jobs page displays a table of all jobs with:
* Job type and name
* Status (running, completed, failed)
* Start and end times
* Progress information
### Starting Jobs
Click **Start New Job** to manually trigger a processing job. This is useful for:
* Initial document ingestion after setup
* Re-processing after configuration changes
* Running one-off processing tasks
### Job Details
Click any job to view detailed information:
* Execution timeline
* Files processed and their status
* Error logs for failed files
* Processing metrics and statistics
* **Graph evaluation** (unstructured full-ingestion jobs) — see [Graph Evaluation](/admin-guide/graph-evaluation)
### Parse-Only Jobs
Data sources configured with **Ingestion Type: Parse only** run a shortened pipeline that stops after parsing. The job detail page surfaces parse-only mode with several visual affordances:
* **Parse Only badge** — The job header shows an amber "Parse Only" badge next to the scan type. Full ingestion jobs show an indigo "Full Ingestion" badge.
* **Dimmed phases** — The Processing Phases pipeline still renders all six stages, but Classify and Ingest appear dimmed with a "Parse-only mode" caption and a dashed border to indicate they are skipped. A section-level "Parse-only pipeline" badge is shown in the top-right of the card.
* **Parsed status** — Files that complete successfully show a green **Parsed** badge (terminal status `parsed_only`). This is the equivalent of **Ingested** for full-mode jobs.
* **Context-aware file status filter** — The status filter dropdown on the Scanned Files table hides statuses that cannot apply in parse-only mode (Pending Classification, Classifying, Classification Failed, Pending Ingestion, Ingesting, Ingestion Failed, Ingested). All failure variants are consolidated into a single **Failed** option.
## Flow Executions
Navigate to **Admin > Monitoring > Flow Executions** to see the execution history of all automated flows.
The executions page shows:
* Flow name
* Execution status
* Start and end timestamps
* Duration
* Error information (if applicable)
Click any execution to see step-by-step details.
## System Logs
Navigate to **Admin > Monitoring > Logs** to view system-level logs.
### Filtering Logs
| Filter | Description |
| ------------- | ------------------------------------------------ |
| **Level** | Filter by log level: DEBUG, INFO, WARNING, ERROR |
| **Search** | Full-text search across log messages |
| **Timestamp** | Filter logs by date and time range |
### Log Entries
Each log entry shows:
* Timestamp
* Log level (color-coded)
* Message content
* Source module
Logs stream in real-time, so you can monitor system activity as it happens.
## Scan Order Statistics
Access scan order statistics from the admin dashboard to review ingestion metrics:
* Number of files discovered per data source
* Files successfully processed
* Files pending processing
* Files that failed processing with error details
* Processing throughput metrics
## Service Scaling
Navigate to **Admin > Monitoring > Scaling** to monitor and manually scale platform services. The dashboard shows each service's health, current and maximum replicas, and the ingestion pipeline flow. Core infrastructure services are always on and cannot be scaled manually.
See [Service Scaling](/admin-guide/service-scaling) for the full dashboard layout, per-service settings, force-scaling (single and batch), and the scaling event audit trail.
## Startup Health
Navigate to **Admin > Monitoring > Startup Health** to verify app-level migrations, seeds,
indexes, and cache readiness (including lineage indexes). This is separate from replica scaling.
Reading the page requires Monitoring **read** access. Verify and Ensure require Monitoring **write**
access. **Force ensure** on lineage indexes must only run in a maintenance window — creating indexes
on a populated FalkorDB graph can stall the database.
See [Startup Health](/admin-guide/startup-health) for Verify vs Ensure, status meanings, and deploy notes.
Monitor jobs and logs regularly during initial ingestion to catch configuration issues early. Pay attention to files with processing errors — they may indicate content type configuration problems or connector access issues.
# Matching Strategies
Source: https://docs.experio.cloud/admin-guide/matching-strategies
Configure how Experio identifies and deduplicates entities
## Overview
Matching strategies control how Experio identifies duplicate or related entities across different data sources. When the same person, project, or organization appears in multiple documents, matching strategies determine whether they should be merged into a single entity or kept separate.
Navigate to **Admin > Graph > Matching Strategies**.
## Strategy Types
### Default Strategy
The default strategy applies to all entity types unless overridden. It defines the baseline matching behavior for your entire knowledge graph.
### Entity-Specific Strategies
Create custom strategies for specific entity types that need different matching rules. For example, person names may need fuzzy matching, while project codes need exact matching.
## Configuration Sections
Each matching strategy has four configuration areas:
### 1. Matching Methods
Enable and weight different matching algorithms:
| Method | Description |
| --------------------- | ---------------------------------------------------------- |
| **Exact** | Case-insensitive exact string match |
| **Vector Similarity** | Semantic similarity using vector embeddings |
| **Fuzzy** | Approximate string matching (handles typos, abbreviations) |
| **Phonetic** | Sound-based matching (handles spelling variations) |
| **Synonym** | Matches using defined synonym lists |
Each enabled method has a **weight slider** (0 to 1) that controls its relative importance in the overall match score.
### 2. Filters
Apply additional constraints to matching:
**Temporal Filter:**
* **Max Distance (Days)** — Maximum time difference between entities (default: 1825 / 5 years)
* **Penalty Per Year** — Score reduction per year of difference (default: 0.1)
* **Date Attribute Names** — Which entity attributes contain dates
**Relationship Filter:**
* **Parent Entity Types** — Limit matching to entities with specific parent types
* **Relationship Types** — Consider only entities connected by specific relationships
### 3. Normalization
Preprocessing steps applied before matching:
| Option | Description |
| ----------------------------- | ----------------------------------------- |
| **Unicode Normalization** | Standardize unicode characters |
| **Remove Prefixes** | Strip "The", "A", "An" from entity names |
| **Remove Company Suffixes** | Strip "Inc", "LLC", "Corp", etc. |
| **Expand Abbreviations** | Expand common abbreviations to full forms |
| **Punctuation Normalization** | Standardize punctuation |
### 4. Thresholds
Define confidence levels that determine how matches are handled:
| Threshold | Default | Behavior |
| ---------------------- | ------- | --------------------------------------------------- |
| **Auto-Match** | 0.9 | Matches above this score are merged automatically |
| **LLM Disambiguation** | 0.7 | Matches in this range are sent to the AI for review |
| **Human Review** | 0.5 | Matches in this range are queued for manual review |
Matches below the Human Review threshold are treated as distinct entities.
## Creating Entity-Specific Strategies
1. Click **Create New Strategy**
2. Select the entity type this strategy applies to
3. Configure matching methods, filters, normalization, and thresholds
4. Save the strategy
The entity-specific strategy overrides the default for that entity type only.
Each strategy shows a **compatibility status**. Entity-specific strategies become **invalid** if their
entity type is removed from the ontology. The global default strategy (applied when no specific
strategy exists) is not treated as a missing entity type. See
[Ontology Compatibility](/admin-guide/ontology-compatibility).
## Resetting to Defaults
Each strategy section can be reset to default values using the **Reset** option. This is useful if experimental changes produce poor results.
# MCP Servers
Source: https://docs.experio.cloud/admin-guide/mcp-servers
Configure Model Context Protocol server integrations
## Overview
MCP (Model Context Protocol) servers extend the capabilities of AI assistants by providing additional tools, data sources, and context. Configure external MCP server connections to enhance what your assistants can do.
Navigate to **Admin > Settings > MCP Servers**.
## Viewing MCP Servers
The MCP servers page lists all configured servers with:
* Server name
* URL
* Type/Protocol
* Connection status
* Enable/disable toggle
* Created and modified dates
## Creating an MCP Server
Click **Create New** to add a server:
| Field | Description |
| ------------------------- | --------------------------------------------- |
| **Name** | A descriptive name for the server |
| **URL** | The server endpoint URL |
| **Type** | The protocol/transport type |
| **Authentication** | Credentials for connecting to the server |
| **Environment Variables** | Additional configuration passed to the server |
| **Enabled** | Whether the server is active |
## Managing MCP Servers
### Quick Toggle
Use the enable/disable toggle directly on the server list to quickly activate or deactivate a server without opening its detail page.
### Editing
Click any server to view and edit its full configuration.
### Health Checks
The system periodically checks MCP server connectivity and displays the current status. Servers that fail health checks show an error status.
### Deleting
Remove an MCP server configuration permanently. Assistants using tools from this server will lose access to those tools.
## Audit tab
The MCP Servers page includes an **Audit** tab with:
* **Write approvals** — user approve/decline decisions for MCP write tools, with action summaries
* **Tool usage** — MCP tool invocations (integration, tool name, success, duration)
Use this for compliance review and debugging connector issues.
Organization-wide credentials are configured under **Admin > MCP > Organization integrations**.
User OAuth connections (Google, Slack) are managed by each user on `/integrations`.
MCP servers extend assistant capabilities. Consult your organization's AI engineering team before adding or modifying MCP server configurations.
# Model Configurations
Source: https://docs.experio.cloud/admin-guide/model-configurations
Configure and manage AI language models
## Overview
Model configurations define which AI language models are available in your Experio deployment. You can configure multiple models from different providers and control which ones are available to end users.
Navigate to **Admin > Settings > Model Configurations**.
## Viewing Models
The model configurations page lists all configured models with:
* Name (internal identifier)
* Display Name (shown to users)
* Type (LLM, Embedding, Reranking)
* Provider
* Active status
* User-Facing status (whether end users can select this model)
## Creating a Model Configuration
Click **Create New** and fill in:
| Field | Description |
| --------------------- | --------------------------------------------------------------------------------------------------- |
| **Name** | Internal identifier (no spaces, used in API calls) |
| **Display Name** | Human-readable name shown in the UI |
| **Type** | The model type: LLM, Embedding, or Reranking |
| **Provider** | The model provider (OpenAI, Anthropic, etc.) |
| **API Configuration** | JSON configuration with API keys, endpoints, model parameters, and other provider-specific settings |
| **Active** | Whether this model is available for use |
| **User-Facing** | Whether end users can select this model in the chat interface |
## Model Types
| Type | Purpose |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| **LLM** | Language model for generating chat responses |
| **Embedding** | Model for creating vector embeddings of documents (used in semantic search) |
| **Reranking** | Model for re-ranking search results by relevance |
| **Classification** | Document classification during ingestion |
| **Ingestion - Large** | Primary entity extraction for large or complex documents |
| **Ingestion - Medium** | Optional mid-tier model for primary extraction when set on a content type |
| **Ingestion - Small** | Lightweight model for secondary ingestion steps (validation, JSON repair, disambiguation) |
Assign ingestion models in [System Settings](/admin-guide/system-settings):
* `CLASSIFICATION_MODEL_CONFIG`
* `INGESTION_LARGE_MODEL_CONFIG`
* `INGESTION_MEDIUM_MODEL_CONFIG` (optional; used when a content type sets `model_tier: medium`)
* `INGESTION_SMALL_MODEL_CONFIG`
Content types can override the primary tier per type. See [Extraction Policy](/admin-guide/extraction-policy).
## Testing Connections
Before activating a model, test its connection:
1. Click **Test Connection** on any model
2. The system sends a real API request to the provider
3. Results show:
* Response time
* Response preview
* Embedding dimensions (for embedding models)
* Success or error status
Always test a model connection after creation or after changing API configuration. This validates credentials, endpoints, and model availability before end users encounter issues.
## Managing Models
### Editing
Click any model to edit its configuration. Changes to API configuration or active status take effect immediately.
### Activating/Deactivating
Toggle the **Active** status to enable or disable a model. Deactivated models are not available for any operation.
Toggle the **User-Facing** status to control whether end users see the model in the model selector dropdown. Non-user-facing models can still be used by assistants as their default model.
### Deleting
Remove a model configuration permanently. Ensure no assistants are configured to use the model before deleting.
# Onboarding Checklist
Source: https://docs.experio.cloud/admin-guide/onboarding-checklist
Step-by-step guide to setting up a new Experio deployment
## Overview
The admin dashboard presents a guided onboarding checklist when setting up a new Experio deployment. Each step builds on the previous one to create a fully configured knowledge management system.
## Setup Steps
Follow these steps in order for a complete deployment:
Define your organization's identity — name, synonyms, branding, and logo. This information is used throughout the knowledge graph and the user interface.
[Configure your organization](/admin-guide/client-configuration)
Define the entity types and relationships that form the structure of your knowledge graph. For example: People, Projects, Clients, Skills, and how they relate to each other.
[Set up ontology](/admin-guide/ontology)
Map fields from your external data sources to the entities and relationships defined in your ontology. This tells Experio how to interpret your data.
[Configure data mapping](/admin-guide/data-mapping)
Create hierarchical classification systems for categorizing entities. Taxonomies provide structured vocabularies for consistent tagging.
[Add taxonomies](/admin-guide/taxonomies)
Define the types of documents your system will process and how they should be classified and extracted.
[Set up content types](/admin-guide/content-types)
Configure how Experio identifies and deduplicates entities across different sources. Set thresholds for automatic matching, AI disambiguation, and human review.
[Configure matching](/admin-guide/matching-strategies)
Authorize connections to your cloud storage providers (Box, Google Drive, SharePoint) using OAuth.
[Set up connectors](/admin-guide/connectors)
Create data source configurations that specify which folders to scan and how to process files from your connected providers.
[Configure data sources](/admin-guide/data-sources)
## After Setup
Once the initial configuration is complete, ongoing operations include:
* **Setup health** — The admin home shows a green / amber / red strip for ontology compatibility.
Red means invalid configs are blocking ingestion. See
[Ontology Compatibility](/admin-guide/ontology-compatibility).
* **Monitoring ingestion** — Review scan order statistics and processing results at [Jobs & Monitoring](/admin-guide/jobs-monitoring).
* **Startup health** — After first deploy or a graph rebuild, verify seeds and indexes at [Startup Health](/admin-guide/startup-health).
* **Resolving conflicts** — Review and resolve low-confidence classifications at [Conflict Resolution](/admin-guide/conflict-resolution).
* **Managing users** — Configure SSO and manage user access at [SSO Configuration](/admin-guide/sso-configuration).
* **Personas (optional)** — Configure audience profiles and feature flags at [Personas](/admin-guide/personas) when you want personalized gating and identity injection in chat.
# Ontology
Source: https://docs.experio.cloud/admin-guide/ontology
Define the entity types and relationships that structure your knowledge graph
## Overview
The ontology defines the schema of your knowledge graph — the types of entities (nodes) and relationships (edges) that Experio uses to organize extracted knowledge. A well-designed ontology ensures that information from different sources is connected meaningfully.
At runtime the graph may be backed by **Neo4j** or **FalkorDB** depending on **`GRAPH_PROVIDER`** in System Settings.
The ontology editor and schema semantics are the same; provider-specific behavior (for example certain analytics queries)
is summarized in [Graph backend (Neo4j & FalkorDB)](/admin-guide/graph-backend).
Navigate to **Admin > Graph > Ontology**.
## Visual Editor
The ontology is managed through an interactive visual editor:
* **Entity type nodes** are displayed as draggable boxes on a canvas
* **Relationship edges** connect entity types with labeled arrows
* **Zoom and pan** to navigate the schema
* **Drag nodes** to arrange the layout
* **Hide nodes** from the canvas with the eye icon on the left list (session-only; not saved)
### Filter by content type
The toolbar includes a **content type** dropdown. Use it to see which parts of the ontology a given
content type actually extracts, without changing the saved schema.
1. Leave **All content types** selected to see the full graph.
2. Choose a content type. Nodes and relationships that are not in that type are hidden from the
canvas. This uses the same hide behavior as the eye icon on each node in the left list.
3. Click a remaining node and open **Attributes**. Attributes defined on the ontology but not
selected on that content type are greyed out. They stay editable.
4. Choose **All content types** again to restore the full graph.
The **JSON** view always shows the complete schema. Hiding a node with the eye icon is independent of
the dropdown: clearing the filter does not un-hide nodes you hid by hand. The filter does not mark
the ontology as unsaved.
See [Content Types](/admin-guide/content-types) for how a type selects a subset of this schema.
### Permissions
* **Read-only users** can view the ontology but cannot make changes
* **Write users** can add, edit, and remove entity types and relationships
## Entity Types
Entity types represent the categories of things in your knowledge graph. Common examples:
| Entity Type | Description |
| ---------------- | --------------------------------------------------- |
| **Person** | Individuals in your organization or client contacts |
| **Project** | Consulting engagements or internal projects |
| **Client** | Organizations your firm serves |
| **Skill** | Competencies and areas of expertise |
| **Document** | Processed files and their metadata |
| **Organization** | Companies, agencies, or institutions |
Each entity type has:
* **Name** — A unique identifier for the type
* **Properties** — Attributes that instances of this type can have (e.g., Person has "name", "email", "title")
## Relationships
Relationships define how entity types connect to each other. Examples:
| Relationship | From | To | Description |
| --------------- | ------- | -------- | ---------------------------------- |
| **WORKS\_ON** | Person | Project | A person is assigned to a project |
| **HAS\_SKILL** | Person | Skill | A person possesses a skill |
| **MANAGED\_BY** | Project | Client | A project is for a specific client |
| **AUTHORED** | Person | Document | A person created a document |
## Saving Changes
After modifying the ontology:
1. Click **Save**. If the change **renames** or **deletes** schema elements (or otherwise breaks
dependents), a confirmation modal lists the impact before anything is published.
2. Confirm to persist the schema. Experio creates a new **ontology revision**, auto-updates matching
names in content types, mappings, and related config, and sets those configs to **stale** or
**invalid** as needed.
3. The canvas layout (node positions) is saved with the schema.
Deleting an entity type, attribute, or relationship does **not** silently rewrite or wipe downstream
configuration. Affected content types, mappings, enrichment rules, and matching strategies keep their
references and become **invalid**. Ingestion that uses those configs is **blocked** until you fix them
and re-validate. See [Ontology Compatibility](/admin-guide/ontology-compatibility).
Existing nodes and edges already in the knowledge graph are not deleted.
Renames are applied automatically (for example, an entity type named `Employee` that you rename to
`Person` updates matching names in configs). Those configs become **stale** so you can review them.
Stale status warns in the admin UI; it does not block scans.
## Revision history and rollback
Open **History** on the ontology editor, or go to **Admin > Graph > Ontology > Revision history**.
The revisions list shows each published schema (newest first), who saved it, and a short change
summary (additions, renames, breaking deletes). Select a revision to see a human-readable diff and
the schema JSON.
### Rollback
Write-access admins can restore a **previous** revision:
1. Select the revision to restore (not the current one).
2. Review the impact preview — the same confirmation used when saving.
3. Confirm. Experio sets the live schema to that snapshot and publishes a **new** revision that
records which revision you rolled back from.
Rollback does **not** rewrite content types or mappings to “fix” them. The compatibility engine
re-checks every dependent config against the restored schema. You may still need to re-validate or
edit configs afterward.
The **Audit** tab on the history page logs revision publishes, rollbacks, and config re-validations.
## Deleting an ontology
You cannot delete the **default** ontology. You also cannot delete an ontology that still has
[data mappings](/admin-guide/data-mapping) attached — remove or reassign those mappings first.
## Related: Inference Rules
After ingestion populates the graph, you can use [Enrichment Rules](/admin-guide/enrichment-rules) to enrich it further. Enrichment rules process existing nodes and create new attributes, nodes, or relationships — all defined using the entity types and relationships in your ontology.
## Best Practices
* Start with a small, focused ontology and expand as needed
* Use clear, descriptive names for entity types
* Define relationships that reflect real-world connections in your organization
* Review the ontology periodically as your data sources grow
* After each save, check [Ontology Compatibility](/admin-guide/ontology-compatibility) and fix
invalid configs before the next scan
# Ontology Compatibility
Source: https://docs.experio.cloud/admin-guide/ontology-compatibility
Keep artifact types, mappings, enrichment rules, and matching strategies in sync with ontology revisions
## Overview
Ontology changes are versioned. Each time you save a schema change, Experio publishes a new **revision**
and checks every configuration that depends on that schema:
* Content types (Artifact Types)
* Data mappings
* Enrichment rules
* Matching strategies
Configs that no longer match the ontology are flagged. **Invalid** configs **block ingestion** until you
fix them. **Stale** configs warn only — scans still run.
Navigate to **Admin > Graph > Compatibility**, or open **History** from the [Ontology](/admin-guide/ontology)
editor.
## Statuses
| Status | Meaning | Ingestion |
| ------------------ | -------------------------------------------------------------------------------- | ---------------------- |
| **Valid** | Checked against the current ontology revision | Allowed |
| **Stale** | Names were auto-updated after an ontology rename; review the config | Allowed (warning only) |
| **Pending review** | Needs an admin to confirm the config still looks right | Allowed (warning only) |
| **Invalid** | References an entity, attribute, or relationship that is missing or incompatible | **Blocked** |
Invalid examples include a deleted ontology attribute that a content type still extracts, or an
enrichment rule whose output field is no longer on the target entity.
## Compatibility dashboard
The dashboard summarizes counts for valid, stale, invalid, and pending-review configs. Filter the
table to **Needs attention**, **Invalid**, **Stale**, or **Review**, then open a row to edit that
configuration.
Use **Revision history** on the dashboard (or **History** on the ontology editor) to inspect past
schema publishes. See [Ontology](/admin-guide/ontology#revision-history-and-rollback).
## Fixing an invalid or stale config
On the edit screen for a content type, mapping, enrichment rule, or matching strategy:
1. Read the **compatibility issues** listed on the page (each issue names the missing or renamed
ontology element).
2. Update the configuration so it matches the current ontology — or restore the ontology element if
the deletion was a mistake (see rollback on the ontology page).
3. Click **Re-validate** to re-check against the current revision.
4. For **stale** or **pending review** after a rename that already looks correct, click
**Mark as reviewed** to return the status to **valid** without editing the schema.
## Blocked scans
Starting a job (scan, file upload, classification, or enrichment run) fails immediately when a linked
config is **invalid**. The error lists the blocking configs with links to their admin pages. No
RabbitMQ message is published.
The **Data Sources** list shows a computed compatibility column from linked content types and
mappings, so you can see a problem before you click Start Job.
Stale configs do **not** block ingestion. Invalid configs do. Fix invalid items before you rely on
scheduled or OAuth-triggered scans.
## Setup health
The admin home (**Admin**) shows a setup-health strip:
* **Green** — all tracked configs are valid
* **Amber** — some configs are stale or pending review (ingestion still runs)
* **Red** — at least one config is invalid (ingestion is blocked for linked sources)
The strip links to this compatibility dashboard.
## Related pages
* [Ontology](/admin-guide/ontology) — save impact, history, and rollback
* [Content Types](/admin-guide/content-types)
* [Data Mapping](/admin-guide/data-mapping)
* [Enrichment Rules](/admin-guide/enrichment-rules)
* [Matching Strategies](/admin-guide/matching-strategies)
* [Data Sources](/admin-guide/data-sources)
# Admin Overview
Source: https://docs.experio.cloud/admin-guide/overview
Getting started with Experio administration
## Admin Panel
The Experio admin panel is available at `/admin` for users with administrator privileges. It provides a centralized interface for configuring data sources, managing the knowledge graph, monitoring system health, and controlling user access.
## Admin Navigation
The admin sidebar is organized into five sections:
Connect cloud storage, configure data ingestion, set up content types, and manage automated flows.
Configure the ontology, taxonomies, matching strategies, and client branding. Review
[ontology compatibility](/admin-guide/ontology-compatibility) after schema changes.
Track jobs, view execution history, inspect system logs, and check startup health.
Manage SSO configuration and user access.
Configure AI models, MCP servers, AI instructions, document templates, personas, and system settings.
See [Document Templates](/admin-guide/document-templates) for PPTX template management and
[Personas](/admin-guide/personas) for audience profiles and login gating.
## Permissions
Admin access is controlled through a role-based permission system:
| Permission Group | Controls |
| ---------------- | ------------------------------------------------------------------------------ |
| **Data Source** | Connectors, data sources, mappings, content types, flows |
| **Graph** | Ontology, compatibility, taxonomies, matching strategies, client configuration |
| **Monitoring** | Jobs, flow executions, system logs, service scaling, startup health |
| **Users** | SSO clients, user management |
Each permission group has two levels:
* **Read** — View configurations and data (required to see the section)
* **Write** — Create, edit, and delete (required to make changes)
## Onboarding Checklist
When you first access the admin panel, you'll see a guided **onboarding checklist** that walks through the initial setup steps in order. See [Onboarding Checklist](/admin-guide/onboarding-checklist) for details.
The admin home also shows **setup health** for ontology compatibility: green when every dependent
config is valid, amber when some are stale, and red when invalid configs are blocking ingestion.
Open [Ontology Compatibility](/admin-guide/ontology-compatibility) from the strip or from
**Admin > Graph > Compatibility**.
## Admin Help
Every admin page includes a **Help** button in the top bar. It opens a chat panel backed by this admin guide so you can ask questions about the page you are on.
* Answers include links to the relevant Mintlify documentation pages.
* Suggested starter questions adapt to the sidebar section you are in (Connect, Process, AI & Agents, and so on).
* If documentation on disk has changed since the last index, the panel shows a **Re-index docs** banner (admin write access required). Re-indexing is incremental — only changed pages are re-processed. Use it after pulling doc updates, or run `python manage.py index_admin_docs` from the server directory (add `--full` to rebuild every page).
* Configure the chat model under **System Settings → LLM** with `ADMIN_HELP_MODEL_CONFIG` (falls back to the reasoning model when unset).
## Next Steps
Follow the step-by-step setup process for a new deployment.
Connect your first cloud storage provider.
Set up Google Workspace integration for per-user access to Gmail, Drive, Calendar, and Docs.
# Personas
Source: https://docs.experio.cloud/admin-guide/personas
Configure audience personas for personalized chat and optional login gating
## Overview
Personas describe **who is asking** — for example "CEO of a small enterprise" or "Freelancer".
They are separate from [Assistants](/admin-guide/assistants), which define **which agent** runs the
conversation. A user picks a persona once (or per conversation) so the report writer receives
name, email, profile free text, and optional structured gating answers.
Navigate to **Admin > Settings > Personas**.
Personas are **off by default**. Turn them on under [System Settings](/admin-guide/system-settings)
before end users see any persona UI. Admin CRUD remains available while the feature is off so you
can seed personas first.
## Feature flags
Two settings in **Admin > Settings > System Settings > CORE** control rollout:
| Setting | Default | Effect |
| ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PERSONAS_ENABLED` | `False` | **Master switch.** Off hides all end-user persona UI, skips persona graph steps, and blocks persona REST updates. Admin Personas CRUD still works. |
| `REQUIRED_PERSONAS` | `False` | When master is on, show a **login gate** until the user selects a persona. Ignored when master is off. |
If a deployment already has `REQUIRED_PERSONAS=True` from an earlier config, you must also set
`PERSONAS_ENABLED=True` or the feature stays dark.
### Behavior matrix
| | Master off | Master on, required off | Master on, required on |
| -------------------------------------------------- | :--------: | :---------------------------------: | :-----------------------: |
| Login persona gate | No | No | Yes (if no persona saved) |
| Profile picker & composer toggle | No | Yes | Yes |
| Identity in report writer (name, email, free text) | No | Yes | Yes |
| Persona gating graph & Cypher steering | No | Only when conversation toggle is on | Same |
When the per-conversation **Use persona** toggle is off, identity still injects but persona gating
nodes and the persona memory blob do not run.
## Permissions
Persona admin routes require **AI Managers** (`access_ai_admin`) write access for create, update,
delete, and gating-form generation. Any authenticated user may call **`GET /api/personas/choices/`**
when `PERSONAS_ENABLED` is on — the response uses a slim serializer (no operator-only fields).
## Creating a persona
| Field | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| **Name** | Display name shown on selection cards and the header badge |
| **Slug** | Stable identifier for URLs and idempotent seeding |
| **Description** | Short summary on the selection card |
| **Gating prompt** | Bullet list of information to collect; used to **Generate form** |
| **Gating questions** | Structured JSON form (`GatingFormSchema`); usually produced by generation, editable in JSON mode |
| **Free text prompt** | Label for the optional open-ended box (e.g. "Anything else about you?") |
| **Graph node name** | Name of the matching Persona node in the knowledge graph; required before Cypher steering applies |
| **Cypher instructions** | Optional hand-written hints appended to generated Cypher steering |
| **Order** | Sort order in the user picker (lower first) |
| **Active** | Whether the persona appears in `/personas/choices/` |
### Gating form workflow
1. Write a **Gating prompt** describing what to ask (checklist style works well).
2. Click **Generate form** — an LLM builds structured `gating_questions`.
3. Preview the form in the drawer before saving.
4. Regenerate after changing the prompt; a stale-form warning appears if the prompt drifted during generation.
A persona can be **active with an empty gating form** (`{}`) if you never generate one or the gating
prompt is blank (generation returns 400). Users can still select it; they only see the free-text field
and any empty question list. Generate the form before requiring personas at login.
### Graph steering
When **`graph_node_name`** is set and the conversation has persona pipeline enabled, Cypher
generation receives steering text for that node. If **`graph_node_name`** is blank, no steering
snippet is injected — even if **`cypher_instructions`** is set. Set the graph node name only after
matching Persona nodes exist in your graph ontology.
## Deployment checklist
Apply Django migrations so `PERSONAS_ENABLED` and `REQUIRED_PERSONAS` exist in the config table
(`common.0020_enable_persona_dashboard_settings`).
Run `npm run config:seed` (or add both keys in Django admin) so the flags appear in System Settings.
Add at least one **active** persona and generate gating forms where needed.
Set `PERSONAS_ENABLED=True` in **System Settings > CORE**.
Set `REQUIRED_PERSONAS=True` only after personas exist and forms are ready.
## Related documentation
* [System Settings — CORE flags](/admin-guide/system-settings#core-settings)
* [Settings & Preferences — user profile persona](/user-guide/settings-and-preferences#persona-profile)
* [Assistants — assistant-level gating](/admin-guide/assistants#gating) (separate from persona gating)
# Service Scaling
Source: https://docs.experio.cloud/admin-guide/service-scaling
Monitor autoscaling and manually force-scale ingestion pipeline services
## Overview
Navigate to **Admin > Monitoring > Scaling** to monitor and manually scale the platform's document processing services. The dashboard shows each service's health, current and maximum replicas, and how they fit into the ingestion pipeline.
Reading the dashboard requires Monitoring **read** access. Changing settings or force-scaling a service requires Monitoring **write** access.
## Dashboard
The **Dashboard** tab groups services into three sections:
### Ingestion Pipeline
The core document processing chain — **Reader → Downloader → Parser → Classifier → Ingestion** — rendered as a left-to-right flow. An arrow between two stages turns green when both stages are healthy; a queued-message count appears under the arrow when the downstream stage has backlog waiting to be picked up.
Pipeline satellites (services that support the pipeline but aren't sequential stages — coordinator, MinIO, the Kreuzberg text extractor, enrichment, structured data, cleanup) appear as a row of cards underneath the flow.
### Core Infrastructure (Always On)
Services that cannot be scaled manually: always-on infrastructure with no scaling configuration (server, PostgreSQL, Redis, RabbitMQ) and platform-safety-locked services (Neo4j, FalkorDB). These render with a lock icon and no selection checkbox.
### Other Services
Any remaining scalable service that isn't part of the pipeline or its satellites.
### Health indicators
Each service card shows a colored dot summarizing its status:
| Color | Status | Meaning |
| ------------------- | ------------------------- | ---------------------------------------------------------------------- |
| 🟢 Green | Running / Idle | Service is up (idle is a normal resting state, not a problem) |
| 🟡 Yellow (pulsing) | Scaling up / Scaling down | A scaling operation is in flight |
| ⚪ Gray | Stopped | Scaled to zero — the expected resting state for most pipeline services |
| 🔴 Red | Error | The autoscaler recorded consecutive failures for this service |
## Service Detail Page
Click any scalable service's card to open its detail page, which shows:
* **Current State** — status, current replicas, scaling mode, and configured min/max bounds.
* **Settings** tab — edit the service's autoscaling configuration (requires write access).
* **Events** tab — the scaling event history for just this service.
### Settings
Basic settings:
| Setting | Description |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| **Autoscaling enabled** | Whether the autoscaler evaluates this service at all |
| **Scaling mode** | `Auto` (strategy-based), `Keep Alive` (always ≥1 replica), or `Keep Down` (always 0 replicas) |
| **Min / Max replicas** | Bounds the autoscaler and force-scale requests must stay within |
| **Cooldown (seconds)** | Minimum time between successive scaling actions |
Advanced settings (collapsed by default, with a warning that misconfiguration can break the pipeline dependency chain):
* **Capacity step size** and **messages per replica** — control how aggressively the queue-based strategy scales.
* **Queue names** — which RabbitMQ queues this service's strategy watches.
* **Schedule times** / **window duration** — for schedule-based strategies.
* **Warm-keeping enabled** / **warm-keeping policies** — keep a service warm during specific in-progress operations (scan orders, structured data scan orders, enrichment jobs, dependency chains) instead of scaling it to zero.
* **Dependency order** — this service's position in the pipeline dependency chain.
The **Strategy** shown at the top of the panel (queue, reader, schedule, manual, phoenix, propelauth) is display-only and cannot be changed from the UI.
## Force Scaling
Manually scaling a service switches its mode to **Keep Alive** (or **Keep Down** when scaled to zero) so the autoscaler doesn't immediately revert the change on its next cycle. Reset the scaling mode from the service's Settings tab to return it to automatic scaling.
### Single Service
From a service's detail page, click **Force Scale** (hidden for core services) to open a dialog where you set a target replica count within the service's configured bounds.
Scaling a service to zero opens a confirmation dialog instead: you must type the service name to confirm, and scaling a stateful service (such as MinIO) to zero additionally requires acknowledging that its persistent volume data will be permanently deleted.
### Multiple Services (Batch)
Services can be scaled together in a single operation:
1. Select services using the checkboxes on their cards (core services are not selectable).
2. Click **Force scale selected** in the actions bar that appears.
3. Set a target replica count per service — the dialog previews whether each service will scale up, down, or stay unchanged. A reason is required for the audit trail.
4. Confirm to apply all changes as one batch.
Results are reported per service; services that fail to scale stay selected so you can retry them. Each batch is assigned a correlation ID shown with the results, which links the individual scaling events in the **All Events** tab for auditing.
Batch scaling requests are rate-limited to a few per minute — scaling actions take about a minute to materialize (pods must start), so rapid resubmission has no effect.
### Deferred Scaling Conflicts
The platform serializes scaling operations per instance: only one resource can converge at a time. If you force-scale a service while another resource's scaling operation is still in progress, the request is deferred rather than treated as a failure — you'll see "A previous scaling operation is still in progress. Please try again in a few seconds." in muted text rather than a red error, and no failed event is recorded. Wait a few seconds and retry.
## Scaling Events
The **All Events** tab (global) and each service's **Events** tab (per-service) show the scaling audit trail: timestamp, action (scale up/down, manual scale up/down/to-zero, or failed), replica change, who triggered it (or "Autoscaler" for automatic actions), and the reason given.
Use the correlation ID from a batch force-scale result to find every event that batch produced across services.
# SSO Configuration
Source: https://docs.experio.cloud/admin-guide/sso-configuration
Set up enterprise Single Sign-On with PropelAuth BYO and identity providers like Microsoft Entra ID, Okta, and Generic OIDC
## Overview
Experio uses [PropelAuth BYO](https://docs.byo.propelauth.com/) (a self-hosted sidecar) for enterprise Single Sign-On. It supports OIDC-based authentication with Microsoft Entra ID, Okta, and any Generic OIDC-compatible identity provider.
PropelAuth acts as the source of truth for SSO client configuration -- Experio's admin panel manages SSO clients by syncing them with the PropelAuth sidecar via its integration API.
Navigate to **Admin > Users > SSO Clients** to manage SSO configurations.
## Prerequisites
Before configuring SSO, ensure you have:
* A running Experio deployment with the PropelAuth BYO sidecar container
* Admin access to the Experio admin panel
* An identity provider account (Microsoft Entra ID, Okta, or any OIDC-compatible provider)
* The redirect/callback URL for your deployment: `https:///api/auth/sso/callback`
## PropelAuth First-Time Setup
Before configuring SSO clients, you must set up PropelAuth BYO and obtain an integration API key.
### Automated Setup (Default)
The default deployment automatically creates the PropelAuth API key during initialization. To enable this, set the **PropelAuth Setup Secret** (`propelauthSetupSecret`) deployment parameter when creating or updating your Experio instance. This is the only configuration needed — everything else is handled automatically.
The automated setup works as follows:
1. You provide the `propelauthSetupSecret` deployment parameter (must be at least 16 characters). This sets the `INITIAL_SETUP_SECRET` environment variable on the PropelAuth BYO container.
2. During deployment, the `setup_propelauth` management command calls the PropelAuth `/api/initial_setup` endpoint with this secret.
3. The returned API key is automatically stored as `PROPELAUTH_API_KEY` in system settings.
The automated setup only works for the first API key creation. If you need to recreate the key, use the manual method below.
### Manual Setup (Fallback)
If automated setup is not available or fails, follow these steps:
1. **Access the cluster** where Experio is deployed.
2. **Forward the PropelAuth port** to your local machine:
```bash theme={null}
kubectl port-forward svc/propelauth 2884:2884
```
3. **Open the PropelAuth dashboard** at `http://localhost:2884`. Log in with the default credentials:
* Username: `root`
* Password: `thispasswordistemporary`
4. **Create a new password** when prompted. **Save this password securely**.
5. **Create an integration API key**: Click the gear icon, then go to **Manage Settings** and select the **Integration Keys** tab. Create a new key with `FullAccess` permissions.
6. **Save the API key** in Experio: Navigate to **Admin > Settings > System Settings > AUTH** and set `PROPELAUTH_API_KEY`. See [System Settings](/admin-guide/system-settings) for details.
Change the default PropelAuth dashboard password immediately. Store the new password and the API key securely.
## System Settings
Navigate to **Admin > Settings > System Settings > AUTH tab** to configure SSO behavior. See [System Settings](/admin-guide/system-settings) for the complete reference.
| Setting | Default | Description |
| ---------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `USE_PROPELAUTH` | `False` | Enable PropelAuth-based SSO. When enabled, the login page shows SSO authentication. |
| `PROPELAUTH_API_KEY` | — | Integration API key for the PropelAuth BYO sidecar. Set automatically during deployment or manually via the steps above. |
| `PROPELAUTH_AUTH_URL` | `http://propelauth:2884` | Internal URL of the PropelAuth BYO sidecar. Only change if your sidecar runs on a different host or port. |
| `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT` | `True` | When enabled, users created through SSO are automatically activated. Disable to require manual admin activation. |
| `ALLOW_HYBRID_LOGIN` | `False` | When enabled, both password-based and SSO login are available. When disabled (with SSO enabled), only SSO login is shown. |
**Login modes based on settings:**
* `USE_PROPELAUTH=False` — Standard email/password login only
* `USE_PROPELAUTH=True` + `ALLOW_HYBRID_LOGIN=False` — SSO-only login
* `USE_PROPELAUTH=True` + `ALLOW_HYBRID_LOGIN=True` — Both password and SSO login available
## Identity Provider Setup
Before creating an SSO client in Experio, register an application in your identity provider.
### Microsoft Entra ID (Azure AD)
1. **Register an application** in the [Azure Portal](https://portal.azure.com):
* Navigate to **Microsoft Entra ID** then **App registrations** then **New registration**.
* Name the application (e.g., "Experio SSO").
* Select **Accounts in this organizational directory only** (single tenant).
* Set **Redirect URI** type to **Web** and enter: `https:///api/auth/sso/callback`
2. **Copy credentials** from the app's Overview page:
* **Application (client) ID** — used as `client_id`
* **Directory (tenant) ID** — used as `tenant_id`
3. **Create a Client Secret**:
* Go to **Certificates & secrets**, then **Client secrets**, then **New client secret**.
* Set a description and expiration period.
* Copy the secret **Value** immediately (it is shown only once).
4. You will need these three values when creating the SSO client in Experio:
* `client_id` (Application ID)
* `tenant_id` (Directory ID)
* `client_secret` (Secret Value)
Client secrets expire. Set a calendar reminder to rotate the secret before expiration to avoid SSO login failures.
For more details, see the [PropelAuth Entra Setup Guide](https://docs.byo.propelauth.com/sso/example-setup-guides/entra).
### Okta
1. Go to Okta Admin Console, then **Applications**, then **Create App Integration**.
2. Select **OIDC - OpenID Connect** and **Web Application**.
3. Set the **Sign-in redirect URI** to `https:///api/auth/sso/callback`.
4. Note your **Client ID**, **Client Secret**, and **Okta domain** (e.g., `dev-12345.okta.com`).
For more details, see the [PropelAuth Okta Setup Guide](https://docs.byo.propelauth.com/sso/example-setup-guides/okta).
### Generic OIDC
For any OIDC-compatible provider, collect these values:
| Field | Description |
| --------------- | ----------------------------------- |
| `client_id` | OAuth2 Client ID from your provider |
| `client_secret` | OAuth2 Client Secret |
| `auth_url` | Authorization endpoint URL |
| `token_url` | Token endpoint URL |
| `userinfo_url` | UserInfo endpoint URL |
Set the redirect URI in your provider to `https:///api/auth/sso/callback`.
## Supported Identity Providers
| Provider | Protocol | Required Fields |
| --------------------------------- | -------- | ----------------------------------------------------------- |
| **Microsoft Entra ID** (Azure AD) | OIDC | Client ID, Client Secret, Tenant ID |
| **Okta** | OIDC | Client ID, Client Secret, Okta Domain |
| **Generic OIDC** | OIDC | Client ID, Client Secret, Auth URL, Token URL, UserInfo URL |
## Creating SSO Clients
Navigate to **Admin > Users > SSO Clients** and click **Create New**.
### Basic Information
* **Email Domains**: One or more domains this SSO applies to (e.g., `acme.com`, `acme.io`). Users with any of these email domains will be redirected to SSO login. Type each domain and press Enter to add it. **Leave empty to allow all domains** — the SSO client becomes a catch-all that any email can use. At most one active SSO client can have empty email domains at a time.
* **Display Name** (required): Human-readable name (e.g., "Acme Corporation").
* **Identity Provider Type** (required): Microsoft Entra ID, Okta, or Generic OIDC.
### Identity Provider Credentials
* **Client ID** (required): From your IdP app registration.
* **Client Secret** (required): From your IdP.
* **Tenant ID** (Microsoft Entra only): Azure Directory/Tenant ID.
* **SSO Domain** (Okta only): Your Okta domain (e.g., `dev-12345.okta.com`). Enter just the domain, not a full URL — the `http://` prefix is automatically stripped if provided.
* **Auth URL, Token URL, UserInfo URL** (Generic OIDC only): The OIDC endpoint URLs from your provider.
### Review and Confirm
* Review the configuration summary.
* The **Redirect URL** is displayed (auto-generated as `https:///api/auth/sso/callback`).
* This redirect URL must match what you configured in your identity provider.
The Customer ID is automatically generated from the first email domain with a unique suffix (e.g., `acme.com` becomes `acme-com-a1b2c3d4`). For allow-all clients with no email domains, the ID uses the format `allow-all-a1b2c3d4`. Customer IDs are immutable after creation.
## Activating SSO
Follow these steps carefully to enable SSO without losing admin access:
1. **Create a fallback admin user**: Before enabling SSO, ensure you have a local admin user with the same email domain as the SSO client. Set this user as a superuser so you retain access if SSO has issues.
2. **Enable SSO**: Set `USE_PROPELAUTH` to `True` in **Admin > Settings > System Settings > AUTH**.
3. **Enable hybrid login** (recommended during testing): Set `ALLOW_HYBRID_LOGIN` to `True` to keep password login available as a fallback.
4. **Test SSO**: Open a new incognito/private browser window, navigate to the login page, enter an email for the configured domain, and verify the SSO redirect works correctly.
5. **Disable hybrid login** (optional): Once SSO is confirmed working, set `ALLOW_HYBRID_LOGIN` to `False` for SSO-only login.
Always test SSO in an incognito window before logging out of your current session. If SSO is misconfigured and hybrid login is disabled, you may be locked out. Keep `ALLOW_HYBRID_LOGIN` set to `True` during initial testing.
## How SSO Login Works
1. User navigates to the Experio login page.
2. The login page shows an email-only form (SSO-only mode) or both password and SSO options (hybrid mode).
3. User enters their email and submits.
4. Experio extracts the email domain and looks up the matching SSO client.
5. If no domain-specific match is found, Experio checks for an active "allow all domains" SSO client as a fallback.
6. If a matching active SSO client is found (either by domain or fallback), PropelAuth initiates an OIDC login flow.
7. The user is redirected to their identity provider's login page (e.g., Microsoft login).
8. After authenticating, the IdP redirects back to Experio's callback URL.
9. PropelAuth completes the OIDC flow and extracts user information.
10. Experio creates or updates the user account. If `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT` is `False`, new users are created inactive and see a "pending administrator approval" message instead of being logged in.
11. Active users are redirected to Experio with an active session.
SSO users are automatically provisioned on first login. The `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT` setting controls whether they are immediately active or require admin approval.
## Managing SSO Clients
### Viewing
The SSO clients page shows a table with Display Name, Email Domains, IdP Type, Active Status, and Created dates. Clients with no email domains display an "Allow all domains" badge.
### Editing
Click any SSO client to view and edit its configuration. You can clear all email domains to make the client an "allow all" catch-all — the detail page shows an "Allow all domains" badge in this state. Changes take effect immediately for new login attempts.
Client secrets are never displayed after creation for security. To update a client secret, enter the new value in the edit form.
### Deactivating
Toggle the **Active** status to disable an SSO configuration without deleting it. Users for that domain will fall back to password authentication (if hybrid login is enabled) or be unable to log in.
### Deleting
Remove an SSO client permanently from both Experio and PropelAuth. A confirmation dialog prevents accidental deletion.
### Audit Trail
Each SSO client tracks who created the configuration, when it was created, and when it was last modified.
## Troubleshooting
| Problem | Possible Cause | Solution |
| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "SSO not configured for this organization" | No SSO client with a matching email domain and no allow-all client | Create an SSO client that includes the user's email domain, or configure an allow-all client |
| "SSO not enabled for this organization" | SSO client exists but is deactivated | Activate the SSO client in Admin > Users > SSO Clients |
| SSO redirect fails or returns 500 | Invalid API key or PropelAuth unreachable | Verify `PROPELAUTH_API_KEY` and `PROPELAUTH_AUTH_URL` in system settings |
| "Account pending administrator approval" | `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT` is `False` and the user is new | Open **Admin > Users > Profile Management**, click the **Activate** action on the inactive user (this also sends them a welcome email), or change the setting to `True`. Activating from Django admin at `/staff/core/user/` skips the welcome email. |
| "Only one allow-all client is supported" | Trying to create or edit a second SSO client with no email domains | Only one active SSO client can be configured as "allow all" at a time. Deactivate the existing one first |
| Locked out after enabling SSO | SSO misconfigured with hybrid login disabled | Access Django admin at `/staff/` to change `USE_PROPELAUTH` back to `False` |
| Client secret expired | IdP client secret has reached its expiration | Rotate the secret in your IdP and update the SSO client in Experio |
## External References
* [PropelAuth BYO Documentation](https://docs.byo.propelauth.com/)
* [Microsoft Entra Setup Guide](https://docs.byo.propelauth.com/sso/example-setup-guides/entra)
* [Okta Setup Guide](https://docs.byo.propelauth.com/sso/example-setup-guides/okta)
* [PropelAuth SSO Overview](https://docs.byo.propelauth.com/sso/overview)
* [PropelAuth Backend Setup](https://docs.byo.propelauth.com/getting-started/backend-setup)
# Startup Health
Source: https://docs.experio.cloud/admin-guide/startup-health
Verify and repair app-level migrations, seeds, indexes, and cache readiness
## Overview
Navigate to **Admin > Monitoring > Startup Health** to see whether this environment finished
its Django, graph, seed, and index bootstrap work.
This page is **app-level readiness**, not replica scaling. Use [Service Scaling](/admin-guide/service-scaling)
to change how many pipeline workers are running.
Reading the page requires Monitoring **read** access. Verify, Ensure, and Verify all require
Monitoring **write** access.
## Verify vs Ensure
| Action | What it does |
| ---------------- | --------------------------------------------------------------------------- |
| **Verify** | Read-only. Asks “is this check good right now?” and stores an audit row. |
| **Verify all** | Runs Verify for every applicable check. Does not create or repair anything. |
| **Ensure** | Mutating repair: seed, migrate, warm a cache, or create indexes. |
| **Force ensure** | Same as Ensure, but required for **dangerous** checks (large-graph DDL). |
Checks that do not apply to this environment (for example PropelAuth when unused) are hidden.
They are not shown as red.
## Status and risk
| Status | Meaning |
| ------------ | ----------------------------------------------------------------------------- |
| **ready** | Last run reported the check as good |
| **degraded** | Partial or retryable failure |
| **missing** | Required seed, index, or cache is not present |
| **terminal** | Needs a maintenance-window Force ensure (typical for large-graph lineage DDL) |
| **unknown** | Not verified yet, or the last run raised an error |
| Risk | Ensure behavior |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| **safe** | Ensure runs immediately (browser; no extra confirm) |
| **gated** | Browser confirm, then Ensure. **UI hint only** — a Monitoring writer can POST Ensure without `force`. |
| **dangerous** | Confirm, then Force ensure. Server rejects Ensure unless `force=true`. |
| **ops-only** | Verify only — no Ensure button |
## Blocks and last-run audit
Each row shows:
* **Blocks** — workers or features that stay gated while the check is missing
* **Blocked by** — other checks that should be ready first
* **Last run** — relative time and who triggered Verify or Ensure
* **History** — recent audit rows (action, status, actor, duration, message)
## Lineage indexes (Force ensure)
The **Lineage indexes** check is **dangerous**. Verify only lists whether required indexes are
operational. Ensure calls the same **Redis-locked** path as `ensure_lineage_indexes` (other checks
do not take a registry-wide lock) and can stall a populated FalkorDB graph for many minutes.
**Django DB migrations** is **gated**: Ensure runs `migrate --noinput` after a browser confirm.
Run **Force ensure** for lineage indexes only in a maintenance window. Do not run it while
ingestion, structured data, or enrichment workers are writing to the graph. Lineage `CREATE INDEX`
is not performed automatically on Django startup.
If Verify reports **terminal**, the graph is over the safety ceiling and needs an explicit Force
ensure after you have scheduled downtime.
## Orphaned agent flow runs
An [Agent Flow](/admin-guide/agent-flows) run executes inside the API process. If that process stops
mid-run — a deploy, a pod restart, a dev-server reload — nothing is left to finish the run, and the
run keeps whatever status it had: the conversation goes on looking busy for ever.
The API reconciles those runs once on every start. **Orphaned agent flow runs** is the operator lever
for the same repair: Verify reports how many stale runs exist right now, and Ensure (**gated** — a
browser confirm, no maintenance window) marks them failed without restarting the API.
What it takes and what it leaves alone:
* **A run parked at a Human review gate is never touched.** It is not orphaned — it is waiting for a
reviewer, is still listed in the Agent Inbox, and resumes the moment someone answers.
* A **running** run whose executor stopped renewing its lease is reaped after a few minutes.
* A **pending** run that outlived the process that would have started it is reaped on the same clock.
Agent flows have no queue, so nothing else will ever move it.
* A **running** run that never recorded a lease at all — written by an older release, or by a replica
still being rolled — is left alone until it has been untouched for **hours**, not minutes, so a
long-running node is never cut off mid-work.
Reaping a run you are unsure about is not destructive. If the executor of a run **this check reaped**
turns out to have been alive after all, that run can still settle its real result when it finishes, and
the files it produced still reach the conversation.
## Platform health endpoint
`GET /health/` includes an informational `checks.init` object: a last-run **summary** of Startup
Health (ready / missing / terminal counts). It does **not** re-run checks and it never returns
HTTP 503 by itself. Load balancers should keep using the existing critical set (PostgreSQL,
Redis, RabbitMQ, LLM).
## Deploy notes
* Apply the `startup_health` Django migration (`InitCheckRun`) before relying on last-run history.
* After a new environment or graph rebuild, open this page and **Verify all**.
* Repair missing seeds with **Ensure** on the specific check rather than expanding `migrate-init`
into a long blocking job.
* Large Falkor graphs: Force ensure lineage only in a maintenance window, then Verify again
before starting ingestion\_v2, structured\_data, and enrichment.
## Related
* [Jobs & Monitoring](/admin-guide/jobs-monitoring) — job list, logs, and scan-order stats
* [Service Scaling](/admin-guide/service-scaling) — Omnistrate replica counts
* [Graph Lineage](/admin-guide/graph-lineage) — what lineage records are used for
# System Settings
Source: https://docs.experio.cloud/admin-guide/system-settings
View and configure system-level settings
## Overview
System settings provide access to low-level configuration values organized by category. These settings control database connections, authentication, job processing, infrastructure, and AI model behavior.
Navigate to **Admin > Settings > System Settings**.
## Categories
Settings are organized into tabs by category:
| Category | Icon | Description |
| ------------------ | --------- | ---------------------------------------------------------------- |
| **LLM** | CPU | AI model and API configuration |
| **DB** | Database | Database connection settings |
| **AUTH** | Shield | Authentication and security settings |
| **CORE** | Settings | Core application settings |
| **JOBS** | Briefcase | Job processing and queue configuration |
| **INFRA** | Server | Infrastructure and deployment settings |
| **FEATURE\_FLAGS** | Flag | Features that ship switched off and are turned on per deployment |
Each tab shows the number of settings in that category.
## Viewing Settings
Settings are displayed as key-value pairs with:
* Setting name
* Current value (encrypted values are masked)
* Read-only indicator (for settings that cannot be changed from the UI)
## Editing Settings
Settings marked as editable can be modified directly from the admin panel. Read-only settings must be changed through environment variables or the Django admin interface.
Only settings marked "Show in Dashboard" in the Django admin are visible here. If you need to access additional settings, use the Django admin interface or configure them via environment variables.
Changing system settings can affect the behavior of the entire platform. Ensure you understand the impact of a change before saving. Some changes may require a server restart to take effect.
***
## Settings Reference
**Legend:** Settings prefixed with 🔒 store encrypted values. Settings marked `(Dashboard)` are visible in the admin dashboard UI. Settings marked `(Client)` are accessible to the frontend client application.
### LLM Settings
| Setting | Description | Default | Related Docs |
| --------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `USE_AZURE_OPENAI` | Use Azure OpenAI for LLM | `True` | — |
| `USE_AZURE_OPENAI_EMBEDDINGS` | Use Azure OpenAI for embeddings | `True` | — |
| `USE_GOOGLE_GENAI` | Use Google Gemini | `False` | — |
| 🔒 `AZURE_API_KEY` | Azure OpenAI API key | — | — |
| 🔒 `GOOGLE_API_KEY` | Google Gemini API key | — | — |
| `GOOGLE_GENAI_MODEL` | Google GenAI model name | `gemini-2.0-flash` | — |
| `AZURE_CHAT_API_CONTEXT_WINDOW` | Azure OpenAI context window size (tokens) | `128000` | — |
| `AZURE_CHAT_API_DEPLOYMENT` | Azure OpenAI chat deployment name | `gpt-4o` | — |
| `AZURE_CHAT_API_VERSION` | Azure OpenAI API version for chat | `2024-04-01-preview` | — |
| `AZURE_CHAT_API_ENDPOINT` | Full Azure OpenAI endpoint URL for chat | *(provider-specific)* | — |
| `AZURE_CHAT_API_TEMPERATURE` | Temperature for Azure OpenAI chat | `0.0` | — |
| `AZURE_CHAT_API_FREQUENCY_PENALTY` | Frequency penalty for Azure OpenAI | `0.1` | — |
| `AZURE_CHAT_API_PRESENCE_PENALTY` | Presence penalty for Azure OpenAI | `-0.5` | — |
| `AZURE_EMBEDDINGS_API_DEPLOYMENT` | Azure OpenAI embeddings model name | `text-embedding-3-large` | — |
| `AZURE_EMBEDDINGS_API_VERSION` | Azure OpenAI API version for embeddings | `2023-05-15` | — |
| `AZURE_EMBEDDINGS_API_ENDPOINT` | Full Azure OpenAI endpoint URL for embeddings | *(provider-specific)* | — |
| `AZURE_EMBEDDINGS_API_DIMENSIONS` | Dimensions for Azure OpenAI embeddings | `1536` | — |
| 🔒 `AZURE_EMBEDDINGS_API_KEY` | Azure OpenAI API key for embeddings (optional fallback) | — | — |
| `GOOGLE_GENAI_CONTEXT_WINDOW` | Google Gemini context window size (tokens) | `2000000` | — |
| `GOOGLE_GENAI_TEMPERATURE` | Temperature for Google Gemini | `0.05` | — |
| `CONTEXT_SAFETY_MARGIN` | Safety margin for context window (fraction of max) | `0.9` | — |
| `USE_AWS_BEDROCK` | Use AWS Bedrock | `False` | — |
| 🔒 `AWS_BEDROCK_ACCESS_KEY_ID` | AWS Bedrock Access Key ID | — | — |
| 🔒 `AWS_BEDROCK_SECRET_ACCESS_KEY` | AWS Bedrock Secret Access Key | — | — |
| `REASONING_MODEL_CONFIG` `(Dashboard)` | Default reasoning model configuration | `azure-gpt4o-reasoning-default` | [Model Configurations](/admin-guide/model-configurations) |
| `EMBEDDING_MODEL_CONFIG` `(Dashboard)` | Default embedding model configuration | `azure-embedding-default` | [Model Configurations](/admin-guide/model-configurations) |
| `ADMIN_HELP_MODEL_CONFIG` `(Dashboard)` | Chat model for the admin Help panel (name or UUID); falls back to reasoning model | — | [Admin Overview](/admin-guide/overview#admin-help) |
| `CLASSIFICATION_MODEL_CONFIG` `(Dashboard)` | Default classification model config name or UUID | — | [Model Configurations](/admin-guide/model-configurations) |
| `INGESTION_LARGE_MODEL_CONFIG` `(Dashboard)` | Default model config used for large-document ingestion paths | — | [Model Configurations](/admin-guide/model-configurations) |
| `INGESTION_MEDIUM_MODEL_CONFIG` `(Dashboard)` | Model config for medium-tier primary ingestion (content-type `model_tier: medium`) | — | [Model Configurations](/admin-guide/model-configurations), [Extraction Policy](/admin-guide/extraction-policy) |
| `INGESTION_SMALL_MODEL_CONFIG` `(Dashboard)` | Model config used for small-document ingestion paths and secondary ingestion steps | — | [Model Configurations](/admin-guide/model-configurations) |
| `LARGE_DOCUMENT_TOKEN_THRESHOLD` | Token threshold for large document handling | `5000` | — |
| `LEAD_PARAGRAPH_TOKEN_THRESHOLD` | Token threshold for lead paragraph extraction | `20` | — |
| `MAX_LIST_CONTEXT_CHARS` | Maximum characters for list context | `1000` | — |
| `PRODUCED_DOC_CONTEXT_CHARS` | Max characters of a single produced document included in chat context | `4000` | [Agent Flows](/admin-guide/agent-flows) |
| `PRODUCED_DOC_CONTEXT_TOTAL_CHARS` | Max characters of produced documents included in one conversation's chat context | `12000` | [Agent Flows](/admin-guide/agent-flows) |
#### Produced Documents in Chat Context
When a flow writes a document into a conversation, an excerpt of that document is included in the chat's context as a head start on follow-up questions about it. The two caps bound that **excerpt** only — they are not the limit of what the assistant can see, because it can also list the documents produced in the conversation, search inside one, and read any part of it on demand (each read is access-checked for the person asking). They still matter, because the excerpt is what the assistant has before it goes looking. `PRODUCED_DOC_CONTEXT_CHARS` bounds the document text in a single excerpt. `PRODUCED_DOC_CONTEXT_TOTAL_CHARS` is spent on document text *and* on the sentences that announce a cut — an excerpt that had to be truncated carries that sentence on top of its own allowance, and the sentence is charged in full against the conversation budget. A conversation full of truncated documents therefore carries less document text than the total alone suggests.
The conversation budget is spent **newest first**, so the document the flow just wrote — the one the user's next message is almost certainly about — keeps its excerpt, and older documents give way instead. Nothing is dropped quietly: an excerpt that was cut short says so, and a document that got no budget at all is still named in the context as existing, with its body omitted. That announcement is deliberately exempt from `PRODUCED_DOC_CONTEXT_TOTAL_CHARS` — saying a document exists must not be starved by the budget it is announcing — so the injected text can exceed the total by one short fixed sentence for every document whose body was omitted. A truncation notice is never cut in half either, because half a notice reads as document text: when the leftover budget cannot hold body text plus a whole notice, that document degrades to the omission announcement instead of getting a sliver of body.
### DB Settings
Graph connectivity uses **`GRAPH_PROVIDER`** plus either the **Neo4j** or **FalkorDB** connection group.
See [Graph backend (Neo4j & FalkorDB)](/admin-guide/graph-backend) for how switching works and when to run a migration.
| Setting | Description | Default | Related Docs |
| --------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------- |
| `GRAPH_PROVIDER` `(Dashboard)` | Active graph backend: `neo4j` or `falkordb` | `neo4j` | [Graph backend](/admin-guide/graph-backend) |
| 🔒 `NEO4J_URI` `(Dashboard)` | Neo4j Bolt URI (used when `GRAPH_PROVIDER=neo4j`) | — | [Graph backend](/admin-guide/graph-backend) |
| `NEO4J_DATABASE` `(Dashboard)` | Neo4j database name | `neo4j` | — |
| `DEFAULT_NEO4J_DATABASE` `(Dashboard)` | Default Neo4j database | `neo4j` | — |
| `NEO4J_USER` `(Dashboard)` | Neo4j username | `neo4j` | — |
| 🔒 `NEO4J_PASSWORD` `(Dashboard)` | Neo4j password | — | — |
| `FALKOR_URI` `(Dashboard)` | FalkorDB Redis-style URI (used when `GRAPH_PROVIDER=falkordb`) | `redis://falkordb:6379` (cluster); devcontainer uses `redis://localhost:6380` on the host | [Graph backend](/admin-guide/graph-backend) |
| `FALKOR_DATABASE` `(Dashboard)` | FalkorDB graph name | `experio` | — |
| `DEFAULT_FALKOR_DATABASE` `(Dashboard)` | Default FalkorDB graph name | `experio` | — |
| `FALKOR_USER` `(Dashboard)` | FalkorDB username | `default` | — |
| 🔒 `FALKOR_PASSWORD` `(Dashboard)` | FalkorDB password | — | — |
Older deployments may still list **`USE_NEO4J`**, **`USE_FALKOR`**, **`FALKOR_DB_USER`**, or **`FALKOR_DB_PASSWORD`**.
Those keys are **legacy**; **`GRAPH_PROVIDER`** with **`NEO4J_*`** / **`FALKOR_*`** is authoritative for new installs and
the dashboard. Prefer aligning configuration with the rows above.
### AUTH Settings
| Setting | Description | Default | Related Docs |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------- |
| `VITE_USE_AUTH0` `(Client)` | Enable Auth0 integration | `False` | — |
| `VITE_CLIENT_ID` `(Client)` | Auth0 client ID | — | — |
| `VITE_DOMAIN` `(Client)` | Auth0 domain | — | — |
| `AUTH0_ISSUER` | Auth0 issuer URL | — | — |
| `AUTH0_JWKS_URL` | Auth0 JWKS URL | `/.well-known/jwks.json` | — |
| `USE_PROPELAUTH` `(Dashboard, Client)` | Enable PropelAuth SSO authentication | `False` | [SSO Configuration](/admin-guide/sso-configuration) |
| `ALLOW_HYBRID_LOGIN` `(Dashboard, Client)` | Allow both password and SSO login | `False` | [SSO Configuration](/admin-guide/sso-configuration) |
| `PROPELAUTH_AUTH_URL` `(Dashboard)` | PropelAuth BYO sidecar URL | `http://propelauth:2884` | [SSO Configuration](/admin-guide/sso-configuration) |
| 🔒 `PROPELAUTH_API_KEY` `(Dashboard)` | PropelAuth integration API key | — | [SSO Configuration](/admin-guide/sso-configuration) |
| `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT` `(Dashboard)` | Auto-activate SSO users on first login | `True` | [SSO Configuration](/admin-guide/sso-configuration) |
| `OAUTH2_REDIRECT_DOMAIN` `(Client)` | OAuth2 redirect domain for callback URLs | `http://localhost:8080` | — |
| 🔒 `GOOGLE_MCP_CLIENT_ID` `(Dashboard, Client)` | Google OAuth Client ID for MCP integration | — | [Integrations](/admin-guide/integrations) |
| 🔒 `GOOGLE_MCP_CLIENT_SECRET` `(Dashboard, Client)` | Google OAuth Client Secret for MCP integration | — | [Integrations](/admin-guide/integrations) |
| `ALLOW_PASSWORD_LOGIN` `(Dashboard)` | Allow users to log in with email + password. Disable to force magic-link or SSO. | `True` | — |
| `ALLOW_MAGIC_LINK_LOGIN` `(Dashboard)` | Allow users to request a one-time magic-link login email. | `False` | — |
| `ALLOW_SELF_REGISTRATION` `(Dashboard)` | Allow anonymous users to request an account via the public register form. Disable for SSO-only tenants. | `True` | — |
| `MAGIC_LINK_TOKEN_TTL_MINUTES` `(Dashboard)` | How long a magic-link token is valid for, in minutes. | `15` | — |
| `EMAIL_PROVIDER` | Transactional email provider. Single supported value: `azure_ecs`. | `azure_ecs` | — |
| 🔒 `AZURE_COMMUNICATION_CONNECTION_STRING` `(Dashboard)` | Azure Email Communication Services connection string. Required when `EMAIL_PROVIDER=azure_ecs`. | — | — |
| `AZURE_ECS_SENDER_ADDRESS` `(Dashboard)` | Sender address (MailFrom) used by Azure Email Communication Services. Must be a verified address on the linked domain. | `login@experiolabs.ai` | — |
| `AUTH_FROM_DISPLAY_NAME` `(Dashboard)` | Display name rendered on the **From:** line of transactional emails (welcome, magic link, password reset, SSO pending approval). | `Experio` | — |
| `AUTH_REPLY_TO_ADDRESS` `(Dashboard)` | **Reply-To** address added to every transactional email. Should point at a monitored mailbox so user replies are not silently dropped. | `support@experiolabs.ai` | — |
#### Login Methods
`ALLOW_PASSWORD_LOGIN`, `ALLOW_MAGIC_LINK_LOGIN`, and `ALLOW_SELF_REGISTRATION` control which authentication paths the public login/register pages expose. They compose with `USE_PROPELAUTH` and `ALLOW_HYBRID_LOGIN`:
* For SSO-only tenants, set `ALLOW_PASSWORD_LOGIN=False`, `ALLOW_MAGIC_LINK_LOGIN=False`, `ALLOW_SELF_REGISTRATION=False`, `USE_PROPELAUTH=True`, and `ALLOW_HYBRID_LOGIN=False`.
* Magic-link login requires a working email provider (see Transactional Email below). The link is valid for `MAGIC_LINK_TOKEN_TTL_MINUTES` minutes.
#### Transactional Email Deliverability
The `AZURE_ECS_SENDER_ADDRESS`, `AUTH_FROM_DISPLAY_NAME`, and `AUTH_REPLY_TO_ADDRESS` settings together control how outbound auth emails appear to recipients and to mailbox spam filters:
* The **From:** line is rendered as `"" ` (e.g. `"Experio" `).
* A `Reply-To` header points at `AUTH_REPLY_TO_ADDRESS` so replies land in a monitored inbox instead of bouncing off the no-reply sender.
* Every send also sets `Message-ID` (with the sender's host), `Date`, and `Auto-Submitted: auto-generated`, and ships a real plain-text alternative alongside the HTML body — these reduce the chance of the message landing in junk.
* `AZURE_ECS_SENDER_ADDRESS` must be a verified MailFrom on the Azure ECS linked domain. Update it (and the corresponding domain verification) if you re-brand the sender.
#### SSO / PropelAuth
The five PropelAuth settings (`USE_PROPELAUTH`, `PROPELAUTH_API_KEY`, `PROPELAUTH_AUTH_URL`, `PROPELAUTH_SSO_USERS_ACTIVE_BY_DEFAULT`, and `ALLOW_HYBRID_LOGIN`) work together to enable enterprise Single Sign-On. See the [SSO Configuration](/admin-guide/sso-configuration) page for complete setup instructions, identity provider guides, and troubleshooting.
#### Google Workspace Integration
The `GOOGLE_MCP_CLIENT_ID` and `GOOGLE_MCP_CLIENT_SECRET` settings enable per-user Google Workspace MCP integration. See the [Integrations](/admin-guide/integrations) page for setup instructions.
### CORE Settings
| Setting | Description | Default | Related Docs |
| ----------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------- | --------------------------------------------------------- |
| `STAFF_EMAIL` | Staff notification email address | `Experio ` | — |
| `DEFAULT_FROM_EMAIL` | Default sender email address | `Experio ` | — |
| `CLIENT_NAME` `(Client)` | Client branding name | `Experio` | [Client Configuration](/admin-guide/client-configuration) |
| *(Icon/wide logos)* | Stored as **`ClientLogo`** uploads in Admin — not writable as `CLIENT_LOGO_*` keys | — | [Client Configuration](/admin-guide/client-configuration) |
| `DJANGO_SERVER_URL` | Django server base URL | `http://server:8000` | — |
| `VITE_DEV_BACKEND_URL` `(Client)` | Development backend URL | — | — |
| `BOX_REDIRECT_URI` | Box API redirect URI | `http://localhost:8080` | — |
| `GOOGLE_DRIVE_REDIRECT_URI` | Google Drive redirect URI | `http://localhost:8080` | — |
| `SHOW_CHANNEL_ID_UI` `(Client)` | Show channel ID in the UI | `False` | — |
| `CLIENT_TRACING_ENABLED` `(Client)` | Enable client-side telemetry | `False` | — |
| `CLIENT_TRACING_SCOPE` `(Client)` | Client tracing scope (disabled, errors, errors\_api, full) | `errors` | — |
| `CLIENT_SIGNOZ_ENDPOINT` `(Client)` | SigNoz OTLP endpoint for client telemetry | *(provider-specific)* | — |
| `CLIENT_SIGNOZ_INGESTION_KEY` `(Client)` | SigNoz ingestion key for client telemetry | — | — |
| `USE_KREUZBERG_PARSER` | Use Kreuzberg text extraction parser | `True` | — |
| `MCP_ENABLED` | Enable Model Context Protocol | `True` | [MCP Servers](/admin-guide/mcp-servers) |
| `MCP_CONNECTION_TIMEOUT` | MCP connection timeout (seconds) | `30` | [MCP Servers](/admin-guide/mcp-servers) |
| `SCOPE_MAX_COUNT_THRESHOLD` `(Dashboard)` | Max count threshold for scope phase | `300` | — |
| `SKIP_CYPHER_VALIDATION` `(Dashboard)` | Skip LLM-based Cypher validation | `True` | — |
| `RESTRICT_SHARING_TO_SAME_DOMAIN` `(Dashboard)` | Limit non-staff search and sharing to same email domain (staff bypass) | `True` | — |
| `PERSONAS_ENABLED` `(Dashboard)` | Master switch for persona UI, identity injection, and persona graph steps | `False` | [Personas](/admin-guide/personas) |
| `REQUIRED_PERSONAS` `(Dashboard)` | Require persona selection at login (only when `PERSONAS_ENABLED` is on) | `False` | [Personas](/admin-guide/personas) |
| 🔒 `TAVILY_API_KEY` `(Dashboard)` | Tavily API key for chat web search and agentflows web research | — | — |
#### Persona feature flags
`PERSONAS_ENABLED` and `REQUIRED_PERSONAS` ship **off** so existing tenants are unaffected until you
opt in. See [Personas](/admin-guide/personas) for the behavior matrix, admin workflow, and deploy
checklist.
* **`PERSONAS_ENABLED`** — Off hides persona gate, profile picker, and composer toggle; the agent
does not inject identity or run persona graph nodes. Admin **Settings > Personas** stays available
for seeding.
* **`REQUIRED_PERSONAS`** — On (with master on) blocks the app until the user picks a persona. Seed
active personas and gating forms before enabling.
### JOBS Settings
| Setting | Description | Default | Related Docs |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------- |
| `DOCLING_SERVE_ENDPOINT` | Docling service API endpoint | `http://docling:5001` | — |
| `INGESTION_THREADS` `(Dashboard)` | Number of ingestion worker threads | `6` | [Jobs & Monitoring](/admin-guide/jobs-monitoring) |
| `INGESTION_PARSER_CONNECTION_RETRIES` | Max retries for Docling connection | `20` | — |
| `INGESTION_PARSER_RETRY_SLEEP_SECONDS` | Sleep between Docling retries (seconds) | `30` | — |
| `INGESTION_USE_MULTI_THREADING` | Enable multi-threaded ingestion | `True` | — |
| `MINIO_RETENTION_DAYS` `(Dashboard)` | MinIO file retention (days) | `7` | — |
| `KREUZBERG_ENDPOINT` | Kreuzberg text extractor endpoint | `http://kreuzberg-text-extractor:8000` | — |
| `KREUZBERG_REQUEST_TIMEOUT_SECONDS` | Kreuzberg request timeout (seconds) | `600` | — |
| `SERVICE_SECRET_KEY` | Internal service authentication key | *(auto-generated)* | — |
| `STRUCTURED_DATA_THREADS` | Structured data processing threads | `4` | — |
| `ENRICHMENT_THREADS` | Enrichment job worker threads | `4` | [Enrichment Rules](/admin-guide/enrichment-rules) |
| `GRAPH_EVALUATION_THREADS` | Graph evaluation job worker threads | `2` | [Graph Evaluation](/admin-guide/graph-evaluation) |
| `GRAPH_EVALUATION_SHUTDOWN_TIMEOUT_SECONDS` | Seconds to wait for graph evaluation workers on shutdown | `600` | [Graph Evaluation](/admin-guide/graph-evaluation) |
| `GRAPH_EVALUATION_SAMPLES_PER_CONTENT_TYPE` | Default samples per content type for manual and auto runs | `3` | [Graph Evaluation](/admin-guide/graph-evaluation) |
| `GRAPH_EVALUATION_AUTO_RUN_AFTER_JOB` | Automatically queue graph evaluation when all job files finish processing | `False` | [Graph Evaluation](/admin-guide/graph-evaluation) |
| `ENRICHMENT_NODE_CONCURRENCY` | Concurrent nodes per enrichment job | `4` | [Enrichment Rules](/admin-guide/enrichment-rules) |
| `ENRICHMENT_RESULT_RETENTION_DAYS` | Days to retain `EnrichmentResult` rows before purge (`0` = keep forever) | `90` | [Enrichment Rules](/admin-guide/enrichment-rules) |
| `SCALE_TO_ZERO_ENABLED` `(Dashboard)` | Enable scale-to-zero for job services | `False` | — |
| `SCALE_TO_ZERO_DRY_RUN` `(Dashboard)` | Scale-to-zero dry run mode | `False` | — |
| `SCALE_TO_ZERO_POLL_INTERVAL` `(Dashboard)` | Scale-to-zero polling interval (seconds) | `60` | — |
| `USE_MULTI_RESOURCE_SCALING` `(Dashboard)` | Let the autoscaler apply all pending scaling actions in one grouped Omnistrate call per cycle (instead of one action per cycle) | `False` | [Jobs & Monitoring](/admin-guide/jobs-monitoring) |
| `SCALE_UP_PENDING_TIMEOUT_SECONDS` `(Dashboard)` | How long an accepted scaling request may wait for resources to reach ACTIVE before the autoscaler abandons it (platform scale-ups normally take several minutes) | `600` | [Jobs & Monitoring](/admin-guide/jobs-monitoring) |
| `OMNISTRATE_SIDECAR_URL` | Omnistrate sidecar URL | `http://127.0.0.1:49750` | — |
| `ENABLE_FULL_SCAN_DOCUMENT_CHECK` | Check for existing documents during full scans | `True` | — |
| `MAX_PARSED_SIZE_MB` `(Dashboard)` | Max parsed file size (MB) | `20` | — |
| `MAX_INGESTION_CHUNKS` `(Dashboard)` | Max chunks per document | `120` | — |
| `INGESTION_TIMEOUT_SECONDS` `(Dashboard)` | Document ingestion timeout (seconds) | `900` | — |
| `INGESTION_COST_GUARD_CHUNK_THRESHOLD` `(Dashboard)` | Estimated chunk count above which full-mode extraction falls back to metadata-only (aligned with `MAX_INGESTION_CHUNKS`) | `120` | [Extraction Policy](/admin-guide/extraction-policy) |
| `MAX_EXCEL_SHEET_CHARS` | Skip LLM extraction on Excel sheets above this character count | `50000` | [Extraction Policy](/admin-guide/extraction-policy) |
| `MAX_EXCEL_INGESTION_CHUNKS_PER_SHEET` | Max LLM chunks per sheet in full-mode Excel extraction | `25` | [Extraction Policy](/admin-guide/extraction-policy) |
| `PHOENIX_TRACING_MODE` `(Dashboard)` | Phoenix tracing mode (disabled, central, cluster) | `disabled` | — |
| `PHOENIX_ENDPOINT` `(Dashboard)` | Phoenix endpoint URL | *(provider-specific)* | — |
| `PHOENIX_TRACING_SCOPE` `(Dashboard)` | Phoenix tracing scope (jobs, retrieval, all) | `all` | — |
### INFRA Settings
| Setting | Description | Default | Related Docs |
| --------------------------------------- | ---------------------------------------------- | -------------- | ------------ |
| `RABBITMQ_HOST` | RabbitMQ server hostname | `rabbitmq-0` | — |
| `RABBITMQ_USER` | RabbitMQ username | `admin` | — |
| 🔒 `RABBITMQ_PASS` | RabbitMQ password | — | — |
| `RABBITMQ_PORT` | RabbitMQ server port | `5672` | — |
| `RABBITMQ_PUBLISHER_HEARTBEAT` | Publisher heartbeat interval (seconds) | `20` | — |
| `RABBITMQ_PUBLISHER_BLOCKED_TIMEOUT` | Publisher blocked connection timeout (seconds) | `60` | — |
| `RABBITMQ_CONSUMER_HEARTBEAT` | Consumer heartbeat interval (seconds) | `20` | — |
| `RABBITMQ_CONSUMER_BLOCKED_TIMEOUT` | Consumer blocked connection timeout (seconds) | `60` | — |
| `MINIO_ENDPOINT` | MinIO server endpoint | `minio-0:9000` | — |
| `MINIO_ACCESS_KEY` | MinIO access key | `admin` | — |
| 🔒 `MINIO_SECRET_KEY` | MinIO secret key | — | — |
| `MINIO_SECURE` | Enable MinIO TLS | `False` | — |
| `SIGNOZ_QUERY_ENDPOINT` `(Dashboard)` | SigNoz Query API base URL | — | — |
| 🔒 `SIGNOZ_QUERY_API_KEY` `(Dashboard)` | SigNoz Query API access token | — | — |
### FEATURE\_FLAGS Settings
| Setting | Description | Default | Related Docs |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------- |
| `GRAPH_AUTHORIZATION_MODE` `(Dashboard)` | Graph access control: `disabled`, `shadow` (work out and log the decision, still answer from the unfiltered query) or `enforce` (filter every answer) | `disabled` | [Graph Access Control](/admin-guide/graph-access-control) |
| `GRAPH_AUTHZ_SUPERUSER_BYPASS` `(Dashboard)` | Let superusers read the graph **without** the access policy applied | `False` | [Graph Access Control](/admin-guide/graph-access-control) |
**Enter the policy before switching `GRAPH_AUTHORIZATION_MODE` to `enforce`.** A label with no baseline row
is treated as private, so on a deployment where the policy has not been entered yet, `enforce` makes the
knowledge graph invisible to everyone. Use `shadow` first: it works out and records the same decision while
still answering from the unfiltered query, so you can see what would change before anything does.
**`GRAPH_AUTHZ_SUPERUSER_BYPASS` is one switch for all superusers, not a per-account setting.** Turning it on
means every superuser reads the graph unfiltered, immediately, with no further step. It exists so a tester or
the customer's own IT owner can check the whole graph; it is not a way to give one person more access.
It has no effect unless the mode is `enforce`, and a bypassed read still works out the decision it is
ignoring and records it, so the audit trail survives. Anyone whose session is bypassing sees a banner saying
so — see [What the AI can see](/user-guide/chat-interface).
# Taxonomies
Source: https://docs.experio.cloud/admin-guide/taxonomies
Create hierarchical classification systems for organizing knowledge
## Overview
Taxonomies provide structured, hierarchical vocabularies for classifying and categorizing entities in the knowledge graph. They help ensure consistent tagging and enable structured browsing of your organization's knowledge.
Navigate to **Admin > Graph > Taxonomies**.
## Viewing Taxonomies
The taxonomies page displays a hierarchical tree table showing:
* Taxonomy name
* Parent-child relationships
* Full path from root to leaf
* Associated synonyms
* Status information
You can filter taxonomies by **type** to focus on a specific classification system.
## Creating Taxonomies
Click **Add New Taxonomy** to create a new entry:
| Field | Description |
| ------------ | ---------------------------------------------------------------------------- |
| **Name** | The taxonomy term (e.g., "Healthcare", "Federal Contracting") |
| **Type** | The classification system this belongs to (e.g., "Industry", "Service Line") |
| **Parent** | Optional parent taxonomy for hierarchical nesting |
| **Synonyms** | Alternative terms that should be treated as equivalent |
### Hierarchical Structure
Taxonomies support unlimited nesting depth. For example:
```
Industry
├── Healthcare
│ ├── Pharmaceuticals
│ └── Medical Devices
├── Technology
│ ├── Software
│ └── Hardware
└── Government
├── Federal
└── State & Local
```
## Type-Specific Views
Click on a taxonomy type in the sidebar to see a filtered view showing only taxonomies of that type. This provides a focused view for managing a single classification system.
## Synonyms
Each taxonomy entry can have multiple synonyms — alternative terms that the AI treats as equivalent during classification. For example:
* **Healthcare** → "Health Care", "HC", "Medical"
* **Federal** → "Fed", "US Government", "USG"
Synonyms improve classification accuracy by helping the AI recognize variations in terminology.
## Managing Taxonomies
### Editing
Click any taxonomy entry to edit its name, parent, type, or synonyms.
### Deleting
Remove taxonomy entries that are no longer needed. Deleting a parent entry does not automatically delete its children — you'll need to reassign or remove them separately.
Build your taxonomy before ingesting documents. The AI uses taxonomies during classification, so having a well-defined vocabulary from the start produces better results.
## Related: Inference Rules
Taxonomies are also used in [Enrichment Rules](/admin-guide/enrichment-rules) via the `@TaxonomyName` syntax. When you reference a taxonomy in an enrichment prompt (e.g., `@Domain`), the system expands it to the full list of values, helping the LLM classify and tag nodes consistently.
# Relationship attributes
Source: https://docs.experio.cloud/architecture/relationship-attributes
How ontology relationship attributes flow through content types, extraction, and Neo4j
## Overview
**Relationship attributes** extend edges with structured properties (similar to entity attributes on nodes).
They are defined in the **ontology** per relationship triple (source entity type, relationship type,
target entity type), configured per **content type**, extracted by the LLM as JSON on each edge, filtered
to allowed keys, and written to the graph as relationship properties where applicable.
## Data model
* **Ontology** defines relationships between **entity types** and optional **relationship attributes**
(name, type, extraction defaults, enum options).
* **Content types** persist selections and overrides under `DocumentType.metadata["relations"]`.
* **Extraction JSON** uses **instance names** on edges:
`{ "source": "", "type": "", "target": "", "attributes": { ... } }`.
That differs from schema lines in prompts, which show **entity type** labels for source/target.
## Available entities API
`GET /api/ingestion/content_types/available_entities/` returns the default ontology via
`extract_schema_structure()`, which **normalizes relationship attributes** the same way as node attributes
(stable `extraction_instructions`, enum `options`, optional `semantic_intent`).
## Prompt construction
`DocumentTypeConfig.get_document_type_config()` builds `prompt_config["relationships"]` with
`attributes` as **attribute name → extraction instruction text**.
`build_prompt()` / `build_completion_prompt()` (and paginated prompts) append those instructions per
triple so the model knows which keys may appear under `relationships[].attributes`.
## Filtering extracted attributes
`filter_relationship_attributes_by_config()` restricts keys to those allowed on the document type.
Schema triples use **entity type names** for source/target; extracted edges use **instance names**.
`relationship_key_for_schema_lookup()` resolves instance names to entity types using the extracted
`entities` list before lookup. If a triple cannot be aligned to the schema map, extracted attributes are
not blindly cleared (avoids dropping valid LLM output when matching is imperfect).
## Graph and LLM schema context
Relationship properties are reflected in graph introspection and LLM-facing schema strings on both
Neo4j and FalkorDB. Helpers live in `experio.neo4j_graph.schema_helpers` (shared formatting and
ontology filtering).
**Fan-out statistics** — For each relationship triple in the schema, Experio computes
`fan_out_risk`, max distinct targets per source, and max distinct sources per target from live graph
data. A single portable batch Cypher query runs on whichever provider is active (no Neo4j 5.23+
scoped subquery required). Stats are cached with the schema (24h TTL); refresh after deploy or
provider switch.
## Related code
| Area | Package / module |
| ------------------------- | ---------------------------------------------------------------------------- |
| Content-type metadata | `experio.ingestion.views.content_types` |
| Prompt config and prompts | `experio.ingestion.services.document_type_config`, `experio.ingestion.utils` |
| Extraction pipeline | `experio.ingestion.services.extraction_processor` |
| Neo4j schema | `experio.neo4j_graph.schema_helpers`, `experio.neo4j_graph.api` |
| Artifact-types UI | `client-cn` content-types editor |
## See also
* [Content Types](/admin-guide/content-types) (admin workflow)
* `server/experio/ingestion/README.md` (developer notes)
# What is Experio?
Source: https://docs.experio.cloud/introduction
AI-powered knowledge management for consulting and professional services firms
## Overview
Experio is an AI-powered knowledge management platform that transforms your organization's scattered documents and data into a living, intelligent system. Purpose-built for consulting and professional services firms, Experio preserves organizational memory and delivers instant, context-aware insights through specialized AI agents.
## The Problem
Professional services firms face persistent knowledge challenges:
* **Knowledge chaos** — Critical information is scattered across Box, Google Drive, SharePoint, and other systems with no unified way to search or access it.
* **Memory loss** — When team members leave, their institutional knowledge disappears with them.
* **Inefficient operations** — Consultants spend hours searching for existing work, past proposals, and project insights that already exist somewhere in the organization.
* **Poor knowledge reuse** — Teams can't leverage past projects to accelerate current client delivery.
## How Experio Works
Experio connects to your existing cloud storage, ingests and processes your documents using AI, and builds a knowledge graph that understands the relationships between your people, projects, clients, and expertise.
Link Box, Google Drive, or SharePoint accounts. Experio continuously scans for new and updated files.
Documents are ingested, parsed, and enriched with AI. Entities, relationships, and taxonomies are extracted and stored in a knowledge graph.
Team members interact with specialized AI assistants that search across all connected sources, cite their references, and provide context-aware answers.
## Key Capabilities
Multiple specialized assistants for different tasks — data exploration, knowledge transition planning, conflict of interest analysis, and more.
Automatic document processing with entity extraction, classification, and semantic search powered by vector embeddings.
Neo4j-powered graph that maps relationships between people, projects, clients, topics, and expertise across your organization.
Native connectors for Box, Google Drive, and SharePoint with OAuth authentication and real-time sync.
Every AI response includes source citations linked back to the original documents, so you can verify and trust the answers.
Share conversations, export responses as PDF, Word, or Markdown, and organize work into folders for team collaboration.
## Who Uses Experio
Experio serves two types of users:
| Role | What They Do |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| **End Users** | Consultants and team members who chat with AI assistants, search organizational knowledge, and export insights for client work. |
| **Administrators** | System administrators who configure data sources, manage the knowledge graph, set up AI models, and control user access. |
Learn how to chat with AI assistants, organize conversations, and export insights.
Set up data sources, configure the knowledge graph, manage users, and monitor the system.
# FalkorDB Browser (Local Development)
Source: https://docs.experio.cloud/local-development/falkordb-browser
Connect to the local FalkorDB instance using the bundled FalkorDB Browser UI
## Overview
The **FalkorDB Browser UI** is the visual query tool for the local FalkorDB graph database — the FalkorDB equivalent of the Neo4j Browser at `http://localhost:7474`. It is hosted by the devcontainer and lets you run ad-hoc Cypher queries against the local graph.
[BETA-272](https://github.com/Experio-AI/experio/pull/909) introduced FalkorDB as an alternate graph backend via the graph abstraction layer (see
[Graph backend](/admin-guide/graph-backend)). Both backends ship in the devcontainer for local development; production
deployments often default to Neo4j and treat FalkorDB as bring-your-own.
The browser is exposed at:
```
http://localhost:3005
```
## Connection settings
Open `http://localhost:3005` in your browser, then enter the following on the connection screen:
| Field | Value |
| ---------------------- | ----------------------------- |
| Host | `localhost` |
| Port | `6379` |
| Username | `default` |
| Password | `experio_falkor_dev_password` |
| TLS Secured Connection | unchecked |
Both the host port mapping and the password are defined in `.devcontainer/docker-compose.yml`:
```yaml theme={null}
falkordb:
image: falkordb/falkordb:latest
ports:
- 6380:6379 # Redis protocol (host:container)
- 3005:3000 # Browser UI (host:container)
environment:
- FALKORDB_ARGS=QUERY_MEM_CAPACITY 1073741824 TIMEOUT_DEFAULT 60000 --requirepass experio_falkor_dev_password
```
## Important: use port `6379`, not `6380`
When filling in the browser connection form, **use port `6379`, not `6380`**. This is the most common cause of "connection refused" when wiring up the browser for the first time.
The reason: the browser process runs **inside** the FalkorDB container, so it connects to the Redis protocol port that is internal to the container — which is `6379`. The host port mapping `6380:6379` in `.devcontainer/docker-compose.yml` exists only so processes running on the **host machine** (e.g. Django on macOS) can reach FalkorDB. The browser is already inside the container network, so it bypasses the host port mapping entirely.
```
┌─────────────────────────────────┐
│ FalkorDB container │
│ │
Host (macOS/Linux) │ ┌─────────────────────────┐ │
────────────────── 6380 │ │ Redis protocol :6379 │ │
redis-cli, Django ──────►│──►│ (internal) │ │
│ └─────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌─────────────────────────┐ │
Browser UI :3005 ──────│──►│ Browser :3000 │ │
────────────────── 3005 │ │ (uses host=localhost │ │
│ │ port=6379 inside) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
```
In short:
| Caller | Host | Port |
| ------------------------------- | ----------- | ------ |
| FalkorDB Browser (in container) | `localhost` | `6379` |
| `redis-cli` / Django (on host) | `localhost` | `6380` |
## Verifying the connection from the host
To confirm FalkorDB is up and accepting the password before you launch the browser, run from the host:
```bash theme={null}
redis-cli -h localhost -p 6380 -a experio_falkor_dev_password PING
```
Expected output:
```
PONG
```
You can also confirm the graph database engine is loaded:
```bash theme={null}
redis-cli -h localhost -p 6380 -a experio_falkor_dev_password GRAPH.LIST
```
## Switching the active graph provider
Experio chooses between Neo4j and FalkorDB at runtime via System Settings. To change the active provider, navigate to:
**Admin → Settings → System Settings → Database & Storage → Graph Database**
There you can toggle between providers and update connection credentials. See [System Settings](/admin-guide/system-settings) for the full list of `FALKOR_*` and `NEO4J_*` keys.
After pulling graph-backend or scale-to-zero changes onto a previously-seeded database, run `cd server && pipenv run python manage.py seed_config --force` once to refresh connection defaults (cluster `FALKOR_URI` is `redis://falkordb:6379`; devcontainer host access remains `redis://localhost:6380`). On Omnistrate, also run `seed_autoscaling_config` if `neo4j` / `falkordb` ServiceConfiguration rows are missing.
## When to use FalkorDB Browser vs System Settings
| Use case | Tool |
| ------------------------------------------------------------ | ---------------------------------------------- |
| Ad-hoc Cypher exploration of the graph | **FalkorDB Browser** (`http://localhost:3005`) |
| Inspecting nodes, relationships, indexes, vector indexes | **FalkorDB Browser** |
| Changing connection credentials (host, port, password, user) | **System Settings** |
| Switching the active graph backend (Neo4j ↔ FalkorDB) | **System Settings** |
The browser is read/write — you can run `MATCH`, `CREATE`, `MERGE`, and `DROP` queries — but credentials and provider selection live in System Settings.
## Troubleshooting
If the browser does not load or the connection fails:
* **Browser does not load at `http://localhost:3005`**
* Confirm the devcontainer is running: `docker compose -f .devcontainer/docker-compose.yml ps falkordb` should show `running (healthy)`.
* Confirm port `3005` is not already in use on the host: `lsof -i :3005`.
* **Connection refused / "ETIMEDOUT" from the browser form**
* Verify you used port `6379`, not `6380`. See the "Important" callout above.
* **Authentication failed**
* The password must match the `FALKORDB_ARGS=--requirepass …` value in `.devcontainer/docker-compose.yml`. If you changed it locally, restart the FalkorDB service: `docker compose -f .devcontainer/docker-compose.yml restart falkordb`.
* **Browser loads but `redis-cli` fails from the host**
* From the host you must use port `6380`, not `6379`. The host's `6379` is mapped to the separate `redis` (Redis Stack) service.
* **Connection drops or graph is empty after a restart**
* The graph is persisted to the `falkordb-data` Docker volume. If you ran `docker compose down -v`, the volume was removed; reseed the graph through the normal ingestion flow.
# AI Assistants
Source: https://docs.experio.cloud/user-guide/assistants
Understand the specialized AI assistants available in Experio
## What Are Assistants?
Assistants are specialized AI agents, each configured for a specific type of task. They differ in their system prompts, knowledge focus, available tools, and response style. Your organization's administrator configures which assistants are available and how they behave.
## Browsing Assistants
Navigate to the **Agents** page from the sidebar to see all available assistants. Each card shows:
* **Icon** — A visual identifier for the assistant
* **Title** — The assistant's name
* **Description** — A brief explanation of what the assistant does
You can **search** assistants by name or description and **sort** them alphabetically.
## Default Assistant Types
Experio includes several built-in assistant types that your administrator may have configured:
The primary assistant for exploring your organization's knowledge base. Ask questions about projects, clients, expertise, documents, and more. This assistant searches across all connected data sources and provides answers with source citations.
Specialized for organizational transitions and onboarding. Helps teams understand historical context, map expertise, identify best practices, and create knowledge transfer plans.
Focused on conflict of interest analysis. Identifies potential conflicts by analyzing relationships between people, organizations, projects, and contracts in your knowledge graph.
Your administrator may have configured additional assistants specific to your organization's needs. Some assistants may be restricted to staff members only.
## Choosing the Right Assistant
Select an assistant based on what you're trying to accomplish:
| Task | Recommended Assistant |
| --------------------------------------- | ----------------------------------- |
| Search for project details or past work | Talk to Your Data |
| Onboard a new team member | Knowledge Transition Plan |
| Check for potential conflicts | Organizational Conflict of Interest |
| General document exploration | Talk to Your Data |
## Starting a Conversation
Click any assistant card to start a new conversation. You'll see the assistant's **welcome message** with guidance on how to use it effectively. Each assistant carries its own **accent color** — on its card, its welcome page, and the badge shown next to its conversations — so you can recognize it at a glance.
Each conversation is tied to the assistant you selected. To switch assistants, start a new conversation from the Agents page or the Home page.
## Deep Agent Mode
Some assistants use **Deep Agent** mode, which enables multi-step reasoning. When active, you'll see detailed task progress showing the AI's thinking process:
1. **Planning** — The AI determines what steps to take
2. **Searching** — Querying your knowledge base and documents
3. **Analyzing** — Processing and synthesizing information
4. **Responding** — Generating the final answer with citations
This mode provides more thorough answers for complex questions but may take longer to respond.
# Chat Interface
Source: https://docs.experio.cloud/user-guide/chat-interface
How to interact with AI assistants, manage messages, and use advanced chat features
## Starting a Conversation
There are several ways to begin a new chat:
* **Home Page** — Type a message in the input box or click an assistant card.
* **Sidebar** — Click the **New Chat** button.
* **Agents Page** — Browse all assistants and click one to start a conversation.
* **Existing Conversation** — Continue any previous conversation from the sidebar or chats page.
When you select an assistant, you'll see its **welcome page** explaining what that assistant can help with. What else appears depends on how you got there:
* Starting a **new chat** (from the home page or the **New Chat** button) shows just the assistant's introduction and the message input — a clean slate.
* Opening the assistant from the **Agents page** also lists your **previous conversations** with that assistant, so you can resume one instead of starting over.
### Required Context
Some assistants are configured to require specific context before they will answer — for example, your company size, industry, or strategic focus. When this is the case, an **amber banner** on the welcome page lists what you need to provide.
Send your first message including that information. If anything is missing, the assistant replies with a short clarification asking for the missing pieces; once you provide them, the conversation continues normally and the requirement is not re-checked for the rest of that conversation.
To review or update the context you provided, click the **clipboard icon** next to the message input. The panel shows your original context along with any updates you've added, and lets you submit a new amendment from the same place.
## Sending Messages
Type your message in the input area at the bottom of the chat and press **Enter** to send (or **Shift+Enter** for a new line).
### Attaching Files
Click the **paperclip icon** to attach files to your message:
* Maximum file size: **25 MB** per file
* You can attach **multiple files** to a single message
* Remove files before sending by clicking the **X** on each attachment
* Supported formats include PDFs, Word documents, text files, and more
Each attachment appears **inside the message box**, above where you type, showing its
file-type icon, name and size — so you can see exactly what you're about to send.
After you send, the attachment **stays on your message** — click it to read the original
in the panel on the right, and download it from there if you need a copy. Files attached
before this was introduced still show on the message, but can't be re-opened: only their
extracted text was kept at the time.
The assistant works from the **text extracted** from an attachment, and only the first
**10,000 characters** of that text are stored. On a long document that means the assistant
is reading the opening section, not the whole file — the remainder is not kept. When an
attachment is cut short this way the assistant is told the document is partial and asked to
say so rather than summarizing it as though it were complete. Your original file is
unaffected: it stays on the message in full and you can still open and download it.
### Selecting a Model
If multiple AI models are available, use the **model selector** next to the input area to choose which model processes your message. Your selection persists for the current assistant session.
### Generating Documents
If document templates are configured, a **Generate button** (wand icon) appears next to the model selector. Click it to browse templates organized by category and generate presentations, formatted messages, and more. See [Document Templates](/admin-guide/document-templates#using-templates-in-chat) for full details.
## Reading Responses
AI responses stream in **real-time** — you'll see the text appear as it's generated.
When you ask a follow-up in a long conversation, your question moves to the **top of the
view and stays there** while the assistant works, so you can keep reading it alongside the
progress steps. The view follows the answer once it starts arriving.
### Task Progress
For complex queries, the AI may perform multiple steps (searching documents, analyzing data, synthesizing information). A **task progress indicator** shows:
* The current step being performed
* Completed steps with checkmarks
* Sub-steps and tool calls being made
The indicator updates as the assistant moves through the run, so a long answer shows
where it has actually got to rather than a single unchanging label. A typical question
moves through **Analyzing Question**, **Routing Query** (deciding how to handle it),
**Planning**, **Scope Analysis** (finding the data that answers it), then **Report
Generation** as the answer is written. Steps vary by question — a query that is answered
from what it already found will skip the retrieval step entirely.
Longer steps are normal on complex questions: scope analysis and query generation are
usually the slowest parts of a run. The label tells you which of them is in progress.
### Citations and Sources
When the AI references your organization's documents, you'll see **numbered citations** in the response. Click any citation to open the **Source Details Panel** on the right, which shows:
* File title and type
* Data source name (Box, Google Drive, SharePoint)
* File path and last modified date
* A content preview of the referenced section
* A direct link to the original file
Citations are filtered to your own access, like the answers themselves — you will only see a citation
for a record you are allowed to read. On very broad questions a correct answer can arrive with no
citations at all; see [Sources and citations](/user-guide/sources-and-citations#citations-and-your-access).
For **graph entities** cited in provenance, use **View lineage** on the entity card or open the graph
explorer to see how properties were extracted, imported, or enriched. See
[Graph Lineage](/admin-guide/graph-lineage).
### Artifacts
When the AI generates long-form content (reports, analysis, structured outputs), it opens automatically in the **Artifact Panel** on the right side. You can:
* **Maximize** the panel for full-width reading
* **Minimize** it back to a side panel
* **Close** it to return to the chat view
* **Export** the content as PDF, DOCX, or Markdown — click the download icon in the panel header.
* **Generate document** from the content — click the wand icon in the panel header and pick a template to have the agent produce a formatted file (DOCX, PPTX, or PDF). See [Document Templates](/admin-guide/document-templates).
* **Edit** the content inline with the built-in editor
You can also export from the message action toolbar on the chat response itself (see [Export & Sharing](/user-guide/export-and-sharing)). Generated files (DOCX, PPTX, PDF, CSV, and similar) use a **Download** button in the file panel to save the original file.
### Questions from the Assistant
Sometimes the assistant needs something from you before it can answer — which part of the business you're asking about, whether you want a summary or a full report, which of several close matches you actually meant. Rather than writing the question into its reply and leaving you to guess the format of an answer, it asks with a **question card** underneath the response.
A card carries a handful of questions — five at most by default, and an administrator can set a different limit per assistant — each with a few suggested answers shown as **selectable chips**:
* **Pick one or several.** Most questions accept more than one answer. A question whose options are mutually exclusive — *summary* versus *detailed report*, say — accepts only one, and picking a second replaces the first. The hint under each question tells you which it is.
* **Add your own.** Type in the **Add your own…** box and press **Enter** (or click **+**) to add an answer that isn't offered. Your additions appear as chips you can remove with their **X**. If what you type matches one of the options, that option is ticked instead of adding a duplicate.
* **Send answers** submits the whole card in one go. It stays disabled until every question has at least one answer.
What you send is posted as a normal message in the conversation — one line per question, in `Question: answer` form — so the exchange stays readable when you scroll back. The assistant then resumes from wherever the question came from: usually it goes back and retrieves data using what you told it, and occasionally — when the question was only about how to present an answer it already has — it simply rewrites the answer.
You don't have to use the card. The message box stays open, so you can **type your answer instead** and send it like any other message; the assistant reads it as the reply to what it asked. Either way, the card clears as soon as you send.
If you reload the page or come back to the conversation later, a question you haven't answered yet is **still waiting** — the card is restored with the conversation. It disappears if you stop or delete that turn.
The assistant won't keep asking indefinitely. After a couple of rounds of questions — two by default, configurable per assistant — it answers with what it has and explains in plain language what it could not pin down.
This is not the same as the **Required Context** banner described above. Gating is a one-off precondition checked on the first message of a conversation; a question card can appear on any turn, is answered inline, and asks only for what that particular request is missing.
### Recommendations
After each response, the AI may suggest **follow-up questions** based on the conversation context. Click any suggestion to send it as your next message.
Suggestions are held back while a question card is waiting for you — nothing appears under an open question but the card itself. They return once the assistant delivers its final answer.
## What the AI can see
Answers are limited to what **you** are allowed to see. When your organization has access control enabled for the knowledge graph, every question the assistant asks of the graph is filtered to your own access before any answer is written — so two people asking the same question can correctly get different answers.
You do not configure this and there is nothing to switch on. If an answer seems to be missing something you expected, it usually means that record is not linked to you, and an administrator can check with **Preview as user** on the Access Control screen.
**"You are seeing everything."**
Some administrator accounts read the knowledge graph **without** that filtering, so they can test and troubleshoot. When your session is one of them, a banner appears above the conversation saying so.
While it is showing, the answers you get are **not** the answers a normal user would get. Do not use that session to check what someone else has access to — use **Preview as user** instead, which answers as that person rather than as you.
## Agent Flows
Some requests are handled by an **agent flow** — a multi-step pipeline the assistant runs for you (for example, drafting a response to an RFP). There are two ways to start one:
* **Just ask.** When your request matches a flow, the assistant **confirms first** ("I can run *RFP Response Pipeline* — proceed?"). Once you approve, it launches the flow **in the background**, so you can keep chatting while it works.
* **Pick it from the composer.** Click the **Launch a flow** button (workflow icon) next to the message input to see the flows available for this conversation, each with a short description. Choosing one launches it right away — the pick is your go-ahead, so there's no separate confirmation. The composer prefills a short prompt you can extend with any details the flow needs before sending. The flow you picked shows as a **pill on the top edge of the message box**, so it's clear the next message will launch it; clear it with the pill's **X**.
Files you've attached to the conversation are routed into the flow automatically — including the file you attach to the message that starts it, so "here's the RFP, draft a response" works in one go. Only files **you uploaded** are picked up this way; a document an earlier run produced is never swept in as the next run's input unless you ask for it by name.
If the flow still needs a file you haven't shared, it doesn't start, and a message in the conversation names what to attach — by the input's readable name (for example **RFP document**), not an internal field name — and tells you to attach it and send your message again.
Asking the assistant to **continue a run that already stopped** doesn't do what it sounds like, and it now says so. No run can be picked up where it left off once it has ended — that includes one that **finished successfully**, not just one that failed or was cancelled. Running the flow again starts a **new run from the first step**, with no partial output carried over, no reading or revising of the earlier run's document, and every approval asked again. Before it launches anything, the assistant looks up what actually happened to your last run in this conversation, and tells you plainly that this is a fresh start rather than describing it as a continuation. If that earlier run is **still going**, it reports where the run has got to instead of starting a second one.
### Watching a flow run
When a flow launches, a **flow-run view opens automatically in the panel on the right** and follows the run live. It is deliberately spare — it exists to tell you the flow is working and to let you act when it needs you:
* A status badge showing where the run is — **Pending** just after you start it, **Running** while it works, and **Awaiting review** whenever there's a question or an approval waiting for you. It can read **Paused** for a moment while a pause is still making its way to the panel
* One line naming what it is working on right now, by the step's name: *Working on Draft the response*
* The **approval or question card** when the run pauses for you, so you can answer it without leaving the conversation (see [Approval Steps](#approval-steps)). Documents and files a card shows you open right there in the panel
* A **Stop** button to cancel a run that's still going
**When the run finishes, the panel closes** — unless you're reading a file the run produced, in which case it waits until you close that document rather than taking it away mid-read. Nothing is lost either way: the result and any files the flow produced arrive as a message in the conversation, and so do the questions you answered and the decisions you made — the conversation is the record.
You can close the panel at any time — the flow keeps running, and a **View progress** control appears above the message box to reopen the live view. While a flow is active, its conversation **pulses in the sidebar**, so you can tell at a glance that work is still in progress; the pulse clears once the run finishes. If a run is **waiting on you** (see [Approval Steps](#approval-steps)), the reopen control turns amber — **"Review needed — open to approve or reject"** — and the conversation's sidebar row shows an amber review badge instead of the pulse, so you can spot which conversation needs action from anywhere.
These sidebar signals keep up **live**, with no page refresh: the pulse appears as soon as a flow starts, gives way to the amber badge when a run pauses for you, returns to the pulse once you've answered and the flow carries on, and clears when the run finishes or you **Stop** it. This works both for the conversation you're reading and for conversations working in the background.
You can also ask the assistant for the **status or results** of a flow it launched at any time. When the flow finishes, a brief notification appears and its full result — along with any files it produced — is added as a **message in the conversation** on its own, without a manual refresh.
Files stay with the answer that produced them: each one is listed **inside its own message**, so running a flow twice in the same conversation gives you two answers with their own files rather than everything piling onto the newest one. Re-running a flow doesn't replace the earlier document either — when a run would write a file that another run already produced here, it keeps its own copy under a slightly different name. Filenames mentioned in the answer text are clickable too, and open the same reader.
A run can also stop because the service running it did — a deployment or a restart. A run stranded that way used to keep its **Running** status indefinitely, leaving the conversation looking permanently busy over work nothing was doing. Those runs are now **reconciled when the service starts up**: a run with nothing left to finish it is marked as failed, the sidebar pulse clears, and the conversation is free again. A run **parked at a review gate is never touched** — a question waiting on you survives a restart, along with the amber badge that tells you it's there.
Passing trouble no longer sinks a whole run either, though the cover has edges worth knowing. A momentary network problem in a step is retried once — never in the steps where an AI agent is doing the work itself, since re-running one of those could repeat something it had already done, and otherwise only while the run is working **on its own**. Once it has stopped to ask you something and picked up again from your answer, nothing in that stretch is retried, because re-running a step there could put a question you have already answered back in front of you. So in a flow that asks for your sign-off early, the work that comes after your sign-off is not covered by that retry.
A step that failed and was then genuinely redone no longer forces the whole run to be reported as failed either — as long as it really was redone: the **same step, over the same piece of work**, on a later pass. A *different* step covering similar ground later on doesn't clear it, and neither does anything else; a failure that nothing redid still fails the run.
### Asking about a document a flow produced
You can carry straight on in the same conversation — "summarise the response you just wrote", "what did it say about pricing?" — without re-attaching anything. The content of documents the flow produced is read into the conversation alongside the messages, so the assistant can answer from them directly.
What it gets **read in** that way is a **bounded excerpt**, not the full document. There are limits on how much of any single document, and how much across the conversation as a whole, is carried this way; the most recently produced document is served first, so the one you just asked about is the one that fits. Where a document was cut short, or an older one didn't fit at all, the assistant is told — so it can say the answer is based on part of the document rather than answering confidently from a fragment.
That excerpt is no longer all it has to go on. The assistant can also **list the documents flows have produced in this conversation, search inside one, and read a section of it at a time** — so a question about something deep in a long report is answerable even though only the opening was read in. Searching looks for an exact word or phrase and ignores capitalization; it is not a semantic search, so name a term you expect to actually appear in the text. Each document is checked against **your own access** before it is read, and only documents produced by flow runs in this conversation can be reached this way. You can still open any of them in the panel and read them yourself.
Flows run per conversation. A flow running in one conversation doesn't tie up the rest of the app — switch to another conversation and keep chatting while it works.
### Approval Steps
Some flows pause partway through for a person to sign off — for example, before a draft is finalized. These pauses don't interrupt your chat, and you don't have to go anywhere else to clear them: the question comes to the conversation. When a run pauses, a notification appears and the flow-run panel shows an **Awaiting review** card where you can **approve** or **reject** it right there, with a comment (optional when approving, required when rejecting). The question lands in the conversation too, as its own message — one per gate, naming the gate it belongs to and carrying a short excerpt of what's up for review; for a mid-run question, the question itself and any options it offers. A run that pauses on three gates at once posts three messages, so you can tell them apart. Long documents scroll inside the card, so the approve and reject buttons stay in reach. A flow can also **ask you a question** mid-run — type your answer into the card and send it to continue. Acting on the card resumes the flow and the card clears.
**You can change what you were shown before approving it.** Where the card presents a document or a block of text, an **Edit** button turns it into an editable field — reword a section, or edit a document's title and each of its sections — and then **Approve and continue**. The version you edited is the one that carries on to the rest of the flow, not the one you were originally shown. An edited card is marked **edited** while you work, and **Revert** puts the original back.
If a run pauses on **more than one question at once**, you don't have to wait between them. Answer them in whatever order you like: an answer you give while the flow is still busy with a previous one is **held and submitted automatically** as soon as the run is free, so you don't have to sit and retry. An answer can still be refused: if that question is no longer waiting for one — someone has already answered it in the **Agent Inbox**, or the run has moved on — a short message tells you so rather than leaving you wondering. On the rare occasion the run stays busy long enough that the automatic retries give up, the card comes back so you can answer it again. Answered cards clear as you go and don't come back — though a flow that revises its work can put the same gate up for review again with the new version, which arrives as a fresh card and a fresh message rather than the answered one returning; the ones you haven't answered stay on screen and stay usable. Each card is signed off on its own, carrying whatever you edited on it — there is no bulk sign-off, since an approval and a rejection each need their own note. Until the last question is answered, the conversation keeps its amber review badge in the sidebar.
Once you've acted, your decision is recorded in the conversation — which gate it was, whether you approved or rejected it, and any note you left. It is folded **into the question's own message**, under **Your response**, instead of arriving as a second message further down: scrolling back, you read the question and the answer it got as one entry, and the question drops its "answer it in the run panel" instruction, which your answer has already satisfied. If you edited before approving, the record says so and includes **what you approved**, so reading back through the conversation later shows the version that actually went through rather than the draft you were offered. A rejection with no reason given is recorded as such. Answers to a mid-run question are merged the same way, under the question they answered.
A long response — an edited draft you approved, say — is shown as a **preview** with a **View full response** button that opens the whole thing in the panel on the right.
There is one deliberate exception. If you **sent a chat message of your own** in between — asking something else while the run was parked — your answer stays as a separate message below it rather than folding back up into the question. Nothing you answered later ever appears above something you said earlier.
The same pending review is also available to admins in the **Agent Inbox**, but you don't need it — either path resumes the flow, which then posts its outcome back into the conversation.
## Message Actions
Hover over any message to access its action toolbar.
### AI response toolbar
| Action | Description |
| ------------ | ----------------------------------------------------------------- |
| **Share** | Open the share dialog for this conversation |
| **Retry** | Regenerate this response from the previous user message |
| **Copy** | Copy the response content to your clipboard |
| **Edit** | Open the response in the Artifact Panel for editing |
| **Feedback** | Rate the response with thumbs up/down and add an optional comment |
| **More** (⋯) | Export, fork, and delete options (see below) |
Open **More** (⋯) on an AI response for additional actions:
| Action | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Export to PDF** | Download the response as a formatted PDF |
| **Export to Markdown** | Download as a Markdown file |
| **Export to DOCX** | Download as a Word document |
| **Fork from here** | Branch a new conversation from this response only (editors only). To copy the whole thread, use **Fork** on the conversation menu instead — see [Forking Conversations](/user-guide/conversations#forking-conversations). |
| **Delete** | Remove the last message turn (your message and the AI response) |
### User message toolbar
| Action | Description |
| -------- | ------------------------------------------------------------- |
| **Edit** | Edit your last user message (the AI regenerates its response) |
## Stopping a Response
If the AI is generating a response you no longer need, click the **Stop** button that appears during streaming. The response will stop at wherever it has reached.
## Editing Messages
Click the **edit icon** on your last message to modify it. The AI will regenerate its response based on your updated message. This is useful when you want to refine your question without starting a new conversation.
## Conversation Management
### Renaming
Click the conversation title in the header to edit it. The new name saves automatically when you click away or press Enter. Press Escape to cancel.
### Forking
To copy the whole conversation, open the three-dot menu in the header (or on the row in the sidebar, chats list, or a folder) and select **Fork**. To branch from a specific AI reply, use **Fork from here** on that message. See [Forking Conversations](/user-guide/conversations#forking-conversations).
### Sharing
Click the **Share** button in the conversation header to share with other users:
1. Search for users by name or email
2. Assign a role: **Viewer** (read-only) or **Editor** (can contribute)
3. Manage existing shares — change roles or remove access
### Deleting
Use the conversation menu (three dots) to delete a conversation. A confirmation dialog will appear before deletion.
## Filters
Some assistants support **response filters** that let you narrow results by category or type. When available, filter options appear above the message input. Click a filter to apply it to your next message.
Like follow-up suggestions, filters are hidden while the assistant is waiting on a [question card](#questions-from-the-assistant).
# Conversations
Source: https://docs.experio.cloud/user-guide/conversations
Managing your chat conversations in Experio
## Viewing Conversations
### From the Sidebar
Your **20 most recent** conversations appear in the **Recent Chats** section of the sidebar, ordered by most recent activity — a conversation you just replied to moves to the top. This is the same order as **Recent** on the Chats page. [Forked conversations](#forking-conversations) nest under their parent in this list. Click any conversation to resume it. If a title is cut off, **hover over the row** — the title slides sideways to reveal the rest. Use the **Chats** page to browse conversations beyond the most recent ones.
The **search box** above the list isn't limited to those 20 rows — typing in it searches **all** your conversations and replaces the list with the matches, though the sidebar still shows only the first 20 of them, so open the **Chats** page to scroll through the rest. See [Searching conversations](#searching-conversations) for what it matches.
#### Activity indicators
A conversation's row in the sidebar can show a small status indicator so you can track work across conversations at a glance:
* **Responding pulse** — appears while an assistant is generating a reply in that conversation.
* **Unread dot** — when a reply finishes in a conversation you're **not** currently viewing, the pulse clears and a dot appears. Opening the conversation clears it.
* **Flow running** — a pulsing **workflow icon** (the same icon as **Launch a flow**) means an [agent flow](/user-guide/chat-interface#agent-flows) is still working in that conversation. It clears when the run finishes or you stop it. A run left stranded by a restart of the service is [reconciled automatically](/user-guide/chat-interface#watching-a-flow-run), so the icon doesn't keep pulsing over work nothing is doing.
* **Review badge** — an amber badge means the conversation is waiting for you to **approve or reject** something before it can continue — either a flow [approval step](/user-guide/chat-interface#approval-steps) or an [external-action approval](/user-guide/integrations#write-approval). If a flow pauses on **several questions at once**, the badge stays lit until you've answered the last one.
These indicators keep up **live**, with no page refresh — including for conversations you're not currently viewing.
Switching between conversations never interrupts a reply that's still generating — each conversation keeps working independently, and you can return to watch it finish.
### From the Chats Page
Navigate to **Chats** from the sidebar to see all your conversations — across every assistant — in a paginated list. Each entry shows:
* Conversation title, with a colored **assistant badge** showing which assistant the conversation belongs to
* Last message preview
* Last updated time
Clicking a conversation opens it with its own assistant. Forks nest under their parent here the same way they do in the sidebar.
### Filtering and Search
On the Chats page, you can:
* **Search** — Type in the search box to find conversations (see [Searching conversations](#searching-conversations)).
* **Sort** — Choose between **Recent** (newest first) or **Oldest** (oldest first).
The page uses **infinite scroll** — more conversations load automatically as you scroll down.
## Searching conversations
Search is available in two places — the box above **Recent Chats** in the sidebar, and the one on the **Chats** page. Both work the same way, and both search across **all** your conversations rather than only the ones currently on screen.
A conversation matches when your search text appears in either:
* its **title**, or
* **a message someone typed** in it.
Matching on message content means you can find a conversation by a phrase you remember typing, which is often easier to recall than an auto-generated title.
Messages people typed are searched, not the assistant's replies — including a colleague's messages in a conversation shared with you. Searching for a phrase the assistant wrote won't find the conversation — search for what was asked instead.
Results update shortly after you stop typing. Search only ever narrows what you can already see: conversations you don't have access to are never returned.
## Creating Conversations
New conversations are created automatically when you:
* Send a message from the **Home Page**
* Click an assistant on the **Agents** page
* Use the **New Chat** button in the sidebar
Each conversation is assigned to the assistant you were interacting with when it was created.
## Renaming Conversations
There are two ways to rename a conversation:
1. **In the header** — Click the conversation title at the top of the chat to edit it inline.
2. **From the menu** — Click the three-dot menu on a conversation in the sidebar or chats list, then select **Rename**.
## Sharing Conversations
Share a conversation with colleagues to collaborate or provide visibility:
1. Open the conversation you want to share
2. Click the **Share** button in the header
3. Search for users by name or email
4. Select a role for each user:
* **Viewer** — Can read the conversation but not add messages
* **Editor** — Can read and contribute messages
5. Click **Share** to confirm
A Viewer still sees the message box — sending is what's refused. The conversation answers **You don't have permission to write to this conversation** instead of a reply, and neither the message nor that notice is saved. Editing a message or retrying an answer is refused the same way, and a Viewer's thumbs-up or thumbs-down on a reply is never recorded.
To manage existing shares, open the share dialog to see current recipients. You can change roles or remove access.
## Forking Conversations
Fork a conversation to explore a different direction without changing the original thread. You must have **Editor** access (or be the owner). Viewers cannot fork shared conversations.
There are two ways to fork. Both are labeled **Fork**.
### Copy the whole conversation
Use this when you want a full copy of the current thread — the same messages and assistant state as the latest AI reply.
1. Open the conversation, or find it in **Recent Chats**, on the **Chats** page, or in a folder
2. Click the three-dot menu on the conversation (header, sidebar row, chats list, or folder list)
3. Select **Fork**
The conversation must already have at least one AI reply.
### Branch from a specific reply
Use this when you want to continue from an earlier AI response and leave later messages behind.
1. Open the conversation
2. Hover over the **AI response** where you want to branch
3. Click **More** (⋯), then **Fork from here**
**Fork from here** is only available on **AI responses**, not on user messages. It is disabled while the assistant is still generating a response.
### What happens when you fork
* A **new conversation** is created with the same assistant and folder as the original
* **Fork** from a menu copies every message through the latest AI reply
* **Fork from here** copies messages only through the selected AI reply
* The **original conversation** is unchanged — you can keep chatting in both independently
* You are taken to the new conversation automatically
* A **Forked from** banner at the top links back to the parent conversation
The fork title is prefixed with `Fork:` followed by the original conversation name.
### How forks appear in lists
In **Recent Chats**, on the **Chats** page, and in folder conversation lists, a fork sits **under its parent** (indented, with a branch icon). When the list is sorted by **Recent**, activity in the parent or any of its forks moves **the whole group** together — a busy fork does not leave its parent behind. **Oldest** sorts each conversation on its own.
## Deleting Conversations
1. Click the three-dot menu on any conversation (in the sidebar or chats list)
2. Select **Delete**
3. Confirm the deletion in the dialog
Deleting a conversation permanently removes it and all its messages. This action cannot be undone.
## Adding Conversations to Folders
Organize conversations by assigning them to folders:
1. Click the three-dot menu on a conversation
2. Select **Add to Folder**
3. Check one or more folders to assign the conversation to
4. Optionally create a new folder inline by clicking **Create New Folder**
A conversation can belong to **multiple folders** simultaneously. See [Folders](/user-guide/folders) for more on organizing with folders.
# Export & Sharing
Source: https://docs.experio.cloud/user-guide/export-and-sharing
Export conversations and share insights with your team
## Exporting Responses
Experio supports exporting AI responses in multiple formats for use in client deliverables, reports, or documentation.
### Export Formats
| Format | Description | Best For |
| --------------- | ----------------------------------------- | ------------------------------------- |
| **PDF** | Formatted document with preserved styling | Client deliverables, formal reports |
| **Markdown** | Plain text with formatting syntax | Documentation, knowledge bases, wikis |
| **Word (DOCX)** | Editable Word document | Collaborative editing, proposals |
### How to Export
1. Hover over any AI response to reveal the action toolbar
2. Click **More** (⋯)
3. Choose your format under the **Export** section:
* **PDF** for PDF export
* **Markdown** for Markdown export
* **DOCX** for Word export
4. The file downloads automatically with an auto-generated filename
The same PDF / DOCX / Markdown options are available from the **Artifact Panel** for long responses and in-chat documents: click the download icon in the panel header.
To have the agent produce a templated file (proposal, case study, report) instead of exporting the current text, use **Generate document** (wand icon) in the panel header. Picking a template asks the agent to produce a real formatted file (DOCX, PPTX, or PDF) that is delivered back into the chat — see [Document Templates](/admin-guide/document-templates) for how templates are configured.
When the agent has already written a file, open it in the panel and use **Download** to save the original bytes.
## Sharing Conversations
Share entire conversations with colleagues for collaboration or visibility.
### How to Share
1. Open the conversation you want to share
2. Click the **Share** button in the conversation header
3. Search for users by name or email
4. Assign a role:
* **Viewer** — Read-only access to the conversation
* **Editor** — Can read and add messages to the conversation
5. Click **Share** to confirm
### Managing Shared Access
From the share dialog, you can:
* **View current recipients** — See who the conversation is shared with
* **Change roles** — Switch a user between Viewer and Editor
* **Remove access** — Revoke a user's access to the conversation
## Copying Content
For quick sharing, use the **Copy** button on any AI response to copy its content to your clipboard. The content is copied in plain text format, preserving the response structure.
# Folders
Source: https://docs.experio.cloud/user-guide/folders
Organize your conversations into folders for easy access
## Overview
Folders let you group related conversations together. Use them to organize by project, client, topic, or any structure that fits your workflow. A conversation can belong to multiple folders.
## Browsing Folders
Navigate to **Folders** from the sidebar to see all your folders. The page shows:
* Folder name
* Number of conversations in each folder
* Last updated date
Use the **search bar** to filter folders by name, and **sort** by newest or oldest.
## Creating a Folder
1. Navigate to the **Folders** page
2. Click the **Create Folder** button
3. Enter a name for the folder
4. Click **Create**
You can also create folders inline when assigning a conversation to a folder — click **Create New Folder** in the assignment dialog.
## Viewing Folder Contents
Click any folder to see all conversations inside it. The folder detail view provides:
* A list of all conversations in the folder, each with a colored **assistant badge** showing which assistant it belongs to
* Search and sort controls
* Direct navigation to any conversation — each opens with its own assistant
* [Forked conversations](/user-guide/conversations#forking-conversations) stay in the same folder as the original and nest under their parent in the list
## Managing Folders
### Renaming
1. Click the three-dot menu on a folder
2. Select **Rename**
3. Enter the new name
4. Click **Save**
### Deleting
1. Click the three-dot menu on a folder
2. Select **Delete**
3. Confirm the deletion
Deleting a folder does **not** delete the conversations inside it. The conversations remain accessible from the Chats page and any other folders they belong to.
## Sidebar Folders
Folders also appear in the sidebar for quick access. Click a folder in the sidebar to expand it and see its conversations. Forks nest under their parent there, and the conversation menu includes **Fork**. Use the **Show More** link to navigate to the full folder view.
# Getting Started
Source: https://docs.experio.cloud/user-guide/getting-started
Log in to Experio and start exploring your organization's knowledge
## Logging In
Navigate to your organization's Experio URL and sign in with your credentials.
* **Email and password** — Enter your work email and password.
* **Single Sign-On (SSO)** — If your organization uses Okta, Microsoft Entra, or Auth0, click the SSO option and authenticate through your identity provider.
After logging in, you'll land on the **Home Page**.
## Your First Visit
When you log in for the first time, Experio greets you and presents the key areas of the platform:
1. **Message Input** — Type a question directly from the home page to start chatting with the default assistant, or click one of the **example prompts** beneath it to see what Experio can do.
2. **Choose an Assistant** — A row of available AI assistants, each specialized for a different task and shown with its own accent color. Select one to start a new conversation; click **Show more** to see the full list.
3. **Quick Actions** — Shortcuts to browse folders, view recent chats, or continue your last conversation.
4. **Jump Back In** — Once you have conversations, your most recent ones appear as quick pills at the bottom of the page.
## Core Concepts
Before diving in, here are the key concepts in Experio:
AI agents configured for specific tasks. Each assistant has its own personality, knowledge focus, and capabilities. For example, one assistant might specialize in exploring your project data, while another focuses on knowledge transition planning.
Individual chat threads between you and an assistant. Each conversation maintains its own context and history. You can rename, share, and organize conversations.
Organizational containers for grouping related conversations. Create folders by project, client, topic, or any structure that works for your workflow.
When an assistant references a document, it provides a citation you can click to view the original source. This ensures every answer is traceable back to your organization's data.
Long-form content generated by the AI — reports, analysis documents, or structured outputs — displayed in a dedicated side panel for easy reading and export.
## Navigating the Interface
The Experio interface has three main areas:
### Sidebar (Left)
The collapsible sidebar provides quick access to:
* **New Chat** — Start a fresh conversation
* **Chats** — View all your conversations
* **Folders** — Browse your organized folders
* **Agents** — See all available AI assistants
* **Recent Chats** — Quick links to your latest conversations
* **Theme Toggle** — Switch between light, dark, and system themes
* **User Menu** — Access your profile and log out
### Main Content Area (Center)
This is where conversations happen. Depending on where you navigate, you'll see:
* The home page with assistant selection
* A chat interface with message history
* A listing of conversations or folders
### Side Panel (Right)
When you click a citation or the AI generates a long artifact, a resizable panel opens on the right showing source details or generated content.
## Next Steps
Learn how to interact with AI assistants and get the most from your conversations.
Understand the different assistants available and when to use each one.
# Integrations
Source: https://docs.experio.cloud/user-guide/integrations
Connect external accounts to extend what Experio's AI assistants can access
## Overview
Integrations let you connect external accounts — such as Google Workspace — directly to Experio. Once connected, AI assistants can access your emails, calendar events, documents, and files on your behalf during conversations.
Navigate to **Integrations** from the sidebar or visit `/integrations`.
## Google Workspace
The Google Workspace integration gives Experio's AI assistants read-only access to your:
* **Gmail** — Search and read email messages
* **Google Drive** — List and search files
* **Google Docs** — Read document content
* **Google Calendar** — View upcoming events and schedules
* **Google Sheets** — Read spreadsheet data
### Connecting Your Account
Click **Integrations** in the sidebar to open the integrations page.
Find the **Google Workspace** card and click **Connect**.
A Google sign-in window opens. Choose the Google account you want to connect and grant the requested permissions.
Once connected, the card shows a **Connected** status. You can now ask assistants about your Gmail, Drive, Calendar, and Docs in any conversation.
### What You Can Ask
Once connected, try messages like:
* "List my recent emails"
* "Search my Drive for the Q4 budget spreadsheet"
* "What meetings do I have this week?"
* "Summarize the contents of my latest Google Doc"
The assistant automatically uses your Google Workspace connection when your question involves email, files, or calendar data.
If you have not connected your Google account and ask about Gmail, Drive, or Calendar, the assistant will direct you to the Integrations page to set up the connection.
### Disconnecting
To disconnect your Google Workspace account:
1. Go to **Integrations**
2. Click **Disconnect** on the Google Workspace card
3. Confirm the disconnection
After disconnecting, assistants will no longer be able to access your Google data. You can reconnect at any time.
## Slack
Connect your Slack workspace to let assistants search channels, read message history, and (with your
approval) post messages.
### Connecting
1. Open **Integrations** (`/integrations`)
2. Click **Connect** on the Slack card
3. Authorize the requested scopes in Slack
4. The card shows **Connected** when complete
### What You Can Ask
* "Search Slack for messages about the product launch"
* "What was discussed in #experio-dev this week?"
* "Post a summary to #general" (requires write approval — see below)
## Write approval
When an assistant needs to **change** something in a connected system (post a Slack message,
create a HubSpot record, etc.), Experio pauses and asks you to approve or decline.
The approval prompt shows:
* A **plain-language summary** of each action (e.g. sending a message in a specific channel)
* **Raw request** data you can expand for full detail
Click **Approve writes** to run the actions, or **Decline** to skip them. The assistant continues
after your decision, and the prompt clears once you act.
If the request comes up in a conversation you're not currently viewing, an **Approval needed**
notification appears and that conversation's row in the sidebar shows an amber **review badge**, so
you can find it and act. Open the conversation to see the approval prompt inline.
Organization-wide integrations (e.g. HubSpot) use the same approval flow for write tools.
## Privacy and Security
* **Read-only access** — Experio only reads your data. It cannot send emails, create files, or modify calendar events.
* **Encrypted tokens** — Your OAuth tokens are encrypted at rest and never exposed in chat responses.
* **No permanent storage** — Data retrieved from your Google account is used to answer your question and is not permanently stored in Experio's knowledge base.
* **Per-user connections** — Each user connects their own account. Your connection is not shared with other users.
* **Revocable** — You can disconnect at any time from the Integrations page, or revoke access from your [Google Account permissions](https://myaccount.google.com/permissions).
# MCP Server
Source: https://docs.experio.cloud/user-guide/mcp-server
Connect external AI tools to Experio's knowledge base via the Model Context Protocol
## Overview
The MCP Server feature lets you connect external AI tools — such as **Claude Desktop**, **Cursor**, **VS Code Copilot**, and other MCP-compatible clients — directly to Experio. Once connected, these tools can query your organization's knowledge graph, search documents, and generate reports through Experio's AI assistants.
Navigate to **MCP Server** from the user dropdown menu in the sidebar.
## Getting Started
Click your avatar in the sidebar footer, then select **MCP Server** from the dropdown.
Click **Generate API Key**. A dialog shows your key (starting with `exp_mc_`). Copy it immediately — the key is shown only once and cannot be retrieved later.
After generating a key, the page displays a JSON configuration block. Click **Copy Config** to copy it to your clipboard.
Open your AI tool's MCP configuration file and paste the JSON. For example:
* **Claude Desktop**: Edit `claude_desktop_config.json`
* **Cursor**: Edit MCP settings in Cursor preferences
* **VS Code**: Edit your MCP settings file
Replace `` in the config with the API key you copied.
Restart your AI tool. You should now see Experio's tools available. Ask your AI tool to query Experio's knowledge base.
## MCP Configuration Format
The configuration uses the `mcp-remote` transport, which works with all MCP clients:
```json theme={null}
{
"mcpServers": {
"experio": {
"command": "npx",
"args": [
"mcp-remote",
"https://your-server.com/mcp",
"--header",
"Authorization: Bearer "
]
}
}
}
```
The `mcp-remote` package is installed automatically via `npx`. Make sure you have Node.js installed on your machine.
## Available Tools
Once connected, your AI tool has access to three Experio tools:
| Tool | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **list\_assistants** | Discover available AI assistants and their capabilities |
| **ask\_experio** | Ask a question to an Experio assistant. Queries the knowledge graph, searches documents, and generates a report. |
| **get\_file** | Download a file generated by ask\_experio (CSV exports, presentations, documents) |
### Typical Workflow
1. Call `list_assistants` to see which assistants are available
2. Call `ask_experio` with your question, assistant name, and an optional `thread_id`
3. If the response mentions a file (e.g., `/results/export.csv`), call `get_file` with the same `thread_id` to get a download URL
### Example Queries
Once connected, try asking your AI tool:
* "Use Experio to list all departments in the organization"
* "Ask Experio to export all employees as a CSV"
* "Query Experio about Cloud division employees and their skills"
* "Use Experio to create a PowerPoint about the organizational structure"
## Managing API Keys
### Viewing Keys
The MCP Server page shows all your active API keys with:
* Key prefix (first 16 characters for identification)
* Name (optional label you set when creating)
* Created date
* Last used date
The full API key is never displayed after creation. Only the prefix is shown for identification purposes.
### Revoking Keys
To revoke an API key:
1. Click the trash icon next to the key
2. Confirm the revocation in the dialog
After revoking, any AI tool using that key will immediately lose access. You can generate a new key at any time.
### Multiple Keys
You can generate multiple API keys — for example, one for Claude Desktop and another for Cursor. Each key can be revoked independently.
## File Downloads
When Experio generates files (CSV exports, PowerPoint presentations, Word documents, PDFs), the `get_file` tool returns a **signed download URL**. These URLs:
* Expire after **10 minutes**
* Require no additional authentication (the URL itself is the authorization)
* Work in any browser or HTTP client
## Privacy and Security
* **Per-user API keys** — Each key is tied to your account. Other users cannot use your key.
* **Hashed storage** — API keys are stored as SHA-256 hashes. Even database administrators cannot see your raw key.
* **Revocable** — Keys can be revoked instantly from the MCP Server page.
* **Scoped access** — MCP tools respect the same data permissions as the web interface.
* **Signed file URLs** — Download links are cryptographically signed and time-limited.
# Settings & Preferences
Source: https://docs.experio.cloud/user-guide/settings-and-preferences
Customize your Experio experience
## Theme
Experio supports three theme options:
| Theme | Description |
| ---------- | ---------------------------------------------------------------- |
| **Light** | Bright interface with light backgrounds |
| **Dark** | Dark interface that reduces eye strain in low-light environments |
| **System** | Automatically matches your operating system's theme setting |
Toggle between themes using the **theme button** in the sidebar footer. Each click cycles through Light, Dark, and System modes.
Your theme preference is saved and persists across sessions.
## Profile & Account
Access your profile from the **user menu** in the bottom-left corner of the sidebar. From here you can:
* View your name and email address
* Access profile settings
* **Log out** — Ends your session with a confirmation dialog
### Persona profile
When your administrator enables personas, you can describe who you are so answers fit your role.
See [Personas — admin guide](/admin-guide/personas) for how administrators configure this feature.
From the profile drawer (when personas are enabled):
* **About you** — Free text always saved, even if you choose "No persona"
* **Persona cards** — Optional roles (for example CEO, Freelancer) with structured follow-up questions
* **Save** — Sends name, email, free text, and gating answers to the report writer when a persona is selected
If login requires a persona, a **Select your persona** dialog appears until you complete selection.
In chat, the **Use persona** toggle (person icon in the composer) controls whether that conversation
uses structured persona gating. Your name, email, and free text still apply when it is off. The header
badge shows the active persona when persona mode is on for that chat.
## Model Selection
When multiple AI models are available, you can choose which model processes your messages using the **model selector** in the chat input area.
* Your model selection is saved **per assistant** for the current session
* If you don't select a model, the assistant uses its configured default
* Not all assistants may offer model selection — this depends on administrator configuration
## Sidebar
The sidebar can be **collapsed** to an icon-only view for more screen space. Click the sidebar toggle button in the header to collapse or expand it.
In collapsed mode:
* Navigation items show as icons only
* Recent chats and folders are hidden
* Click the toggle again to expand
## Keyboard Shortcuts
| Shortcut | Action |
| ----------------- | -------------------- |
| **Enter** | Send message |
| **Shift + Enter** | New line in message |
| **Escape** | Cancel title editing |
# Sources & Citations
Source: https://docs.experio.cloud/user-guide/sources-and-citations
Understanding how Experio references your organization's documents
## How Citations Work
When an AI assistant references information from your organization's documents, it includes **numbered citations** in its response. These citations provide traceability — you can verify every claim by checking the original source.
Citations appear as clickable numbers (e.g., \[1], \[2]) within the AI's response text.
## Viewing Source Details
Click any citation number to open the **Source Details Panel** on the right side of the screen. The panel displays:
| Field | Description |
| ------------------- | ------------------------------------------------------------------- |
| **Title** | The document filename |
| **Type** | File format badge (PDF, DOCX, etc.) |
| **Data Source** | Which connected source it came from (Box, Google Drive, SharePoint) |
| **File ID** | Unique identifier in the source system |
| **File Path** | Location within the data source |
| **URL** | Direct link to the original file |
| **Last Modified** | When the document was last updated |
| **Content Preview** | The relevant text passage that was cited |
| **Metadata** | Additional key-value pairs extracted during ingestion |
## Source Types
Sources can come from any connected data source:
* **Box** — Documents stored in your organization's Box account
* **Google Drive** — Files from connected Google Drive folders
* **SharePoint** — Documents from SharePoint sites and libraries
* **File Upload** — Files uploaded directly to a conversation
## Trusting AI Responses
Experio's citation system helps you:
* **Verify answers** — Click through to the original document to confirm the AI's interpretation.
* **Find more context** — The source preview shows surrounding content that may provide additional detail.
* **Track data freshness** — The "Last Modified" date tells you how current the information is.
* **Access originals** — Use the direct URL link to open the file in its native application (Box, Google Drive, SharePoint).
## Citations and your access
Citations are filtered to your own access, exactly like the answer itself. You will only ever see a
citation for a record you are allowed to read, and the entity list in the **provenance panel** is
filtered the same way.
One consequence is worth knowing, because it looks like a fault and is not one: **a correct answer
can arrive with no citation chips at all.** This happens on very broad questions — "list our
companies" against several hundred — when you are entitled to a small share of them. The figures in
the answer are still filtered to your access; only the chips are missing. Asking something narrower,
about a named company or engagement, brings them back.
If citations disappear on *every* question, including narrow ones, that is worth reporting to an
administrator — see [Graph access control](/admin-guide/graph-access-control#citations-and-why-a-scoped-answer-can-have-none).
## Graph entity provenance
When the AI cites a **knowledge graph entity** (not just a document), you can trace how that entity's
data was created or updated:
* In the **provenance panel**, use **View lineage** on an entity card to open its lineage history
* In the **graph explorer**, select a node and review the **Graph Lineage** timeline
Lineage shows document ingestion, structured imports, enrichment rules, and manual review actions —
including which properties changed and which LLM model was used when applicable.
See [Graph Lineage](/admin-guide/graph-lineage) for a full overview.
If an AI response doesn't include citations, it may be drawing on general knowledge rather than your organization's specific documents. Ask the assistant to "cite sources" or "show references" to encourage document-grounded answers.