# Approval gates for AI agents writing to a database
Source: https://docs.datris.ai/agent-policy
Decide per action whether an agent runs it unattended, waits for a person to approve it, or is refused. Enforced by the platform on every MCP client and API key, not by a prompt.
The agent policy is the boundary between agents being useful and agents being safe. For every kind of change an agent can make through Datris — create a tap, run it, delete a pipeline, migrate a destination table's column types — an administrator decides one of three things:
| Mode | What happens when an agent tries it |
| - | - |
| **Auto** | It runs, exactly as today. |
| **Approve** | It does **not** run. The request is parked, the agent is told it is waiting for approval, and a person approves or rejects it under **Activity → Approvals**. On approval the platform performs the original request on the approver's behalf. |
| **Deny** | It is refused with a clear error the agent can read. |
Anything you have not set is **Auto**, so turning the feature on changes nothing until you set a rule. People using the Datris UI are never gated — they are the approvers.
This is infrastructure, not a system prompt. An agent cannot talk its way past it, and it applies to every client — the in-platform Assistant, Claude Desktop, Cursor, the CLI, or any script holding an API key.
## Enabling it
Off by default — existing installs are unchanged until you opt in.
1. Add the flag to your `.env`:
```bash theme={null}
USE_AGENT_POLICY=true
```
2. Recreate the Datris container:
```bash theme={null}
docker compose up -d --force-recreate datris
```
3. Open **Configuration → Agent Policy** (admins only when user authentication is on). Set the actions you care about, or click **Use recommended** for a starting point that pauses deletes, job kills and destination-type migrations and refuses secret, code-repository and platform-configuration writes (`secret:write`, `code-repo:write`, `config:write`).
The policy applies within a few seconds of saving; no restart.
## What counts as an agent
| Caller | Gated? |
| - | - |
| A person clicking in the Datris UI | No |
| The in-platform Assistant, Ops or Catalog chat acting for a logged-in user | **Yes** — attributed to the user, but it is still the agent proposing |
| The Configuration assistant, after you click Confirm | No — you are the approver |
| An external MCP client (Claude Desktop, Cursor, Claude Code, …) or a script with an API key | **Yes** |
| A scheduled tap run, Kafka or object-store trigger | No — these are the platform's own work |
| A tap script reading platform data during a run | No |
## Actions
The actions you can set are exactly the ones the [capability model](/api-keys) already recognizes — `pipeline:create`, `tap:delete`, `job:kill`, `secret:write`, and so on — plus two refinements: `pipeline:update:dest-types`, the destination column-type migration, which shares a capability with an ordinary pipeline save but rewrites a landed table, and `tap:run:enable-ordered`, which enables and runs every tap of a catalog in dependency order as one job (one approval for the whole job). A sub-action inherits its parent's mode unless set; `resource:*` sets every action on a resource.
Reads, queries, searches and metadata lookups are never gated. What a key may read is governed by its [capabilities](/api-keys).
Two rules are not part of the policy and cannot be changed by it: an agent can never edit the policy, and an agent can never approve or reject an approval — whatever capabilities its key holds.
### Per-resource overrides
An override tightens the policy for one pipeline or tap: `tap:prices → tap:run = approve` pauses runs of that one tap while every other tap runs freely. Overrides can only make an action stricter, never looser.
## Approvals
When an agent's request hits an **Approve** rule, the agent receives a `pending_approval` response with an approval id instead of a result. The Assistant will tell you plainly that the action is waiting, and it can poll for the decision. The request itself — method, path and body, with secret-looking values masked — is shown on the approval card so you see exactly what will run.
Approve, and the platform performs the original request, attributed to you. The audit log then holds two linked entries: the agent's queued request (with the reason it gave, if any) and your approval executing it. Reject, and the agent is told so on its next poll. Approvals not decided within the configured time (24 hours by default) expire on their own.
An approval executes what was proposed, not what the agent would propose now: if the pipeline or tap changed in the meantime, the approval is refused as stale and the agent has to propose again.
Some requests cannot be parked — file uploads, and bodies over 256 KB. If such an action is set to **Approve**, the agent is told to ask you to do it in the UI instead.
## For agents
Three MCP tools support the flow:
| Tool | Purpose |
| - | - |
| `get_agent_policy` | Read the policy before acting, to know whether a delete or migration will run or queue |
| `list_pending_approvals` | The actions this agent has queued, with their state |
| `get_approval` | Poll one approval by id until it is executed, rejected or expired |
Every mutating tool also accepts an optional `reason` — one line stating why the agent wants the change. It is stored in the [audit log](/audit-log) and shown on the approval card.
## Limits
| Setting | Default | Meaning |
| - | - | - |
| Pending approval TTL | 24 hours | Undecided approvals expire after this |
| Max pending per agent | 50 | An agent with this many undecided approvals is told to wait |
## Recovery mode
The policy document also carries the [recovery agent](/incidents)'s dial — `recovery.mode` (`off` / `propose` / `autopilot`) and its limits — plus per-resource `recovery` overrides. The recovery agent's every action passes through this same policy gate: under `propose`, everything it wants to do queues here for your approval.
## Related
* [API Keys](/api-keys) — what a key *may* do; the policy decides what it may do *unattended*
* [Audit Log](/audit-log) — where queued requests, decisions and executions are recorded
# Give your coding agent the Datris skill
Source: https://docs.datris.ai/agent-skill
Install the datris-platform skill in a project so Claude Code, Codex, Hermes, OpenClaw and other coding agents use Datris for data work instead of writing their own loaders, schedulers and vector stores.
Connecting an agent to the [MCP server](/mcp-server) gives it the tools. The **datris-platform skill** gives it the judgment: when to reach for Datris during ordinary project work, in what order to access it, and the rules that keep every run verifiable.
A skill is a folder with a `SKILL.md` file that an agent loads when a task matches its description. This one triggers whenever the work involves getting data in from a file, an API, a website, a database or a document set; validating, transforming or landing it; building a semantic-search index; scheduling a recurring pull; querying data the project already landed; or tracing where a dataset came from. The agent does not need to be told to use Datris each time.
The skill lives in the Datris repository at [`skills/datris-platform`](https://github.com/datris/datris-platform-oss/tree/main/skills/datris-platform).
## What the skill tells the agent
* **Access order.** Use the MCP tools for every action the agent takes itself. Use the REST API only for code it writes into the project, which runs later without an agent, or when a capability has no MCP tool. Never shell out to the CLI; it is a tool for people at a terminal.
* **If Datris isn't installed.** How to tell that no instance is reachable, to offer the user a local install and wait for a yes, the install command, and the two public endpoints that show the server is up. The full steps stay on [Install for Agents](/install-for-agents).
* **First contact.** Confirm the MCP connection, read the server version and budgets, read the [agent policy](/agent-policy), pick one [Data Catalog](/data-catalog) for the project, and look for existing pipelines, taps and secrets before creating anything.
* **What to use Datris for.** A table mapping project needs (load a file, pull from an API on a schedule, store a credential, validate rows, land data in a warehouse, build a document index, preview rows without keeping them, find and query existing data, trace provenance) to the Datris capability that does it.
* **Working rules.** Verify completion by polling the job rollup rather than trusting a response body, read a tap run's `persisted` flag before anything else, test a tap before running or scheduling it, schedule inside Datris, keep configs to source plus destination, respect approval gates, and only act on what the user asked for in the current turn.
Reference files alongside the skill carry the full tool catalog, the REST endpoints a service needs, a destination chooser, step-by-step workflows and setup. The agent reads those on demand, so the skill itself stays short.
## Install
Copy the folder into the project where the agent looks for skills. Pick the lines for your agent.
```bash theme={null}
git clone --depth 1 https://github.com/datris/datris-platform-oss.git /tmp/datris-oss
# Claude Code (project)
mkdir -p .claude/skills && cp -r /tmp/datris-oss/skills/datris-platform .claude/skills/
# Claude Code (every project on this machine)
cp -r /tmp/datris-oss/skills/datris-platform ~/.claude/skills/
# OpenAI Codex, Hermes, Cursor and other agents that read the .agents convention (project)
mkdir -p .agents/skills && cp -r /tmp/datris-oss/skills/datris-platform .agents/skills/
# Hermes (user-wide), or install straight from GitHub without cloning
cp -r /tmp/datris-oss/skills/datris-platform ~/.hermes/skills/
hermes skills install datris/datris-platform-oss/skills/datris-platform
# OpenClaw (user-wide)
cp -r /tmp/datris-oss/skills/datris-platform ~/.openclaw/workspace/skills/
```
Codex also reads `AGENTS.md` at the project root. One line there makes the skill discoverable before any task triggers it:
```
Datris is this project's data platform. Read .agents/skills/datris-platform/SKILL.md before any data ingestion, storage, search or scheduling work.
```
### Agents without a skills loader
For an agent that accepts project or system instructions but has no skills directory, paste the body of `SKILL.md` (everything below the frontmatter) into those instructions and keep the `references/` folder in the repository so the agent can read the tool catalog and workflows when it needs them.
## Before the agent starts
When no Datris instance is reachable, the skill tells the agent to say so and offer to install one locally, and to install only if the user agrees. To have everything ready before the agent starts:
1. [Install Datris](/installation) (from an agent or CI job, see [Install for Agents](/install-for-agents)) and connect the agent: [Claude Desktop and Claude Code](/configuring-claude), [OpenClaw](/configuring-openclaw), or the generic snippet on the [MCP server](/mcp-server#configuring-claude) page.
2. For any shared install, turn on [API keys](/api-keys), the [agent policy](/agent-policy) and the [audit log](/audit-log), and issue the agent its own scoped key. The [Claude Code to PostgreSQL recipe](/recipes/claude-code-postgres) walks through this in about fifteen minutes.
## Keeping the skill current
The skill's tool catalog mirrors the [MCP server tool list](/mcp-server#available-tools). Re-copy the folder when you upgrade Datris so the agent learns about new tools. If you edit the skill for your project, keep your copy under version control alongside the project.
# AI Configuration
Source: https://docs.datris.ai/ai-configuration
Configure AI features
The data platform integrates with AI providers for these features:
* **Schema generation** — upload a file to `POST /api/v1/pipeline/generate` and receive a complete pipeline configuration with inferred field names and types
* **Data quality rules** — describe validation logic in plain English using `aiRule` instead of writing code
* **Transformations** — describe data transformations in plain English using `aiTransformation`
* **Data profiling** — upload a file and get summary statistics, quality issues, and suggested validation rules
* **Header validation** — AI-powered fuzzy matching for CSV/delimited file headers against the pipeline schema
* **Error explanation** — AI analyzes job failures and explains the root cause in plain English
* **Vector search & embeddings** — required when pushing data to vector destinations (Qdrant, Weaviate, Milvus, Chroma, pgvector) or running semantic search
## Independent Secrets
AI configuration is split into independent, self-describing Vault secrets, each pointing at its own endpoint/model/key. Each is a separate slot in `application.yaml`:
```yaml theme={null}
ai:
enabled: "true"
aiPrimary:
secretName: "oss/ai-primary"
codegen:
secretName: "oss/codegen"
embedding:
secretName: "oss/embedding"
webSearch:
secretName: "oss/web-search"
```
| Slot | Purpose | Used by |
| - | - | - |
| `aiPrimary` | General AI tasks | Error explanation, brainstorm chat, profiling, header validation, CRON generation, AI Q\&A |
| `codegen` | Code-generation tasks | Tap scripts, `aiRule`, `aiTransformation`, JSON Schema / XSD generation, natural-language → SQL |
| `embedding` | Vector embeddings | Vector destinations (Qdrant, Weaviate, Milvus, Chroma, pgvector), semantic search |
| `webSearch` | Live web lookup (optional) | Tap script generation and tap diagnosis when the model needs current API documentation |
Each Vault secret carries its full configuration inline:
| Field | Required | Notes |
| - | - | - |
| `provider` | Yes | `anthropic`, `openai`, `azure`, `bedrock`, `grok`, or `ollama` |
| `endpoint` | Yes (blank OK for bedrock) | API endpoint URL. Azure has no default — use your resource URL, e.g. `https://YOUR-RESOURCE.openai.azure.com/openai/v1/chat/completions`. Bedrock derives its endpoint from the AWS region when blank |
| `model` | Yes | Model identifier. For Azure this is your **deployment name**; for Bedrock an invokable model id (e.g. `anthropic.claude-sonnet-5`) |
| `apiKey` | No (not used by ollama or bedrock) | Inline provider API key. The [shared API key store](#shared-api-key-store) is checked first and takes precedence when set. Bedrock authenticates with AWS credentials instead — see the [Bedrock path](#amazon-bedrock-path) |
| `version` | Optional | API version (Anthropic uses `2023-06-01`) |
The resolver reads each secret at the configured path and uses whatever it finds inside — there is no path derivation from a YAML `provider` field. This means the four slots can independently use different providers (e.g. Anthropic for `aiPrimary`, Ollama for `codegen`, OpenAI for `embedding`).
**All three seeded slots (`aiPrimary`, `codegen`, `embedding`) support Ollama** — you can run the entire platform on local models with no API keys. The Configuration UI offers Ollama as a provider choice for every section, with a free-text model input since Ollama supports hundreds of community models.
### Shared API Key Store
Provider API keys live in a shared per-provider secret, `{env}/ai-keys` (`oss/ai-keys` in single-tenant deployments), with one field per provider: `anthropicApiKey`, `openaiApiKey`, `azureApiKey`, and `grokApiKey`. When a slot needs a key, resolution order is:
1. **Shared key store** (`{env}/ai-keys`) — checked first; takes precedence over any inline key
2. **Inline `apiKey`** on the slot secret — legacy deployments that stored the key on the slot itself
3. **`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `AZURE_OPENAI_API_KEY` / `XAI_API_KEY`** container env vars — single-tenant only
Keys are stored once per provider rather than once per slot, so switching a slot between providers never loses the other provider's key. This is the store behind the Configuration UI's shared **API Keys** panel — entering a key there writes it to `{env}/ai-keys` for all slots to use.
The same store holds the Bedrock provider's AWS credentials, in four fields: `awsAccessKeyId`, `awsSecretAccessKey`, `awsSessionToken` (optional, for temporary credentials), and `awsRegion`. Bedrock resolves them in its own order: the store, then the standard `AWS_*` container env vars (single-tenant only), then the server's IAM role / AWS default credential chain — so an instance-role deployment can run Bedrock with no stored secrets at all.
It also holds the Azure Entra service-principal credentials for
[keyless Azure OpenAI auth](#keyless-auth-microsoft-entra-id), in three fields:
`azureTenantId`, `azureClientId`, and `azureClientSecret`. Azure resolves auth per
request: a stored API key wins; otherwise the SP trio; otherwise (single-tenant
only) the Azure default credential chain — `AZURE_*` env vars, managed identity on
Azure compute, workload identity, or `az login`.
## Quick Start with `.env`
On first boot, `docker/vault-init.sh` seeds the `ai-primary`, `codegen`, and `embedding` Vault secrets automatically (the optional `web-search` slot is not seeded — configure it from the Configuration tab) based on which API key you put in `.env` (see [How Configuration Persists](#how-configuration-persists) — after the first boot, the Configuration tab is the source of truth). Set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, the `AZURE_OPENAI_*` trio, `XAI_API_KEY` (see the [Grok path](#grok-xai-path)), or `AI_PROVIDER=bedrock` (see the [Bedrock path](#amazon-bedrock-path)):
```bash theme={null}
# .env
ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=
```
### OpenAI path
When `OPENAI_API_KEY` is set, all three slots are seeded against OpenAI:
| Slot | Provider | Endpoint | Model |
| - | - | - | - |
| `oss/ai-primary` | openai | `https://api.openai.com/v1/chat/completions` | `gpt-5.6-sol` |
| `oss/codegen` | openai | `https://api.openai.com/v1/chat/completions` | `gpt-5.6-sol` |
| `oss/embedding` | openai | `https://api.openai.com/v1/embeddings` | `text-embedding-3-small` |
GPT-6 models (`gpt-6-astra`) are supported for the chat and codegen slots. Requests to the GPT-6 family are routed to the Responses API automatically, whatever endpoint the slot is configured with, and no temperature is sent. Reasoning effort stays at the platform default (`medium`); the `xhigh` and `max` levels are not exposed.
### Azure OpenAI path
Use your Azure OpenAI resource instead of api.openai.com. All three values are required together — Azure endpoints embed your resource name and models are addressed by **deployment name**, so there are no universal defaults:
```bash theme={null}
# .env
AI_PROVIDER=azure
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com
AZURE_OPENAI_MODEL=gpt-5-2 # your chat deployment's name
```
| Slot | Provider | Endpoint | Model |
| - | - | - | - |
| `oss/ai-primary` | azure | `{AZURE_OPENAI_ENDPOINT}/openai/v1/chat/completions` | your deployment name |
| `oss/codegen` | azure | `{AZURE_OPENAI_ENDPOINT}/openai/v1/chat/completions` | `CODEGEN_MODEL` or same deployment |
| `oss/embedding` | tei | `http://tei:80/v1/embeddings` | `BAAI/bge-m3` |
Embeddings default to the bundled service; set `EMBEDDING_PROVIDER=azure` to use an embedding deployment on your resource instead (see [mix-and-match](#mix-and-match-anthropic-chat--openai-embeddings)).
**Name deployments after their model** (e.g. `gpt-5-2`, `o4-mini` — the Azure portal's default). The platform detects model-specific behavior, like omitting sampling parameters that reasoning models reject, from the model name — which for Azure is the deployment name.
Datris targets Azure's **v1 API** (`/openai/v1/...`), which needs no `api-version` parameter. Legacy deployment-scoped URLs also work — put the full URL including `?api-version=...` in the section's Endpoint field in the Configuration UI.
#### Keyless auth (Microsoft Entra ID)
Azure OpenAI also works **without an API key** — required when your resource has
`disableLocalAuth` set (API keys disabled, often tenant-wide by policy), and useful
anywhere you'd rather rotate credentials centrally. Pick the authentication mode in
the Configuration tab's Azure OpenAI credentials section:
* **Service principal** — works for any Datris deployment. Enter the Entra
**Tenant ID**, **Client ID**, and **Client Secret** of an app registration.
Alternatively set the `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET`
container env vars (single-tenant only).
* **Managed identity** — zero stored secrets. Available when the Datris server runs
on Azure compute (VM, AKS, App Service, Container Apps); nothing to enter at all.
Either way, the identity needs the **Cognitive Services OpenAI User** role on the
Azure OpenAI resource:
```bash theme={null}
az role assignment create \
--assignee \
--role "Cognitive Services OpenAI User" \
--scope /subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/
```
Precedence: a stored API key always wins over Entra configuration. If you're moving
a resource to `disableLocalAuth`, switch the auth mode in the Configuration tab —
saving the switch clears the stored key so it can't shadow Entra auth. Chat,
CodeGen, embeddings, and the Assistant all honor the same resolution; tokens are
acquired and refreshed automatically.
### Amazon Bedrock path
Run Claude through your AWS account — IAM auth, AWS billing, no Anthropic API key. Bedrock is **explicit-only**: it activates with `AI_PROVIDER=bedrock`, never from the mere presence of AWS keys (those are routinely set for S3 destinations).
```bash theme={null}
# .env
AI_PROVIDER=bedrock
AWS_REGION=us-east-1
# Optional — leave both unset to use the host's IAM role / default credential chain:
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
# Optional model override (default shown):
# BEDROCK_MODEL=anthropic.claude-opus-5-5
```
| Slot | Provider | Endpoint | Model |
| - | - | - | - |
| `oss/ai-primary` | bedrock | *(blank — derived from region)* | `BEDROCK_MODEL` or `anthropic.claude-opus-5-5` |
| `oss/codegen` | bedrock | *(blank — derived from region)* | `CODEGEN_MODEL` or `anthropic.claude-opus-5-5` |
| `oss/embedding` | tei | `http://tei:80/v1/embeddings` | `BAAI/bge-m3` |
Notes:
* **Model ids** are Bedrock's `anthropic.`-prefixed forms (e.g. `anthropic.claude-opus-5-5`). Models that aren't on-demand invokable in your region are addressed by their **cross-region inference profile** id (e.g. `us.anthropic.claude-...`). The Configuration UI discovers the invokable list live from your account when its IAM policy allows it.
* **Enable model access first.** Models must be enabled for your account in the AWS Bedrock console — a model can appear in listings and still fail with `AccessDeniedException` on the first call until access is granted.
* **Claude Fable 5 / 5.1 need a one-time data-retention opt-in.** Fable 5 and 5.1 (and Mythos 5 / 5.1) require Bedrock's `provider_data_share` retention mode — prompts/completions are shared with Anthropic and retained up to 30 days for trust and safety (not used for training; other Claude models are unaffected by the setting). Without it, calls fail with "data retention mode 'default' is not available for this model". Opt in once per account: `PUT /data-retention` with `{"mode": "provider_data_share"}` on the Bedrock control plane (IAM action `bedrock:PutAccountDataRetention`) — see AWS's Bedrock "Data retention" documentation. There is no console UI for this setting at the time of writing.
* **Minimal IAM policy:** `bedrock:InvokeModel` on the models you use, plus `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles` for the UI's live model dropdown (optional — without them the UI shows a standard list).
* **Endpoint stays blank** in the slot secrets; the invoke URL is derived from the region and model. Set an explicit endpoint only for VPC endpoints or GovCloud.
* **Embeddings and web search are not available on Bedrock** — embeddings default to the bundled bge-m3 service (or pair with OpenAI/Azure), and web search stays on Anthropic/OpenAI.
* Extended thinking works the same as on the direct Anthropic provider, including the always-on thinking of Claude Fable 5 and 5.1.
### Grok (xAI) path
Run chat and CodeGen on Grok through xAI's OpenAI-compatible API. One key from [console.x.ai](https://console.x.ai) covers both slots:
```bash theme={null}
# .env
XAI_API_KEY=...
# Optional model override (default shown):
# GROK_MODEL=grok-4.7
```
| Slot | Provider | Endpoint | Model |
| - | - | - | - |
| `oss/ai-primary` | grok | `https://api.x.ai/v1/chat/completions` | `GROK_MODEL` or `grok-4.7` |
| `oss/codegen` | grok | `https://api.x.ai/v1/chat/completions` | `CODEGEN_MODEL` or `grok-4.7` |
| `oss/embedding` | tei | `http://tei:80/v1/embeddings` | `BAAI/bge-m3` |
Notes:
* xAI has **no embeddings API**, so embeddings default to the bundled bge-m3 service (or pair with OpenAI/Azure via `EMBEDDING_PROVIDER` — see [mix-and-match](#mix-and-match-anthropic-chat--openai-embeddings)).
* **Web search is not available on Grok** — pair it with Anthropic or OpenAI web search if you need it.
* Grok's reasoning happens server-side; unlike Claude's extended thinking it isn't surfaced in the Assistant's thinking panel.
* With multiple keys in `.env`, pin the choice explicitly with `AI_PROVIDER=grok` (auto-detection only picks Grok when `XAI_API_KEY` is the sole key).
### Anthropic path (with bundled embedding service)
When `ANTHROPIC_API_KEY` is set, `aiPrimary` and `codegen` use Claude. Anthropic has no embeddings API, so `embedding` falls back to a **bundled embedding service** running `bge-m3` (a strong open-source 1024-dimension embedding model). No OpenAI key required, vector destinations work out of the box.
| Slot | Provider | Endpoint | Model |
| - | - | - | - |
| `oss/ai-primary` | anthropic | `https://api.anthropic.com/v1/messages` | `claude-opus-5-5` |
| `oss/codegen` | anthropic | `https://api.anthropic.com/v1/messages` | `claude-opus-5-5` |
| `oss/embedding` | tei | `http://tei:80/v1/embeddings` | `BAAI/bge-m3` |
The `tei` service (HuggingFace Text Embeddings Inference) is part of `docker-compose.yml`. On first start it downloads `bge-m3` (\~2.2 GB ONNX weights) into a persistent volume; subsequent restarts use the cached model.
### Fully local with Ollama
You can run all three slots on a local Ollama instance — no cloud API keys needed. Ollama is opt-in: uncomment the optional `ollama` service block in `docker-compose.yml` (and the `ollama-data` volume), then `docker compose up -d ollama` and pull the models you want (e.g. `docker exec ollama ollama pull qwen3:14b`). Alternatively, point at an Ollama instance on your host machine using `host.docker.internal`:
| Slot | Provider | Endpoint | Model (example) |
| - | - | - | - |
| `oss/ai-primary` | ollama | `http://ollama:11434/v1/chat/completions` (bundled) or `http://host.docker.internal:11434/v1/chat/completions` (host) | `qwen3:14b` |
| `oss/codegen` | ollama | same as above | `qwen2.5-coder:7b-instruct` |
| `oss/embedding` | ollama | same as above | `bge-m3` |
With every slot on Ollama no data goes to a cloud model, but the chat assistants (the Assistant, Search chat and the other chat panels) cannot run on Ollama and need a cloud provider; [Data Flows](/production/data-flows) lists where each kind of data goes for each provider choice.
If you only need local AI Primary and CodeGen but want to keep the bundled embedding service (TEI), leave `oss/embedding` pointed at `http://tei:80/v1/embeddings` with `model="BAAI/bge-m3"` and only switch the AI Primary and CodeGen slots to Ollama.
The Configuration UI makes this easy — select **Ollama (if optional service enabled)** as the provider and type your model name. For the AI Primary and CodeGen slots the endpoint defaults to `http://host.docker.internal:11434/v1/chat/completions` (edit it to `http://ollama:11434/v1/chat/completions` for the bundled service); for the Embedding slot it defaults to `http://ollama:11434/v1/embeddings`. No API key is required.
### Optional model overrides
Override the seeded models by setting these in `.env`:
```bash theme={null}
ANTHROPIC_MODEL=claude-opus-5-5
OPENAI_MODEL=gpt-5.6-sol
GROK_MODEL=grok-4.7
CODEGEN_MODEL=claude-opus-5-5 # or gpt-5.6-sol if using OpenAI, grok-4.7 for Grok
ANTHROPIC_OVERLOAD_FALLBACK_MODEL=claude-sonnet-5 # used for the current request when an Anthropic Opus call stays overloaded after retries
```
### Mix-and-match: Anthropic chat + OpenAI embeddings
The embedding slot is **decoupled from the chat slots** — set `EMBEDDING_PROVIDER` in `.env` to override what `vault-init.sh` seeds for `oss/embedding`, regardless of `AI_PROVIDER`. This is the recommended path when you want Claude for chat/codegen but don't want to run the bundled TEI sidecar (for example on a small host where TEI gets OOM-killed).
```bash theme={null}
# .env — Anthropic for chat/codegen, OpenAI for embeddings
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
EMBEDDING_PROVIDER=openai
```
Valid values for `EMBEDDING_PROVIDER`:
| Value | Endpoint | Default model | Requirements |
| - | - | - | - |
| `openai` | `https://api.openai.com/v1/embeddings` | `text-embedding-3-small` (1536-dim) | `OPENAI_API_KEY` set |
| `azure` | `{AZURE_OPENAI_ENDPOINT}/openai/v1/embeddings` | `text-embedding-3-small` (deployment name) | `AZURE_OPENAI_API_KEY` set, embedding deployment created on your resource |
| `tei` | `http://tei:80/v1/embeddings` | `BAAI/bge-m3` (1024-dim) | bundled TEI sidecar running |
| `ollama` | `http://ollama:11434/v1/embeddings` | `bge-m3` (1024-dim) | optional Ollama service enabled, model pulled |
`EMBEDDING_MODEL` overrides the default model for the chosen provider (e.g. `text-embedding-3-large` for higher quality at 3072-dim). `EMBEDDING_ENDPOINT` overrides the URL for the `tei` or `ollama` providers — useful when running TEI or Ollama on a separate host and pointing at it from inside Docker.
When `EMBEDDING_PROVIDER` is unset, embedding follows `AI_PROVIDER`: `anthropic` seeds the bundled TEI; `openai` seeds OpenAI embeddings.
**Switching providers changes vector dimensions.** `bge-m3` is 1024-dim, `text-embedding-3-small` is 1536-dim, `text-embedding-3-large` is 3072-dim. Existing pgvector / Qdrant / Weaviate / Milvus / Chroma collections built on the previous embedder will fail-fast on the next run with a dimension-mismatch message — drop the destination table or collection and re-ingest.
## Hot Reload
When you save AI configuration from the Configuration UI, the server reloads the `ai-primary` and `codegen` configs from Vault immediately — **no restart required**. The next AI call uses the updated provider, model, and endpoint. The `embedding` config is read from Vault on each request and also requires no restart.
## How Configuration Persists
Vault stores its data on a durable Docker volume (`vault-data`), so secrets, AI provider and model choices, and other Configuration-tab changes **survive restarts and rebuilds** — `docker compose up --build`, `down && up`, and host reboots all keep your settings.
`.env` is a **first-boot seed only**. The first time Vault initializes, `vault-init.sh` seeds the secrets from `.env`. After that, Vault persists on disk and the **Configuration tab is the source of truth** — editing a value in `.env` later has no effect. To change AI configuration after the first boot, use the Configuration tab (saves go to Vault and stick).
This applies to everything stored in Vault (AI config and connection secrets). Deployment-level settings like `JAVA_OPTS` and `USE_USER_AUTH` work the opposite way — they're plain container env vars read on every boot, so `.env` edits *do* take effect on a container recreate. See [Changing Settings After Install](/configuration-reference#changing-settings-after-install) for the full two-bucket breakdown.
**Upgrading from an older release — one-time step.** Earlier versions kept Vault data in memory only, so any secret you had set **only** in the UI — **including tap connection secrets** — is not carried into the new on-disk store and must be re-entered once after upgrading. Secrets supplied through `.env` are re-seeded automatically. After this one-time step, everything persists across future rebuilds.
### Re-seeding from `.env`
To discard the persisted Vault data and seed fresh from `.env` again (a clean reset), remove the volume:
```bash theme={null}
docker compose down
docker volume rm datris-platform-oss_vault-data # the project-name prefix may differ
docker compose up -d
```
The next boot re-initializes Vault and seeds from `.env`.
## Setting Secrets Manually
To write a secret directly to Vault (note: manual Vault writes do **not** trigger hot reload — restart the server or re-save from the UI):
```bash theme={null}
docker compose exec -e VAULT_ADDR=http://vault:8200 \
-e VAULT_TOKEN="$(docker compose exec -T datris cat /vault-token/token)" vault \
vault kv put secret/oss/ai-primary \
provider="anthropic" \
endpoint="https://api.anthropic.com/v1/messages" \
model="claude-opus-5-5" \
apiKey="sk-ant-..." \
version="2023-06-01"
```
Same shape for `oss/codegen` and `oss/embedding`. Verify with:
```bash theme={null}
docker compose exec -e VAULT_ADDR=http://vault:8200 \
-e VAULT_TOKEN="$(docker compose exec -T datris cat /vault-token/token)" vault \
vault kv get secret/oss/ai-primary
```
## Embedding Provider Notes
The embedding endpoint must speak the **OpenAI embeddings API** (`POST /v1/embeddings` with `{model, input}` body returning `{data: [{embedding: [...]}]}`). That contract is implemented by:
* **OpenAI** — `https://api.openai.com/v1/embeddings`
* **Azure OpenAI** — `https://YOUR-RESOURCE.openai.azure.com/openai/v1/embeddings` (model = your embedding deployment's name)
* **TEI (bundled)** — `http://tei:80/v1/embeddings` (the bundled HuggingFace Text Embeddings Inference sidecar serves `BAAI/bge-m3` out of the box)
* **Ollama** — `http://ollama:11434/v1/embeddings` (only when the optional Ollama service is enabled and `bge-m3` has been pulled there)
* **Ollama (external)** — `http://host.docker.internal:11434/v1/embeddings` (your own Ollama with any embedding model)
* **Self-hosted vLLM, LM Studio, etc.** — anything implementing the same contract
Anthropic has no embeddings API, so `provider: "anthropic"` is not valid for the `embedding` slot.
## Chat Provider Notes
The `aiPrimary` and `codegen` slots support Anthropic, OpenAI, Azure OpenAI, Amazon Bedrock, Grok (xAI), and Ollama. Ollama must expose the **OpenAI chat completions API** (`POST /v1/chat/completions`), which it does by default. When running Ollama on your host machine, the endpoint from inside Docker is `http://host.docker.internal:11434/v1/chat/completions`.
Azure OpenAI always speaks chat/completions against your configured endpoint (never the OpenAI Responses API, whose Azure availability varies by region and model). Web search is not available on the Azure provider — pair Azure chat with Anthropic or OpenAI web search if you need it.
Amazon Bedrock serves Claude models over the same Anthropic request/response shape as the direct provider, with AWS SigV4 authentication. Web search is not available on Bedrock either — pair it with Anthropic or OpenAI web search if needed.
Grok speaks the OpenAI chat completions API against `https://api.x.ai/v1/chat/completions` with Bearer authentication. Like Azure, it never routes to the OpenAI Responses API, and web search is not available on it.
## Web Search (Optional)
When enabled, four surfaces can consult the live web — official API documentation, deprecation notices, current package names — instead of relying only on the model's training data. Off by default; flip it on from the **Configuration → AI Providers** tab.
Web search is its own independent service — same model as Embedding. Pick a provider regardless of which provider runs the main AI work. The platform attaches the tool natively to the main AI call when the providers match (one round trip, the model decides when to search) or makes a separate out-of-band search call and injects the results as context (when providers differ — e.g. AI Primary=Anthropic, Web Search=OpenAI). Either way the model sees the current information.
| Field | Required | Notes |
| - | - | - |
| `enabled` | Yes | `true` or `false` |
| `provider` | Yes | `anthropic` or `openai` (Ollama has no native web search) |
| `endpoint` | Yes | Provider endpoint — Anthropic Messages or OpenAI Responses |
| `model` | Yes | Model that runs the search call |
| `apiKey` | Yes | Own key, copied from the right-hand panel based on the chosen provider (mirrors Embedding) |
| `version` | Optional | API version (Anthropic uses `2023-06-01`) |
| `maxUses` | Optional | Maximum searches per AI call (default 3) |
| Surface | Web search opt-in |
| - | - |
| Tap brainstorm (chat) | Yes — verifies recommended data sources still exist and surfaces current free-tier / auth state |
| Tap diagnosis (analyzes run logs after a failed test) | Yes — looks up unfamiliar exception messages, library deprecation notices |
| Tap fix path (Auto-Fix after a tap fails) | Yes — looks up current PyPI install names, current method signatures (replaces the legacy PyPI scraper when on) |
| First-pass tap script generation | No — happy path, model uses training data so generation stays fast |
| NL → SQL, schema gen, profiling, DQ rule gen, etc. | No — self-contained, they don't need the web |
OpenAI web search requires the **Responses API**. When the configured endpoint is `/v1/chat/completions`, the call is automatically routed to `/v1/responses`. Ollama has no native web search; selecting it as your AI Primary doesn't prevent you from using Anthropic or OpenAI for web search.
## Stronger Model for Code Generation
**Highly recommended: a provider's strongest coding model for CodeGen.** Tap script generation, AI data quality rules, AI transformations, and NL→SQL all depend heavily on the model's coding ability — the top coding models from Anthropic and OpenAI produce dramatically better results than smaller or cheaper models. The same recommendation appears in `.env.example`.
CodeGen tasks (tap scripts, `aiRule`, `aiTransformation`, JSON Schema / XSD generation, natural-language → SQL) have their own slot so they can run a different model than the general default. On a fresh install the slot seeds the strongest code-generation model — `gpt-5.6-sol` with OpenAI, `claude-opus-5-5` with Anthropic — so code generation is at full quality out of the box, while `aiPrimary` seeds the recommended chat model (`claude-opus-5-5` / `gpt-5.6-sol`). The separate slot is your cost-tuning lever: point `aiPrimary` at a cheaper model (e.g. `claude-sonnet-5`) for the chatty paths and keep the stronger model only on the calls that benefit from it.
| Feature | Uses codegen slot? |
| - | - |
| Tap script generation, auto-fix | Yes |
| `aiRule` data quality rules | Yes |
| `aiTransformation` | Yes |
| CSV / JSON Schema / XSD generation | Yes |
| Natural language → SQL (`/query/ai`) | Yes |
| Error explanation, brainstorm chat, CRON generation | No (general slot) |
| Data profiling, AI Q\&A | No (general slot) |
| AI header validation | No (general slot) |
To use the same model for codegen as for everything else, point `oss/codegen` at the same secret values as `oss/ai-primary`.
## Multi-Tenant: Per-Tenant Overrides
In multi-tenant deployments (`multiTenant: "true"` in `application.yaml`) each tenant can override any of the three slots independently. The resolver checks per-request:
| Slot | Per-tenant Vault path | Falls back to |
| - | - | - |
| AI Primary | `{env}/ai-primary` | `oss/ai-primary` |
| CodeGen | `{env}/codegen` | `oss/codegen` |
| Embedding | `{env}/embedding` | `oss/embedding` |
| Web Search | `{env}/web-search` | `oss/web-search` |
(`{env}` is the tenant's environment name, e.g. `acme`.)
A per-tenant secret takes effect on the next AI call — no restart needed. The override is independent for each slot: a tenant can override just AI Primary, just CodeGen, just Embedding, or any combination.
### Setting your key from the UI
Any tenant can manage all three slots from the platform UI. The Configuration page is organized into sub-tabs:
* **AI Providers** — AI Primary, CodeGen, and Embedding provider + model selectors, plus a shared API Keys panel on the right (backed by the `{env}/ai-keys` secret — see [Shared API Key Store](#shared-api-key-store)). This is where you enter your provider API keys and pick models.
* **Secrets** — operator-facing platform credentials (Vault-backed). Admin-only when user-auth is on.
* **Users** — user accounts and roles. Visible when `USE_USER_AUTH=true`.
* **API-Keys** — issue and revoke scoped credentials for the CLI, MCP agents, and other programmatic clients. Visible when `USE_API_KEYS=true`. See [API Keys](/api-keys).
* **Data Sources**, **Code Repository**, **Audit Log**, **Agent Policy** — additional operator sub-tabs (see their respective pages).
* **Configuration assistant** (chat rail at the right edge) — see [Configuration Assistant](/configuration-assistant).
Some sub-tabs are role- and flag-gated — see the [API Keys matrix](/api-keys#supported-configurations) for the full visibility rules.
To set your AI keys:
1. Open the **Configuration** tab, then the **AI Providers** sub-tab.
2. Each section (AI Provider, CodeGen Provider, Embedding Provider) has its own provider and model selector. Providers include Anthropic, OpenAI, Azure OpenAI, Amazon Bedrock, Grok (xAI), and Ollama (local).
3. For Anthropic/OpenAI/Azure/Grok, enter your API key in the right-hand panel and pick a model from the dropdown. For Azure, also enter your resource endpoint and type your deployment name as the model. For Amazon Bedrock, enter your AWS credentials and region in the right-hand panel (or leave the keys blank to use the server's IAM role) — the model dropdown then lists what your account can actually invoke. For Ollama, type the model name directly — no API key needed.
4. Click **Save Configuration**. Changes take effect immediately — no restart required.
To fall a tenant back to the global `oss/*` defaults, delete its per-tenant secrets in Vault (see below).
### Setting tenant secrets directly in Vault
For operators provisioning tenants programmatically:
```bash theme={null}
docker compose exec -e VAULT_ADDR=http://vault:8200 \
-e VAULT_TOKEN="$(docker compose exec -T datris cat /vault-token/token)" vault \
vault kv put secret/acme/ai-primary \
provider="anthropic" \
endpoint="https://api.anthropic.com/v1/messages" \
model="claude-opus-5-5" \
apiKey="sk-ant-..." \
version="2023-06-01"
```
Same shape for `{env}/codegen` and `{env}/embedding`.
## Misconfiguration
The platform fails fast at startup if the `aiPrimary` slot is missing or malformed — it is always required when `ai.enabled: true`. The `codegen` slot is optional: when its secret is absent, code-generation tasks fall back to `aiPrimary`. The `embedding` slot is read per request and is only required by features that depend on it (vector destinations and semantic search).
# AI Data Profiling
Source: https://docs.datris.ai/ai-data-profiling
AI-powered data profiling
Upload any data file and receive an AI-generated profile — summary statistics, data quality issues, and recommendations for validation rules and transformations. Use profiling to understand your data before setting up a pipeline configuration.
## Endpoint
`POST /api/v1/pipeline/profile`
## Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `file` | multipart file | (required) | The data file to profile (CSV, JSON, or XML) |
| `delimiter` | string | `,` | CSV delimiter character |
| `header` | boolean | `true` | Whether the CSV file has a header row |
| `sampleSize` | int | `200` | Number of rows to sample for large files |
## Example
```bash theme={null}
curl -s -X POST http://localhost:8080/api/v1/pipeline/profile \
-F "file=@stock_prices.csv" \
-F "delimiter=," \
-F "header=true" \
-F "sampleSize=200" | python3 -m json.tool
```
## Response
The endpoint returns a JSON object with three sections:
```json theme={null}
{
"summary": {
"rowCount": 200,
"columnCount": 8,
"columns": [
{
"name": "symbol",
"inferredType": "string",
"nullCount": 0,
"uniqueCount": 195,
"sampleValues": ["FAX", "IAF", "FCO"]
},
{
"name": "date",
"inferredType": "date",
"nullCount": 0,
"uniqueCount": 1,
"sampleValues": ["2016-12-30"]
},
{
"name": "open",
"inferredType": "float",
"nullCount": 1,
"uniqueCount": 198,
"sampleValues": ["4.65", "5.44", "7.91"]
}
]
},
"qualityIssues": [
"Column 'open' has 1 null/empty value",
"Column 'volume' contains a negative value (-500)",
"Column 'close' has a value exceeding $1,000,000"
],
"recommendations": [
"Add an aiRule to validate symbol format, price ranges, and cross-column relationships",
"Consider sampling for large files to keep processing fast"
],
"suggestedDataQuality": {
"aiRule": {
"instruction": "symbol must be 1-5 uppercase letters, all price columns (open, high, low, close, adj_close) must be positive and not exceed $1,000,000, volume must be positive, and high must be greater than or equal to low",
"onFailureIsError": false
}
}
}
```
### Response fields
| Section | Description |
| - | - |
| `summary` | Row count, column count, and per-column statistics (inferred type, null count, unique count, sample values) |
| `qualityIssues` | Data quality problems detected in the sample — missing values, outliers, inconsistent formats, suspicious patterns |
| `recommendations` | Human-readable suggestions for validation rules and transformations |
| `suggestedDataQuality` | Ready-to-use `dataQuality` JSON block that can be copied directly into a pipeline configuration. Contains an `aiRule` with a comprehensive plain-English instruction covering all validation checks |
### Suggested data quality rules
The `suggestedDataQuality` section provides a complete, copy-paste-ready `dataQuality` configuration based on what the AI observed in the data:
* **`aiRule`** — a single comprehensive plain-English instruction covering all validation checks: format patterns (emails, phone numbers, dates), value ranges, cross-column relationships (e.g., high >= low), and business logic. If no validation rule is appropriate, this field is omitted.
See [AI Rule](/data-quality/ai-rules) for full documentation.
## How it works
1. You upload a file — no pipeline registration needed
2. For large CSV files, the pipeline randomly samples `sampleSize` rows (keeping the header)
3. The sampled content is sent to the AI model with a profiling prompt
4. The AI analyzes the data and returns a structured JSON profile
Profiling is a standalone operation — it does not require a registered pipeline, data quality rules, or any pipeline configuration. It is designed to be the first step when working with a new data source, before setting up validation or ingestion.
## Use cases
* **Explore new data** — understand the structure, types, and quality of an unfamiliar file before writing a pipeline configuration
* **Discover quality issues** — find missing values, outliers, format inconsistencies, and suspicious patterns
* **Generate rule ideas** — the AI suggests aiRule instructions and transformations based on what it observes
* **Validate assumptions** — confirm that a file matches expected schema and data quality before loading
## Sampling
For CSV files larger than `sampleSize` rows, the profiling endpoint automatically samples a random subset. The header row is always included. For JSON and XML files, the content is truncated if it exceeds the AI model's context window.
The default sample of 200 rows is typically sufficient to detect patterns, types, and quality issues. Increase `sampleSize` for more thorough profiling at the cost of slower response times.
## When the model declines
An AI provider's safety classifier can decline to look at a file, most often one that looks like personal data. Profiling then still returns `200` with the server's own statistics only: `summary.rowCount`, `summary.columnCount` and `summary.columns` computed over every row of a CSV file (inferred type, nulls, distinct count, value lengths; `sampleValues` empty; empty for JSON and XML files), empty `qualityIssues` and `recommendations`, no `suggestedDataQuality`, a top-level `"aiDeclined": true` and a fixed `note`: "the model declined this request; statistics only". The provider's own text is never included. Read an empty `qualityIssues` list with `aiDeclined` as "no AI review", not as a clean file.
| Provider family | Recognised as a decline |
| - | - |
| Anthropic (direct or AWS Bedrock) | `stop_reason: "refusal"` |
| OpenAI, Azure OpenAI, Grok and Ollama (chat completions, streamed or not) | `finish_reason: "content_filter"`, or a non-empty `message.refusal` (`delta.refusal` when streamed) |
| OpenAI Responses API models | an output item or message content part of type `refusal`, or `incomplete_details.reason: "content_filter"` |
Only response-side declines are recognised. Azure OpenAI's prompt-side content filter answers with HTTP 400 (`code: content_filter`) and is still returned as an error. Nothing else counts as a decline: a missing or wrong provider key, a timeout, a rate limit or any other provider error is still returned as an error, so a misconfiguration is not hidden.
Other AI features (search answers, CodeGen, tap generation, fix suggestions and the rest) have no local fallback: a decline there, including a `content_filter` finish that used to return the cut-off text, is reported as an error.
## Values withheld
When the server sets `DATRIS_AI_SAMPLE_VALUES=false`, no row value is sent to the model. For a CSV file the model gets per-column statistics computed over every row (inferred type, count, nulls, distinct count, minimum and maximum value length); for JSON, NDJSON and XML (by the `.json`, `.ndjson`, `.jsonl` or `.xml` extension; for an unrecognised extension, by content that starts with `{`, `[` or `<`) it gets a value-free skeleton of the document. `.csv`, `.tsv`, `.psv` and `.txt` files are always profiled as delimited. A header cell that does not look like a plain column name is sent as `column_N`, and a first line that looks like data (a dash next to a digit, an `@`, or a long run of digits in any cell) is counted as a row and every column is numbered. A cell with more than two digits or no letter at all also marks line 1 as data. A headerless first row made only of words still passes as names and is sent: single words in any case (`John,Smith,Boston,MA`; `jane,doe,asthma,f`; `Diabetes,Type,Two`), all-caps or lower-case multi-word cells (`JOHN SMITH`, `jane doe`), and names the capitalised-words test misses (`John A Smith`, `McDonald Smith`, `O Brien`). `header` defaults to `true`: profile a headerless file with `header=false` so no cell of its first line is sent. `sampleValues` are empty for every column and the response carries a top-level `"valuesWithheld": true`. Quality issues and the suggested rule are then based on names, types and statistics only. If the model's reply cannot be read as JSON (for example a very wide file whose reply is cut off), or the model call fails for any reason (the model declines the request, a provider error, a timeout), the response holds the server's statistics only: `summary.columns` (empty for JSON and XML files), empty `qualityIssues` and `recommendations`, `"valuesWithheld": true` and a `note` giving the cause in the server's own words; the provider's error text is never included. A missing or misconfigured provider key or model also produces this note when values are withheld; check it with [`datris doctor`](/doctor). With values sent (the default), a failed model call still returns an error; only a model decline falls back, as described in [When the model declines](#when-the-model-declines) (with `aiDeclined`, not `valuesWithheld`). See [Field Protection](/transformation/field-protection#what-still-sees-raw-data).
## Requirements
* `ai.enabled: true` must be set in `application.yaml`
* A configured AI provider (see [AI Configuration](/ai-configuration))
* Cloud providers (Anthropic, OpenAI, Azure OpenAI, Amazon Bedrock, Grok) recommended for best results
# AI Error Explanation
Source: https://docs.datris.ai/ai-error-explanation
AI-powered error explanations, surfaced as an AI Suggested Fix
When a pipeline job fails, the AI automatically analyzes the error and provides a plain-English explanation of the root cause and suggested fix. The result — labelled **AI Suggested Fix** in the job status — appears as a separate status entry after the error, making it easy to diagnose issues without reading stack traces.
## How it works
When any pipeline stage (data quality, transformation, destination loading) throws an exception:
1. The error is logged to status as usual
2. The error message and pipeline configuration are sent to the AI model
3. The AI returns a concise explanation of what went wrong and how to fix it
4. The explanation is written as a separate status entry (`AI Suggested Fix: ...`, with `Diagnosis:` and `Suggested fix:` lines) and logged to the pipeline logs
If the AI call itself fails (rate limit, timeout, etc.), the explanation is skipped with a server log warning — it never causes a secondary failure.
## Example
**Error status:**
```
Process completed, error: Aborting processing this pipeline, 3 error(s) were found
while performing data quality rules:
Data quality CodeGen failure, row: 2, reason: high (7.50) is less than low (8.01)
Data quality CodeGen failure, row: 3, reason: volume is negative (-500)
Data quality CodeGen failure, row: 4, reason: close and adj_close exceed $1,000,000
```
**AI Suggested Fix status (separate entry):**
```
AI Suggested Fix: Three source rows violate the price/volume rules; fix the upstream data.
Diagnosis: Row 2 has a high value lower than low (likely a column swap or data entry
error), row 3 contains a negative volume, and row 4 has close and adj_close values
exceeding $1,000,000.
Suggested fix: Correct the upstream source data or add a pre-processing transformation
step to sanitize these issues before the data quality check runs.
```
## Configuration
No additional configuration is needed. AI error explanation is automatically enabled since an AI provider is required to run the platform.
## Requirements
* A configured AI provider (see [AI Configuration](/ai-configuration))
# API Keys
Source: https://docs.datris.ai/api-keys
Enable API-key authentication for the UI, CLI, MCP clients, and any other programmatic access to Datris.
By default, the Datris OSS install accepts unauthenticated requests on the REST and MCP APIs — fine for laptop development, wrong for any shared or hosted setup. Turn on API-key authentication and every caller (the UI, the CLI, agent MCP clients) must present a key that the platform recognizes.
This page covers single-tenant deployments. Multi-tenant setups (`MULTI_TENANT=true`) use the same flag and a different mapping mechanism — see [Multi-tenant](#multi-tenant) below.
## What it does
When `USE_API_KEYS=true`:
* Programmatic clients (CLI, MCP-connected agents, external scripts) must include `x-api-key: ` on every request.
* The platform validates the value against a known set of issued keys.
* The capability framework enforces what each scoped key is allowed to do (see [the capability model](#the-capability-model)). Calls outside the key's scope return HTTP 403.
* The Datris UI behavior depends on whether [user authentication](/user-auth) is also enabled. With `USE_USER_AUTH=true` (recommended), the UI authenticates via session cookie alone — no API key is sent or required. With user-auth off, the UI falls back to the legacy "paste a key" prompt on first load.
The flag is independent of `USE_USER_AUTH` but they're paired conceptually — see [the supported matrix](#supported-configurations) below.
## Enabling it
The recommended setup pairs `USE_API_KEYS=true` with `USE_USER_AUTH=true` (see [the matrix below](#supported-configurations) for why).
### 1. Flip the flags
In `.env`:
```bash theme={null}
USE_USER_AUTH=true
USE_API_KEYS=true
```
### 2. Restart
```bash theme={null}
docker compose up -d --force-recreate datris
```
### 3. Use the platform
* **The UI**: log in with your admin account. The session cookie authenticates browser flows — no `x-api-key` is sent, no Connect prompt appears. The Assistant inherits your session for capability checks; its tool calls, version-history entries, and [audit log](/audit-log) records are attributed to you (`session:`), not to the platform's internal `ui` key.
* **Programmatic clients** (CLI, MCP agents, scripts): each one needs its own key, issued from **Configuration → API-Keys**. See [the issuance flow](#issuing-keys-for-cli-mcp-and-other-clients) below.
* **Tap scripts** reading platform data need nothing: each run gets a short-lived, read-only token that the tap wrapper attaches automatically (see [Taps → Querying Datris Data](/taps#querying-datris-data)).
That's it. No env var to track, no value to paste anywhere.
### Legacy mode (`USE_USER_AUTH=false`, `USE_API_KEYS=true`)
If you can't or don't want to enable user-auth, the UI falls back to the older "paste a key" flow:
* The Connect prompt appears on first load
* The default seeded value is `default-ui-key` (defined in `docker/vault-init.sh`); paste it once
* Browser stores it in localStorage; subsequent loads skip the prompt
* Rotate it any time from **Configuration → API-Keys → ui → Rotate** (the new value is mirrored automatically into the validation map, and the operator distributes it out of band)
This mode is fine for laptop development but **not** appropriate for shared installs — anyone with the URL can reach the Connect prompt, and the only gate is knowledge of the pasted value.
## Issuing keys for CLI, MCP, and other clients
Open **Configuration → API-Keys → Issue new key**.
1. Pick a label (`cli`, `claude-desktop`, `cursor`, `prod-loader`, etc.) — lowercase, hyphens/underscores ok.
2. Pick a template or build a capability list manually:
* **read-only** — observer access to pipelines/taps/jobs/metadata + query/search
* **rag-builder** — create pipelines/taps in a catalog, upload documents, run vector search
* **reporting** — analyst-shaped read access
* **ops** — run any tap or pipeline, kill stuck jobs, no edits
* **full-access** — legacy `*:*` shape; avoid for agents
3. Review and create.
4. **Copy the value immediately** — it's shown once. The platform stores a hashed copy; the plaintext is not retrievable after this modal closes.
5. Paste the value into the agent's MCP client config (or CLI env var, etc.).
Each label is a distinct identity in the [audit log](/audit-log) and can be revoked independently of the others. Every issued key also gets a stable id (shown in the audit log) so a label that is revoked and later re-issued is not confused with its predecessor.
The Vault label `ui` is reserved for the seeded fallback UI key (used in legacy `USE_USER_AUTH=false` mode and for the Assistant's internal MCP-to-REST hop). You don't issue this one from the UI — it's seeded by `vault-init.sh` on first boot and rotated from the API-Keys tab like any other key.
## Supported configurations
`USE_API_KEYS` and `USE_USER_AUTH` compose into four states:
| `USE_USER_AUTH` | `USE_API_KEYS` | Behavior |
| - | - | - |
| `false` | `false` | Full anonymous. No prompts, no auth, no gating. **OSS default; fine for laptop dev.** |
| `true` | `true` | UI gated by login; programmatic clients gated by key. **Recommended for shared installs.** |
| `true` | `false` | UI gated by login; programmatic clients are anonymous. Workable, slightly odd. |
| `false` | `true` | **Not recommended.** No coherent auth mechanism for the browser. Pick a different combination. |
The fourth row is the trap. When you require strict auth for programmatic clients but no login for the UI, the browser has nothing to present. API keys are intended to be used alongside user auth — set both flags together, or neither.
## Rotating a key
From the **API-Keys** tab in Configuration, click **Rotate** on any row:
1. A new value is generated server-side
2. The validation map (`oss/api-keys`) and operator-facing record are updated in sync
3. The new value is shown once in a copy-to-clipboard modal
4. Distribute the new value out of band to whoever was using it; they'll get 401s on their next request and need the new value
The previous value stops working the moment Rotate returns.
## Revoking a key
From the **API-Keys** tab, click **Revoke**. The key's metadata is marked revoked; the underlying value stays in `oss/api-keys` so failed auth attempts log as "revoked key" rather than "unknown key" — useful for tracking compromised credential reuse.
A revoked key is rejected with `401 {"error":"API key is revoked or invalid"}` on every authenticated route, and the denial is recorded in the audit log as a security event whose actor is the revoked key's label (an unknown key value is recorded as `unknown-key`, never the value itself). If the secret store itself cannot be read, keys are not resolved at all and the response is `503` rather than falling back to full access.
Revoked keys cannot be un-revoked. Issue a new key instead.
## How requests get logged
Each authenticated request emits a structured line in the datris container log:
```
capability check: route=POST /api/v1/pipeline required=pipeline:create key=claude-desktop legacy=false outcome=grant
```
The fields:
* `route` — HTTP method + path
* `required` — the capability the route needs (see [the capability model](#the-capability-model))
* `key` — the label of the API key (or `session:` for cookie-authed UI flows)
* `legacy=true` — the key has full access via the backward-compatibility backstop; `false` means a scoped key
* `outcome` — `grant`, `would-deny`, `deny`, or `unmapped`
Enforcement is **on by default**: a scoped key calling a route it lacks the capability for gets `outcome=deny` and an HTTP 403. To trial a new scope policy without rejecting traffic — for example, you've issued a new scoped key and want to see what it would have been denied under real workload before pointing a production agent at it — set `CAPABILITY_ENFORCEMENT=log-only` in `.env`. The same calls then succeed and emit `outcome=would-deny` log lines instead of 403s. Flip back to `enforce` (or remove the line) once you've validated the policy.
## The capability model
Every API key carries a list of capability strings:
```
pipeline:read:catalog=support
document:upload:collection=support_docs
search:vector:collection=support_docs
job:read
```
Each capability is `resource:action[:scope]`. Scope segments restrict by container (catalog, database, collection), type, or ownership — never by leaf resource name, since agents pick those at runtime.
Tap secrets (`_type=tap`) are the only secrets a tap can read: a tap that names a platform secret is refused on save and at run time, so a key holding `tap:create` and `tap:run` cannot use a tap to read a platform secret that its `secret:read:_type=tap` scope keeps it from reading directly (unless an operator sets `DATRIS_TAP_SECRET_SCOPE=any`; see [Taps](/taps#taps-can-only-use-tap-secrets)).
`protect:reveal` (read back the original of an [`encrypt`-protected field](/transformation/field-protection#revealing-a-value)) and `protect:admin` (rotate its key, and edits to the field-protection secret) are never in a scoped template or in the editor and viewer role bundles: grant them explicitly to the key that needs them. Admins, the `full-access` template, and legacy unscoped keys hold them through `*:*`.
When user-auth is on, logged-in browser sessions are first-class identities too: `session:admin`, `session:editor1`, etc. The capability bundle is derived from the user's role (admin → full access, editor → create/update/run, viewer → read-only). The API-Keys tab in Configuration shows scoped *keys*; per-role capabilities are managed via the **Users** tab.
## Multi-tenant
`MULTI_TENANT=true` switches the model: a single `api-key-mappings` secret maps `keyValue → tenantEnv`, and every tenant carries its own per-environment data. The UI key flow above is single-tenant only; in multi-tenant mode the mappings are managed directly in the `api-key-mappings` Vault secret.
## See also
* [User Authentication](/user-auth) — login + roles for the UI
* [Configuration Reference](/configuration-reference) — full env var list
* [MCP Server](/mcp-server) — how external agents connect
## Capabilities vs. the agent policy
A capability says what a key *may* do. The [agent policy](/agent-policy) says what an agent may do *unattended*: for each action, whether it runs on its own, waits for a person to approve it, or is refused. The policy runs after the capability check and can only pause or refuse what the key already holds — it never grants anything. Two capabilities support it: `policy:read` (read the policy; any key) and `approval:read` (list and poll the approvals the key queued; use `approval:read:owner=self`). Deciding approvals is a person's job — an agent-initiated request is refused even with `approval:decide`.
# Approve a queued action (admin / editor)
Source: https://docs.datris.ai/api-reference/agent-policy/approve-a-queued-action-admin-editor
/openapi.yaml post /api/v1/approvals/{id}/approve
Performs the original request on the approver's behalf by replaying it through the normal request chain. Never permitted from an agent-initiated request. Refused as stale (409) if the target pipeline or tap changed since the action was queued.
# List queued agent actions
Source: https://docs.datris.ai/api-reference/agent-policy/list-queued-agent-actions
/openapi.yaml get /api/v1/approvals
Newest first. An agent-initiated caller sees only the actions it queued; a person sees everything.
# Read one queued action
Source: https://docs.datris.ai/api-reference/agent-policy/read-one-queued-action
/openapi.yaml get /api/v1/approvals/{id}
# Read the agent policy
Source: https://docs.datris.ai/api-reference/agent-policy/read-the-agent-policy
/openapi.yaml get /api/v1/policy
Per-action auto / approve / deny for agent-initiated changes. Anyone may read it (agents consult it before acting). `enabled` is false when USE_AGENT_POLICY is off, in which case every action is auto.
# Reject a queued action (admin / editor)
Source: https://docs.datris.ai/api-reference/agent-policy/reject-a-queued-action-admin-editor
/openapi.yaml post /api/v1/approvals/{id}/reject
# Replace the agent policy (admin)
Source: https://docs.datris.ai/api-reference/agent-policy/replace-the-agent-policy-admin
/openapi.yaml put /api/v1/policy
Admin-only, and never permitted from an agent-initiated request regardless of key capabilities. Unknown action keys, bad modes or malformed overrides are rejected with 400 naming the problem.
# AI data profiling
Source: https://docs.datris.ai/api-reference/ai/ai-data-profiling
/openapi.yaml post /api/v1/pipeline/profile
Upload a file and receive AI-generated summary statistics, quality issues, and suggested validation rules.
# AI-powered question answering
Source: https://docs.datris.ai/api-reference/ai/ai-powered-question-answering
/openapi.yaml post /api/v1/ai/answer
Use AI to answer a question based on provided context. Ideal for RAG workflows — retrieve relevant chunks via vector search, then pass them as context along with the user's question.
# AI schema generation
Source: https://docs.datris.ai/api-reference/ai/ai-schema-generation
/openapi.yaml post /api/v1/pipeline/generate
Upload a file and receive an AI-generated pipeline configuration with inferred field names and types.
# Discover invokable models for a provider
Source: https://docs.datris.ai/api-reference/ai/discover-invokable-models-for-a-provider
/openapi.yaml get /api/v1/ai/models
Live per-provider model discovery, reflecting what the configured account can actually invoke (unlike the static catalog). Currently supports provider=bedrock: the server calls the AWS Bedrock discovery APIs (ListFoundationModels + ListInferenceProfiles) with the configured AWS credentials and merges them into invokable ids — a model without on-demand support is returned as its cross-region inference-profile id (e.g. us.anthropic....). Results are cached for 5 minutes. The Configuration UI overlays catalog labels on this list and falls back to the static catalog when discovery is unavailable.
# Get the AI model catalog
Source: https://docs.datris.ai/api-reference/ai/get-the-ai-model-catalog
/openapi.yaml get /api/v1/ai/model-catalog
Same-origin proxy for the remote model catalog (datris.ai/models.json) used by the Configuration UI's model pickers. Successful fetches are cached for 5 minutes; on fetch failure the stale cache is served if one exists.
# Capability catalog (admin)
Source: https://docs.datris.ai/api-reference/api-keys/capability-catalog-admin
/openapi.yaml get /api/v1/keys/capabilities/catalog
Admin-only. Every capability resource, its actions, and the scope keys it accepts — drives the Keys UI capability editor.
# Capability templates (admin)
Source: https://docs.datris.ai/api-reference/api-keys/capability-templates-admin
/openapi.yaml get /api/v1/keys/templates
Admin-only. Common capability-set starting points (read-only, rag-builder, reporting, ops, full-access) offered by the Keys UI wizard.
# Issue a new API key (admin)
Source: https://docs.datris.ai/api-reference/api-keys/issue-a-new-api-key-admin
/openapi.yaml post /api/v1/keys
Admin-only. The returned `value` is shown ONCE and cannot be retrieved again — store it immediately.
# List API keys (admin)
Source: https://docs.datris.ai/api-reference/api-keys/list-api-keys-admin
/openapi.yaml get /api/v1/keys
Admin-only. Lists every issued API key with its capabilities and audit metadata. Key VALUES are never returned here — only on issue and rotate, one time. Keys issued before capability metadata existed are flagged `isLegacyFullAccess`.
# Revoke an API key (admin)
Source: https://docs.datris.ai/api-reference/api-keys/revoke-an-api-key-admin
/openapi.yaml delete /api/v1/keys/{label}
Admin-only. Marks the key revoked; it immediately stops authenticating, and subsequent attempts are logged as "revoked key".
# Rotate an API key (admin)
Source: https://docs.datris.ai/api-reference/api-keys/rotate-an-api-key-admin
/openapi.yaml post /api/v1/keys/{label}/rotate
Admin-only. Generates a new value for the key; the previous value stops working immediately. Capabilities are preserved. The returned value is shown once — update every caller that held the old value.
# Assistant chat (SSE)
Source: https://docs.datris.ai/api-reference/assistant/assistant-chat-sse
/openapi.yaml post /api/v1/assistant/chat
Runs the build-mode Assistant agent loop and streams agent-loop events (text deltas, tool calls, tool results, errors) as Server-Sent Events. The client cancels by disconnecting. Attachments reference files previously staged via `/assistant/attachment`.
# Initialize the Assistant
Source: https://docs.datris.ai/api-reference/assistant/initialize-the-assistant
/openapi.yaml get /api/v1/assistant/init
UI mount call — warms the MCP client caches and returns the tool catalog, the workflow reference resource, and the active chat model.
# Stage a chat attachment
Source: https://docs.datris.ai/api-reference/assistant/stage-a-chat-attachment
/openapi.yaml post /api/v1/assistant/attachment
Stages a file dropped into the Assistant chat. The bytes are cached server-side (tenant-scoped, TTL'd) and only the returned handle plus a small content sample travel through the chat; the platform substitutes the real bytes when the agent calls a file tool with the `attachmentId`. Limit 25 MB per file.
# Change the current user's password
Source: https://docs.datris.ai/api-reference/auth/change-the-current-users-password
/openapi.yaml post /api/v1/auth/change-password
Requires an authenticated session. The current-password check is skipped on a first-time set (`mustSetPassword`).
# Create a user (admin)
Source: https://docs.datris.ai/api-reference/auth/create-a-user-admin
/openapi.yaml post /api/v1/auth/users
Admin-only. Omit `password` to create the user in first-login mode — they set their own password on first sign-in.
# Delete a user (admin)
Source: https://docs.datris.ai/api-reference/auth/delete-a-user-admin
/openapi.yaml delete /api/v1/auth/users/{username}
Admin-only. The built-in `admin` recovery account can never be deleted, and the last remaining admin cannot be deleted.
# Get the current user
Source: https://docs.datris.ai/api-reference/auth/get-the-current-user
/openapi.yaml get /api/v1/auth/me
UI bootstrap probe — returns the session's user, or 401 if there is no session.
# List users (admin)
Source: https://docs.datris.ai/api-reference/auth/list-users-admin
/openapi.yaml get /api/v1/auth/users
Admin-only. Timestamps are formatted using the server's configured date format and timezone.
# Log in
Source: https://docs.datris.ai/api-reference/auth/log-in
/openapi.yaml post /api/v1/auth/login
Authenticates a user and sets an HTTP-only session cookie. Only available when user auth is enabled on the server. For a user whose password has not been set yet (`mustSetPassword`), any password is accepted and the client must immediately call `/auth/change-password`.
# Log out
Source: https://docs.datris.ai/api-reference/auth/log-out
/openapi.yaml post /api/v1/auth/logout
Deletes the server-side session and clears the session cookie.
# Single sign-on (OIDC) callback
Source: https://docs.datris.ai/api-reference/auth/single-sign-on-oidc-callback
/openapi.yaml get /api/v1/auth/oidc/callback
The redirect URI registered at the identity provider. Checks `state` against the `datris-oidc-tx` cookie, exchanges the code (client secret from Vault, PKCE verifier), validates the ID token, resolves the Datris user and sets the session cookie. Every outcome is a redirect: `/` on success, otherwise `/login?ssoError=` with one of `sso_denied`, `sso_failed`, `sso_no_account`, `sso_unverified_email`, `sso_identity_changed`. Provider error text is logged on the server, never returned. Recorded in the audit log as `auth` / `login`.
# Start single sign-on (OIDC)
Source: https://docs.datris.ai/api-reference/auth/start-single-sign-on-oidc
/openapi.yaml get /api/v1/auth/oidc/login
Browser navigation, not an API call. Sets the short-lived HttpOnly `datris-oidc-tx` cookie (state, nonce and PKCE verifier; ten minutes, `SameSite=Lax`, path `/api/v1/auth/oidc`) and redirects to the identity provider's authorization endpoint with `response_type=code`, `code_challenge_method=S256`, `state` and `nonce`. Only available when user auth and OIDC are both on.
# Update a user (admin)
Source: https://docs.datris.ai/api-reference/auth/update-a-user-admin
/openapi.yaml patch /api/v1/auth/users/{username}
Admin-only. Change a user's role and/or reset their password (back to first-login mode). The built-in `admin` user is locked to the admin role, and the last remaining admin cannot be demoted.
# Catalog chat (SSE)
Source: https://docs.datris.ai/api-reference/chat/catalog-chat-sse
/openapi.yaml post /api/v1/catalog-chat/chat
Agent chat for organizing the inventory — group, move, rename, and describe taps and pipelines. Full tool catalog with catalog tools prioritized. The optional `context` object is a catalog inventory snapshot that grounds answers in what the user is looking at. Streams the same SSE event format as `/assistant/chat`.
# Configuration chat (SSE)
Source: https://docs.datris.ai/api-reference/chat/configuration-chat-sse
/openapi.yaml post /api/v1/config-chat/chat
Admin-only agent chat over the Configuration sub-tabs (AI providers, secrets, data sources, code repository, users, API keys, agent policy, audit log, doctor). Uses Datris-defined configuration tools, not the MCP tool catalog; user and API-key tools are present only when the matching sub-tab is shown. Mutating tools perform nothing on the first call: they answer `needs_confirmation` and the stream emits a `confirm_request` event; the change runs only after the user confirms it in the panel. Secret values are never passed through the model; they are collected through a `secret_request` form event. The optional `context` object is `{tab}`, the active sub-tab. Streams the same SSE event format as `/assistant/chat`.
# Ops chat (SSE)
Source: https://docs.datris.ai/api-reference/chat/ops-chat-sse
/openapi.yaml post /api/v1/ops-chat/chat
Agent chat for the Ops dashboard side panel — explains failures and acts on runs by name, with operational tools (run, status, kill) prioritized. The optional `context` object is a dashboard snapshot that grounds answers in the failures and volumes currently on screen. Streams the same SSE event format as `/assistant/chat`.
# Search chat (SSE)
Source: https://docs.datris.ai/api-reference/chat/search-chat-sse
/openapi.yaml post /api/v1/search-chat/chat
Conversational data discovery for the Search tab — finds the data that answers a question, queries/searches it, and answers with citations. The tool catalog is filtered server-side to READ-ONLY discovery/query/search tools; the agent never sees mutating tools. The optional `context` object may carry a catalog scope, e.g. `{"catalog": "finance"}` (or "Uncataloged"; "All"/empty means unscoped). Streams the same SSE event format as `/assistant/chat`.
# Get the tenant code repository configuration
Source: https://docs.datris.ai/api-reference/code-repository/get-the-tenant-code-repository-configuration
/openapi.yaml get /api/v1/code-repo
Returns the code repository connection used for tap script storage. The auth token itself lives in Vault and is never returned.
# Save the tenant code repository configuration
Source: https://docs.datris.ai/api-reference/code-repository/save-the-tenant-code-repository-configuration
/openapi.yaml put /api/v1/code-repo
Validates the connection when `enabled` (token can push, branch exists — verified via the repository `permissions` block, no probe commits) and persists the configuration.
# Test a code repository connection
Source: https://docs.datris.ai/api-reference/code-repository/test-a-code-repository-connection
/openapi.yaml post /api/v1/code-repo/test
Round-trip check for the posted configuration — repository reachable, token has push access, branch exists. Always returns 200 with an `ok` flag.
# Configuration API
Source: https://docs.datris.ai/api-reference/config-api
Upload configuration files and generate schemas
The Configuration API lets you upload validation schemas and generate JSON Schema or XSD from sample data using AI.
## Upload Config File
Upload a validation schema to the platform's configuration store (MinIO).
```
POST /api/v1/config/upload
Content-Type: multipart/form-data
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `file` | form-data | Yes | The configuration file to upload |
| `type` | form-data | Yes | File type: `validation-schema` or `javascript` |
The uploaded file is stored at `{env}-config/{type}/{filename}` in MinIO and can be referenced in pipeline configurations.
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/config/upload \
-F "file=@my-schema.json" \
-F "type=validation-schema"
```
### Response
```json theme={null}
{
"filename": "my-schema.json",
"path": "s3://oss-config/validation-schema/my-schema.json"
}
```
***
## Generate Schema from Sample Data
Use AI to generate a JSON Schema or XSD from sample data. The generated schema is stored in MinIO and can be referenced in pipeline configurations for data quality validation.
```
POST /api/v1/config/generate-schema
Content-Type: application/json
```
| Field | Type | Required | Description |
| - | - | - | - |
| `type` | string | Yes | Schema type: `json-schema` or `xsd` |
| `name` | string | Yes | Name for the generated schema file |
| `sampleData` | string | Yes | Sample data content for the AI to analyze |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/config/generate-schema \
-H "Content-Type: application/json" \
-d '{
"type": "json-schema",
"name": "orders",
"sampleData": "[{\"id\": 1, \"email\": \"jane@example.com\", \"total\": 249.99}]"
}'
```
### Response
```json theme={null}
{
"filename": "orders.json",
"path": "s3://oss-config/validation-schema/orders.json",
"schema": "{ ... generated JSON Schema ... }"
}
```
For `xsd` type, the filename uses `.xsd` extension and the response contains a generated W3C XML Schema.
# AI-generate a validation schema
Source: https://docs.datris.ai/api-reference/configuration/ai-generate-a-validation-schema
/openapi.yaml post /api/v1/config/generate-schema
Uses AI to generate a JSON Schema (Draft 4) or W3C XSD from sample data. The schema is stored in MinIO.
# Upload a configuration file
Source: https://docs.datris.ai/api-reference/configuration/upload-a-configuration-file
/openapi.yaml post /api/v1/config/upload
Upload a JSON Schema for pipeline dataQuality schema validation to storage. The uploaded file can then be referenced by pipeline configurations.
# Health Check API
Source: https://docs.datris.ai/api-reference/health-api
Health check endpoints
The Health Check API provides endpoints to check the connectivity status of all backend services, and to list which destinations and vector stores are actually available in this deployment.
## Check Service Health
```
GET /api/v1/health/services
```
Returns the health status of all configured backend services.
### Response
```json theme={null}
{
"postgres": {"status": "up", "message": "Connected"},
"mongodb": {"status": "up", "message": "Connected"},
"minio": {"status": "up", "message": "Connected"},
"activemq": {"status": "up", "message": "Connected"},
"kafka": {"status": "up", "message": "Connected"},
"qdrant": {"status": "not_configured"},
"weaviate": {"status": "not_configured"},
"milvus": {"status": "not_configured"},
"chroma": {"status": "not_configured"},
"pgvector": {"status": "not_configured"}
}
```
### Status Values
| Status | Description |
| - | - |
| `up` | Service is reachable and responding |
| `down` | Service is configured but not reachable (includes error message) |
| `not_configured` | No Vault secret configured for this service |
### Services Checked
**Core infrastructure** (always checked):
* `postgres` — PostgreSQL database
* `mongodb` — MongoDB database
* `minio` — MinIO object store
* `activemq` — ActiveMQ message queue
* `kafka` — Apache Kafka
**Vector databases** (checked only if configured via Vault secrets):
* `qdrant` — Qdrant vector database
* `weaviate` — Weaviate vector database
* `milvus` — Milvus vector database
* `chroma` — Chroma vector database
* `pgvector` — PostgreSQL with pgvector extension
### Example
```bash theme={null}
curl http://localhost:8080/api/v1/health/services
```
***
## List Available Destinations
```
GET /api/v1/destinations/available
```
Returns the structured destinations that are usable in this deployment, as a JSON array of destination names. This drives the pipeline wizard's and Assistant's destination pickers.
Two availability semantics are used on purpose:
* **Self-hosted destinations** (`mongodb`, `postgres`, `objectstore`) are **live-probed** — a destination is listed only if the service is actually reachable. The presence of a Vault secret alone is not enough, because the compose stack seeds secrets even when a service is disabled.
* **External SaaS destinations** (`snowflake`, `databricks`) have no local service to probe, so they are listed when any Platform secret contains a complete set of credentials for that destination. Credential secret names are user-chosen per pipeline, so every Platform secret is scanned rather than a fixed name looked up.
### Response
```json theme={null}
["mongodb", "postgres", "objectstore", "snowflake"]
```
### Example
```bash theme={null}
curl http://localhost:8080/api/v1/destinations/available
```
***
## List Available Vector Stores
```
GET /api/v1/vector-stores/available
```
Returns the vector stores that are live and reachable, as a JSON array of store keys. This drives the document-tap pipeline wizard's store picker and the Search tab's store dropdown, and is the recommended way to discover which stores can be searched via [`POST /api/v1/search/{store}`](/api-reference/search-api).
Each candidate store (`qdrant`, `weaviate`, `pgvector`, `milvus`, `chroma`) is actively probed using the same connectivity checks as `/health/services` — only stores whose status is `up` are returned. A store with a configured Vault secret but an unreachable service is excluded.
### Response
```json theme={null}
["pgvector", "qdrant"]
```
### Example
```bash theme={null}
curl http://localhost:8080/api/v1/vector-stores/available
```
# Check backend service health
Source: https://docs.datris.ai/api-reference/health/check-backend-service-health
/openapi.yaml get /api/v1/health/services
Returns the health status of all configured backend services including PostgreSQL, MongoDB, MinIO, ActiveMQ, Kafka, and vector databases (Qdrant, Weaviate, Milvus, Chroma, pgvector). Each service reports "up", "down", or "not_configured".
# List live, reachable vector stores
Source: https://docs.datris.ai/api-reference/health/list-live-reachable-vector-stores
/openapi.yaml get /api/v1/vector-stores/available
Returns the subset of `[qdrant, weaviate, pgvector, milvus, chroma]` whose Vault secret is present AND whose service responds to a probe. Drives the document-tap pipeline wizard's store picker. Secret presence alone isn't sufficient — the dev stack seeds placeholder secrets for every store, so this endpoint must actually probe each one (2s timeout per store, ~10s worst case if everything is unreachable).
# List usable structured destinations
Source: https://docs.datris.ai/api-reference/health/list-usable-structured-destinations
/openapi.yaml get /api/v1/destinations/available
Returns the subset of `[mongodb, postgres, objectstore, snowflake, databricks]` usable in this deployment. Drives the pipeline wizard's and Assistant's destination picker. Two availability semantics on purpose: the self-hosted destinations (mongodb, postgres, objectstore) are live-probed — secret presence alone isn't a reliable signal because the compose stack seeds their secrets even when a service is disabled; the external SaaS destinations (snowflake, databricks) have no local service to probe, so they are included when any Platform secret carries a complete credential set for them.
# Data Ingestion API
Source: https://docs.datris.ai/api-reference/ingestion-api
Data ingestion endpoints
## Upload a File
Upload a data file for processing by a configured pipeline.
```
POST /api/v1/pipeline/upload
Content-Type: multipart/form-data
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `file` | form-data | Yes | The file to upload |
| `pipeline` | form-data | Yes | Target pipeline name |
| `publishertoken` | form-data | No | Publisher identifier for tracking |
**Behavior:**
* **Compressed files** (`.zip`, `.gz`, `.tar`, `.jar`): Extracted in-process. For a CSV pipeline with several files in the archive, the files are concatenated (headers on files 2+ skipped) and submitted as one batch job; otherwise each extracted file is submitted as its own job
* **Uncompressed files**: Processed immediately in-memory
* **Schema evolution**: If a CSV file contains new columns not in the pipeline schema, they are automatically added (as `string` type) and the destination table is altered. See [Schema Evolution](/schemas#schema-evolution).
**Example:**
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline/upload \
-F "file=@/path/to/data.csv" \
-F "pipeline=sales_data" \
-F "publishertoken=batch-001"
```
**Response:** `200 OK` with the pipeline token (for uncompressed files):
```
pt-abc12345-6789-...
```
For compressed files, the response is `200 OK` with the single pipeline token of the batch job (CSV batch mode, or a single-file archive), or the plain-text summary `N file(s) submitted` when each extracted file was submitted as its own job.
***
## Generate Pipeline Schema
Upload a CSV file to automatically infer the schema and generate a partial pipeline configuration.
```
POST /api/v1/pipeline/generate
Content-Type: multipart/form-data
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `file` | form-data | Yes | Data file to analyze (CSV, JSON, or XML) |
| `pipeline` | form-data | No | Pipeline name (auto-derived from filename if omitted) |
| `delimiter` | form-data | No | CSV delimiter (default: `,`) |
| `header` | form-data | No | Whether file has header row |
| `allStrings` | form-data | No | If `true`, all fields are typed as `string` (default: `false`) |
**Example:**
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline/generate \
-F "file=@/path/to/sample.csv" \
-F "pipeline=my_pipeline"
```
**Response:** `200 OK` with a partial PipelineConfig JSON:
```json theme={null}
{
"name": "my_pipeline",
"source": {
"fileAttributes": {
"csvAttributes": {
"delimiter": ",",
"header": true
}
},
"schemaProperties": {
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "created_at", "type": "string"}
]
}
},
"destination": {
"database": {
"dbName": "datris",
"schema": "public",
"table": "my_pipeline",
"usePostgres": true
}
}
}
```
**Inferred types:** `boolean`, `int`, `bigint`, `float`, `double`, `string`, `date`, `timestamp`
**Note:** For CSV files, the AI analyzes the content and infers types. For JSON and XML files, a default config is generated with a single `_json` or `_xml` string field. When `allStrings` is `true`, all fields are set to `string` (used by the MCP server for reliable ingestion). Edit the generated JSON to add your destination configuration before registering it with `POST /pipeline`.
***
## Profile Data
Upload a data file for AI-powered profiling — get summary statistics, quality issues, and suggested validation rules.
```
POST /api/v1/pipeline/profile
Content-Type: multipart/form-data
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `file` | form-data | Yes | Data file to profile |
| `delimiter` | form-data | No | CSV delimiter (default: `,`) |
| `header` | form-data | No | Whether file has header row (default: `true`) |
| `sampleSize` | form-data | No | Number of rows to sample (default: `200`) |
**Example:**
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline/profile \
-F "file=@/path/to/data.csv" \
-F "sampleSize=200"
```
**Response:** `200 OK` with a JSON profile including summary statistics, quality issues, recommendations, and suggested data quality rules. See [AI Data Profiling](/ai-data-profiling) for the full response format.
# Upload a file for processing
Source: https://docs.datris.ai/api-reference/ingestion/upload-a-file-for-processing
/openapi.yaml post /api/v1/pipeline/upload
Upload a data file to a registered pipeline. Returns a pipeline token for tracking job status.
# Clear MCP activity history
Source: https://docs.datris.ai/api-reference/mcp/clear-mcp-activity-history
/openapi.yaml delete /api/v1/mcp/activity
Clears the MCP server's activity buffer so the cleared state survives page reloads. Live sessions are untouched.
# Get MCP server activity
Source: https://docs.datris.ai/api-reference/mcp/get-mcp-server-activity
/openapi.yaml get /api/v1/mcp/activity
Proxies the MCP server's activity feed (agent sessions and tool calls) with server-side enrichment: timestamps are formatted with the platform's date settings and each session/call is labeled with the tenant or key name resolved from its API-key hint. Always returns 200 — when the MCP server is unreachable or auth fails, the payload carries an `error` message and an `errorKind` of "unreachable" or "auth" with empty session/call arrays.
# Metadata API
Source: https://docs.datris.ai/api-reference/metadata-api
Database and vector store metadata discovery
The Metadata API lets you explore the structure of PostgreSQL, MongoDB, and vector databases managed by the platform. Use these endpoints to discover what data is available before writing queries or running searches.
## PostgreSQL
### List Databases
```
GET /api/v1/metadata/postgres/databases
```
Returns a JSON array of PostgreSQL database names.
***
### List Schemas
```
GET /api/v1/metadata/postgres/schemas
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `database` | query | configured Postgres database | Database name. Defaults to the configured Postgres database (`postgres.database`, which is `datris` on a default install) |
Returns a JSON array of schema names. System schemas (`information_schema`, `pg_catalog`, `pg_toast`) are excluded.
***
### List Tables
```
GET /api/v1/metadata/postgres/tables
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `database` | query | configured Postgres database | Database name. Defaults to the configured Postgres database (`postgres.database`, which is `datris` on a default install) |
| `schema` | query | `public` | Schema name |
| `vectorOnly` | query | `false` | If `true`, return only pgvector tables (tables with an `embedding` column) |
Returns a JSON array of table names. When `vectorOnly` is `false`, pgvector tables are excluded from the results.
***
### List Columns
```
GET /api/v1/metadata/postgres/columns
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `database` | query | configured Postgres database | Database name. Defaults to the configured Postgres database (`postgres.database`, which is `datris` on a default install) |
| `schema` | query | `public` | Schema name |
| `table` | query | (required) | Table name |
Returns a JSON array of column definitions:
```json theme={null}
[
{"name": "id", "type": "bigint"},
{"name": "email", "type": "text"},
{"name": "created_at", "type": "timestamp"}
]
```
***
## MongoDB
### List Databases
```
GET /api/v1/metadata/mongodb/databases
```
Returns a JSON array of MongoDB database names. System databases (`admin`, `config`, `local`) and the platform's internal database are excluded.
***
### List Collections
```
GET /api/v1/metadata/mongodb/collections
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `database` | query | (all databases) | Filter to a specific database |
When `database` is provided, returns collection names from that database. When omitted, returns collections from all databases with a `database.collection` namespace prefix.
***
## Vector Stores
### List Qdrant Collections
```
GET /api/v1/metadata/qdrant/collections
```
Returns a JSON array of Qdrant collection names. Returns `[]` if Qdrant is not configured.
***
### List Weaviate Classes
```
GET /api/v1/metadata/weaviate/classes
```
Returns a JSON array of Weaviate class names. Returns `[]` if Weaviate is not configured.
***
### List Milvus Collections
```
GET /api/v1/metadata/milvus/collections
```
Returns a JSON array of Milvus collection names. Returns `[]` if Milvus is not configured.
***
### List Chroma Collections
```
GET /api/v1/metadata/chroma/collections
```
Returns a JSON array of Chroma collection names. Returns `[]` if Chroma is not configured.
***
## Example
```bash theme={null}
# Discover databases
curl -s http://localhost:8080/api/v1/metadata/postgres/databases | jq .
# Explore a schema
curl -s "http://localhost:8080/api/v1/metadata/postgres/tables?database=datris&schema=public" | jq .
# Inspect a table
curl -s "http://localhost:8080/api/v1/metadata/postgres/columns?table=stock_prices" | jq .
# List vector collections
curl -s http://localhost:8080/api/v1/metadata/qdrant/collections | jq .
```
# List Chroma collections
Source: https://docs.datris.ai/api-reference/metadata/list-chroma-collections
/openapi.yaml get /api/v1/metadata/chroma/collections
List all collections in the Chroma vector database. Returns an empty array if Chroma is not configured.
# List Milvus collections
Source: https://docs.datris.ai/api-reference/metadata/list-milvus-collections
/openapi.yaml get /api/v1/metadata/milvus/collections
List all collections in the Milvus vector database. Returns an empty array if Milvus is not configured.
# List MongoDB collections
Source: https://docs.datris.ai/api-reference/metadata/list-mongodb-collections
/openapi.yaml get /api/v1/metadata/mongodb/collections
List MongoDB collections. If database is specified, lists collections in that database. Otherwise lists all collections across all databases in "db.collection" format.
# List MongoDB databases
Source: https://docs.datris.ai/api-reference/metadata/list-mongodb-databases
/openapi.yaml get /api/v1/metadata/mongodb/databases
List all MongoDB databases available in the platform. Excludes system databases (admin, config, local).
# List PostgreSQL databases
Source: https://docs.datris.ai/api-reference/metadata/list-postgresql-databases
/openapi.yaml get /api/v1/metadata/postgres/databases
List all PostgreSQL databases available in the platform.
# List PostgreSQL schemas
Source: https://docs.datris.ai/api-reference/metadata/list-postgresql-schemas
/openapi.yaml get /api/v1/metadata/postgres/schemas
List all schemas in a PostgreSQL database.
# List PostgreSQL tables
Source: https://docs.datris.ai/api-reference/metadata/list-postgresql-tables
/openapi.yaml get /api/v1/metadata/postgres/tables
List all tables in a PostgreSQL schema. Set vectorOnly to true to filter to tables with an embedding column (pgvector tables).
# List Qdrant collections
Source: https://docs.datris.ai/api-reference/metadata/list-qdrant-collections
/openapi.yaml get /api/v1/metadata/qdrant/collections
List all collections in the Qdrant vector database. Returns an empty array if Qdrant is not configured.
# List table columns
Source: https://docs.datris.ai/api-reference/metadata/list-table-columns
/openapi.yaml get /api/v1/metadata/postgres/columns
List all columns and their data types for a specific PostgreSQL table.
# List Weaviate classes
Source: https://docs.datris.ai/api-reference/metadata/list-weaviate-classes
/openapi.yaml get /api/v1/metadata/weaviate/classes
List all classes in the Weaviate vector database. Returns an empty array if Weaviate is not configured.
# Pipeline Configuration API
Source: https://docs.datris.ai/api-reference/pipeline-api
Pipeline configuration endpoints
Manage pipeline configurations through CRUD operations.
## Get a Pipeline
```
GET /api/v1/pipeline?pipeline={name}
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `pipeline` | query | Yes | Pipeline name |
**Example:**
```bash theme={null}
curl "http://localhost:8080/api/v1/pipeline?pipeline=sales_data"
```
**Response:** `200 OK` with the full PipelineConfig JSON.
***
## Unity Catalog sync state
```
GET /api/v1/pipelines/{name}/unity-catalog
```
The last Unity Catalog sync for a pipeline that sets `unityCatalog.enabled` (or that the install default `DATRIS_UNITY_CATALOG_DEFAULT=enabled` turned on): the metadata push and lineage for a Databricks pipeline (see [Databricks](/destinations/databricks#unity-catalog-metadata)), or the table registration for an object store Iceberg pipeline (see [Register Iceberg tables](/destinations/unity-catalog#register-iceberg-tables)).
**Example:**
```bash theme={null}
curl http://localhost:8080/api/v1/pipelines/orders_pipeline/unity-catalog
```
**Response:** `200 OK`. `enabled` says whether Unity Catalog is on for the pipeline and `enabledBy` says who decided: `pipeline` (the pipeline's own `unityCatalog` block, including an `"enabled": false` opt-out), `default` (the install default turned on a Databricks pipeline with no block), or `null` (off, no block). `lineageEnabled` follows the same resolution. `syncSwitchedOff` is `true` while `DATRIS_UNITY_CATALOG_SYNC=false`: nothing is written to Unity Catalog even when `enabled` is `true`. `coordinates` gives the Unity Catalog table (Databricks destinations only). `state` is `never` (no sync yet), `synced`, or `error` (with `lastError` holding the warehouse's message, or when the last `catalogMode: rest` run was refused; see `restRefusedReason`). `lastSyncAt` and `lastRunId` identify the last sync. `lineage` is the lineage publish status: `off` (`unityCatalog.lineage` is `false`, or the push is off), `never`, `published`, or `error` (the failure is a `lastError` line starting with `uc-lineage:`); `lastLineageAt` is when lineage was last published and `lineageHash` identifies the published definition (see [Lineage in Unity Catalog](/destinations/unity-catalog#lineage-in-unity-catalog)). For an object store Iceberg pipeline, `coordinates` names the registered table (`catalog.schema.table`, with `kind: "iceberg"` and the table's `location`), `registerEnabled` says whether registration is on, and `register` is `off`, `never`, `registered`, `stale` (Unity Catalog still points at an older metadata file) or `error` (the failure is a `lastError` line starting with `uc-register:`); `registeredMetadataLocation` and `lastRegisterAt` record the last registration. `catalogMode` is `register`, `rest` or `managed` (see [Keep the catalog current](/destinations/unity-catalog#keep-the-catalog-current-catalogmode-rest) and [Databricks: catalogMode managed](/destinations/unity-catalog#databricks-catalogmode-managed)); in `managed` mode `register` is `managed` when the last run committed through the catalog, `restMetadataLocation` is under the location the catalog chose, and `coordinates.location` is that location once known; in `rest` mode `register` is `rest` when the last run committed through the catalog and `refused` when it wrote by path instead, `restMetadataLocation` and `lastRestCommitAt` record the last catalog commit, and `restRefusedReason` says why a run was refused. `restCreatedTable` names a table the catalog created for the pipeline at a location Datris refused (a Databricks managed table): it was never written to, and an admin has to drop it. `404` if the pipeline does not exist.
```json theme={null}
{
"pipeline": "orders_pipeline",
"enabled": true,
"enabledBy": "pipeline",
"syncSwitchedOff": false,
"coordinates": { "catalog": "datris", "schema": "default", "table": "orders", "qualified": "datris.default.orders" },
"state": "synced",
"lastSyncAt": "2026-09-29T12:00:00Z",
"lastRunId": "...",
"lineageEnabled": true,
"lineage": "published",
"lineageHash": "...",
"lastLineageAt": "2026-09-29T12:00:01Z",
"registerEnabled": false,
"register": "off"
}
```
For an object store Iceberg pipeline:
```json theme={null}
{
"pipeline": "orders_lake",
"enabled": true,
"coordinates": { "catalog": "analytics", "schema": "default", "table": "orders_lake", "qualified": "analytics.default.orders_lake", "kind": "iceberg", "location": "s3a://my-lake/orders" },
"registerEnabled": true,
"register": "registered",
"registeredMetadataLocation": "s3a://my-lake/orders/metadata/00001-....metadata.json",
"lastRegisterAt": "2026-09-29T12:00:02Z"
}
```
***
## List All Pipelines
```
GET /api/v1/pipelines
```
**Example:**
```bash theme={null}
curl http://localhost:8080/api/v1/pipelines
```
**Response:** `200 OK` with an array of all PipelineConfig objects.
***
## Create or Update a Pipeline
```
POST /api/v1/pipeline
Content-Type: application/json
```
**Body:** Full PipelineConfig JSON (see [Pipeline Configuration](/pipeline-configuration)).
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `changeNote` | query | No | Note recorded on the saved pipeline version |
**Example:**
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline \
-H "Content-Type: application/json" \
-d '{
"name": "sales_data",
"source": {
"schemaProperties": {
"fields": [
{"name": "id", "type": "int"},
{"name": "amount", "type": "double"}
]
},
"fileAttributes": {
"csvAttributes": {"delimiter": ",", "header": true, "encoding": "UTF-8"}
}
},
"destination": {
"database": {
"dbName": "mydb",
"schema": "public",
"table": "sales",
"usePostgres": true
}
}
}'
```
**Response:** `200 OK`
```json theme={null}
{"warnings": []}
```
`warnings` is advisory: the pipeline is saved either way. It is empty unless something about the configuration will not work as written, for example an object store Iceberg pipeline whose Unity Catalog secret points at a Databricks workspace with `catalogMode` `register` or `rest` (Databricks cannot serve either; the warning says to set `catalogMode` `managed` or use the databricks destination). Each warning is also written to the server log.
**Validation:** The configuration is validated before saving. Invalid configurations return `400` with `{"error": ""}` and nothing is saved; `500` means the server itself failed. See [Pipeline Configuration](/pipeline-configuration) for all constraints.
***
## Delete a Pipeline
```
DELETE /api/v1/pipeline?pipeline={name}
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `pipeline` | query | Yes | Pipeline name |
| `deleteData` | query | No | Delete destination data (default: `true`). Only takes effect together with `deleteConfig=false`: deleting the configuration always deletes the data too, so `deleteData=false` on its own still removes the data. |
| `deleteConfig` | query | No | Delete pipeline configuration (default: `true`). Set to `false` to delete data only while keeping the pipeline config. |
There is no way to delete a pipeline's configuration and keep its destination data; a pipeline with no configuration would leave tables, collections, and files behind that nothing owns.
**Examples:**
```bash theme={null}
# Delete pipeline and all data
curl -X DELETE "http://localhost:8080/api/v1/pipeline?pipeline=sales_data"
# Delete data only, keep pipeline configuration
curl -X DELETE "http://localhost:8080/api/v1/pipeline?pipeline=sales_data&deleteData=true&deleteConfig=false"
```
**Response:** `200 OK` with a JSON body:
```json theme={null}
{ "warnings": [] }
```
`warnings` lists anything left for you to handle. For an object-store Iceberg pipeline whose table was committed through Unity Catalog (`catalogMode: rest`), it says `Unity Catalog still holds ..
; have an admin drop it`, because Datris removes the files but never drops the catalog table (see [Unity Catalog](/destinations/unity-catalog)). For a `catalogMode: managed` table it says `Unity Catalog owns ..
and its files (catalogMode managed); Datris deleted nothing there; drop ..
in Unity Catalog to remove the data`.
***
## AI Schema Generation
To generate a pipeline configuration automatically from an uploaded file, see the [AI Schema Generation API](/api-reference/schema-generation-api).
***
## Authentication
If `useApiKeys` is enabled, all requests must include the `x-api-key` header:
```bash theme={null}
curl -H "x-api-key: your-api-key" http://localhost:8080/api/v1/pipelines
```
API keys are stored in Vault under `{environment}/api-keys` (`oss/api-keys` on a default install). `secrets.apiKeysSecretName` in `application.yaml` tells the validator where to read them and should point at that same path — the Keys UI and multi-tenant resolution always write to `{environment}/api-keys`.
# Adopt a CodeGen script edited in the code repository
Source: https://docs.datris.ai/api-reference/pipelines/adopt-a-codegen-script-edited-in-the-code-repository
/openapi.yaml post /api/v1/pipelines/{name}/codegen-scripts/{kind}/pull
For a script stored in the code repository: records the commit at the head of the configured branch (origin `repository`), so later runs execute the file as it is in the repository now. No commit is made. Later saves keep the adopted script while the instruction and source schema are unchanged. Requires `pipeline:create`. Audited.
# Apply destination column types
Source: https://docs.datris.ai/api-reference/pipelines/apply-destination-column-types
/openapi.yaml post /api/v1/pipeline/dest-types
Applies column types to an all-string destination. Destination-first: if data has already landed the table is migrated before the config changes — postgres retypes in place, snowflake/databricks validate every landed value with TRY_CAST and then swap the table. Any landed value that will not cast fails the whole apply with the column named and nothing changed. On success the pipeline definition is updated as a new version. The migration locks or replaces the table — do not apply while a run is in flight.
# Cancel a catalog's ordered enable
Source: https://docs.datris.ai/api-reference/pipelines/cancel-a-catalogs-ordered-enable
/openapi.yaml delete /api/v1/catalog/{name}/enable-ordered
The test, run or load wait in progress finishes (a running tap script is not killed); every tap not yet started is marked `cancelled` (a tap already enabled whose run had not started reads `enabled; run cancelled`). Requires `tap:run`, checked against the key's catalog scope and the job starter as owner; 403 otherwise.
# Create or update a pipeline configuration
Source: https://docs.datris.ai/api-reference/pipelines/create-or-update-a-pipeline-configuration
/openapi.yaml post /api/v1/pipeline
Saves (upserts) a pipeline configuration. Invalid field protection is rejected at save time (unknown or not-yet-supported `protect.method`, `preserve` outside `mask`, a string-producing method on a non-string field, any method but `hmac` on a `keyFields` column, or a source that is not delimited or JSON). Like every other validator error, the rejection returns HTTP 400 with `{"error": "..."}` naming the field.
# Delete a catalog (detach or cascade)
Source: https://docs.datris.ai/api-reference/pipelines/delete-a-catalog-detach-or-cascade
/openapi.yaml delete /api/v1/catalog/{name}
`mode=detach` (default) clears `catalog` on every member so it appears under Uncataloged, through a versioned write per member. `mode=cascade` deletes every member tap and pipeline, including each pipeline's destination data, and requires `confirm` equal to `name`. Both remove every placeholder of the catalog (matched by its `catalog` field, or by name suffix when that field is empty). A member already moved out of `name` by a concurrent change is listed under `failed`. Not transactional: failed members are listed under `failed` (207). Catalog operations (rename, detach, cascade) are serialized server-side, so concurrent requests run one after another and the later one sees the earlier one's result. Requires `pipeline:delete`; each member is also checked against the key's owner and catalog scopes, and the placeholder is left untouched if any member is refused. In cascade mode, if any tap delete fails, the catalog's pipelines are not deleted and are reported under `failed`. A cascade response also carries `warnings` (always present, possibly empty): one entry per deleted pipeline that needs a follow-up, such as a Unity Catalog table Datris could not drop; warnings never change the status code. Refused with 409 while an ordered enable of the catalog (`POST /api/v1/catalog/{name}/enable-ordered`) is running.
# Delete a pipeline configuration
Source: https://docs.datris.ai/api-reference/pipelines/delete-a-pipeline-configuration
/openapi.yaml delete /api/v1/pipeline
# Enable a catalog's taps in dependency order
Source: https://docs.datris.ai/api-reference/pipelines/enable-a-catalogs-taps-in-dependency-order
/openapi.yaml post /api/v1/catalog/{name}/enable-ordered
Starts one server-side job that walks the catalog's enable order (see `GET /api/v1/catalog/{name}/enable-order`) level by level, one tap at a time. Per tap: if any of its dependencies failed, was skipped or cancelled, or has not landed data, the tap is `skipped` with the reason; otherwise it is tested when it has no passing test (a failed test marks it `failed`), enabled (the test-before-cron gate applies; a refusal marks it `failed` with that text), given one real run (an error or zero records marks it `failed`) and the job waits until the run's pipeline loads have finished (`success`/`warning` = `landed`, `error` = `failed`, no finish within `datris.catalog.enableWaitMs`, default 30 minutes = `failed`). A tap that was already enabled with landed data is not re-run. No request body. Requires `tap:run`; the key must also hold `tap:update` and `tap:run` for every member tap (owner and catalog scope), otherwise 403 and nothing starts. Agent policy action `tap:run:enable-ordered` (inherits `tap:run`): an approval covers the whole job.
# Enable order of a catalog's taps
Source: https://docs.datris.ai/api-reference/pipelines/enable-order-of-a-catalogs-taps
/openapi.yaml get /api/v1/catalog/{name}/enable-order
Groups the catalog's taps into dependency levels: level 1 holds taps that read no other tap's tables, level 2 the taps that read level 1, and so on. A tap's dependencies are its `dependsOn` when set (even `[]`), else its stored `inferredDependsOn`. Per tap: `hasPassingTest` (the last test run succeeded on the tap's current script; a test stamped for an older script, e.g. after an overwrite restore, does not count), `lastRealRunLanded` (`lastRunStatus` is `success` with `lastRunRecordCount` > 0), `blockedBy` (dependencies that have not landed data) and `ready` (tested and not blocked). Taps on a dependency cycle are listed under `cycles`, not in a level. Read-only and not audited. Requires `tap:read`; taps the key may not read (owner or catalog scope) are omitted and named in `warnings`.
# Export a catalog as a zip backup
Source: https://docs.datris.ai/api-reference/pipelines/export-a-catalog-as-a-zip-backup
/openapi.yaml get /api/v1/catalog/{name}/export
Downloads one zip archive of the catalog: `manifest.json`, `taps/.json` (run status, ownership, timestamps and script references cleared), `taps/.py` (each Python tap's script), `taps/.state.json` (incremental state, when the tap has any), `pipelines/.json`, `pipelines//validation-schema/` (a referenced validation schema) and `pipelines//codegen/.py` (CodeGen scripts, reference copies only). Placeholders are never exported. Secret values are never in the archive: `manifest.requiredSecrets` lists each secret name, the items using it and its type. `manifest.warnings` names members the key may not read (omitted from the archive, never a 403 for the whole export), scripts or schemas that could not be read, taps whose state was included, and install-specific values copied verbatim (object-store endpoint, bucket override, warehouse, absolute `s3://` validation schema), and every inline credential removed from a pipeline (preprocessor and REST destination `bearerToken`/`apiKey`, `restEndpoint` row-function token parameters). The manifest carries `formatVersion: 1`. Requires `pipeline:read`; each member is also checked against the key's owner and catalog scopes as `tap:read` / `pipeline:read`.
# Force a new CodeGen script
Source: https://docs.datris.ai/api-reference/pipelines/force-a-new-codegen-script
/openapi.yaml post /api/v1/pipelines/{name}/codegen-scripts/{kind}/regenerate
Generates a new script for `kind` now, from the source schema, with the current CodeGen model, and replaces the stored one. If generation fails the error is returned (502) and the current script stays in place. For a JSON or XML pipeline the script is marked pending and the next run generates it from its data. The script stays in its current store unless `storage` asks for another; the old copy is left in place. A repository-backed script whose file was edited in the repository since the recorded commit is not overwritten (409) unless `overwrite=true`. Requires `pipeline:create`. Audited.
# Get a pipeline configuration
Source: https://docs.datris.ai/api-reference/pipelines/get-a-pipeline-configuration
/openapi.yaml get /api/v1/pipeline
# List all pipeline configurations
Source: https://docs.datris.ai/api-reference/pipelines/list-all-pipeline-configurations
/openapi.yaml get /api/v1/pipelines
# Progress of a catalog's ordered enable
Source: https://docs.datris.ai/api-reference/pipelines/progress-of-a-catalogs-ordered-enable
/openapi.yaml get /api/v1/catalog/{name}/enable-ordered
Exactly `{"active": false}` when the catalog has no job. A finished job stays readable for one hour with `active` false and `finishedAt` set. The same body is the `job` block of `GET .../enable-order`. Requires `tap:read`, checked against the key's catalog scope (the catalog) and owner scope (who started the job); 403 otherwise. Not audited.
# Propose destination column types from landed data
Source: https://docs.datris.ai/api-reference/pipelines/propose-destination-column-types-from-landed-data
/openapi.yaml get /api/v1/pipeline/dest-types
Agent-created pipelines store every destination column as text. This stateless call samples up to 1000 rows that already landed in the destination (postgres, snowflake, or databricks), infers real column types deterministically, and returns them with per-column evidence: a few distinct sample values, and — for columns kept as string by a dirty value — the offending value and the type it blocked. Nothing is stored or changed.
# Propose field protection from a preset (HIPAA Safe Harbor)
Source: https://docs.datris.ai/api-reference/pipelines/propose-field-protection-from-a-preset-hipaa-safe-harbor
/openapi.yaml post /api/v1/pipeline/protect/preset
Classifies source fields against the preset's fixed name table (the 18 HIPAA Safe Harbor identifier classes) and proposes each recognised field's default method, clamped by the same rules as `/api/v1/pipeline/protect/suggest` (keyFields columns only hmac or none; non-string source or destination types only drop or none). Deterministic: no model call. Stateless: nothing is saved and nothing is audited. Pass `preset` with either `pipeline` (its stored source schema and constraints are used) or `fields`. At most 150 fields per call; `_json` and `_xml` are skipped. A stored JSON, XML or unstructured pipeline is refused with 400. Needs the same capability as reading a pipeline. An aid to applying Safe Harbor, not a certification.
# Rename a catalog
Source: https://docs.datris.ai/api-reference/pipelines/rename-a-catalog
/openapi.yaml put /api/v1/catalog/{name}
Relabels every tap and pipeline whose `catalog` equals `name` to `newName`, each through a versioned write (one new definition version per member, with a change note naming the rename and the actor). Every hidden placeholder of the catalog (a `__catalog__*` tap whose `catalog` field is `name`, or whose name suffix is `name` when that field is empty) is removed, and `__catalog__` is created with `catalog=newName`. A member already moved out of `name` by a concurrent change is listed under `failed` ("no longer in catalog"), and placeholders are left untouched when no member moved. A rename never merges: if `newName` already exists (any tap or pipeline carries it, or it has a placeholder; matched case-sensitively) the request is refused with 409 and nothing changes. To combine catalogs, move their items. Not transactional: members that fail are listed under `failed` and the rest still move (207). API keys whose capabilities are scoped `catalog=` are listed under `affectedKeys`; their scopes are not rewritten. Catalog operations (rename, detach, cascade) are serialized server-side, so concurrent requests run one after another and the later one sees the earlier one's result. Requires `pipeline:update`; each member is also checked against the key's owner and catalog scopes. If any member is refused by scope, placeholders are left untouched (`placeholder: skipped`); for a catalog with only a placeholder, the placeholder is scope-checked (403 on denial). Refused with 409 while an ordered enable of the catalog (`POST /api/v1/catalog/{name}/enable-ordered`) is running.
# Restore a catalog from a zip backup
Source: https://docs.datris.ai/api-reference/pipelines/restore-a-catalog-from-a-zip-backup
/openapi.yaml post /api/v1/catalog/{name}/restore
Restores an archive produced by `GET /api/v1/catalog/{name}/export` as catalog `name` (the same or a new name, on the same or another install). A restore never merges: with `mode=new` (default) it is refused with 409 when catalog `name` already exists or any archived tap or pipeline name exists anywhere on the install (any catalog, either type), and nothing is written. With `mode=overwrite` (requires `confirm` equal to `name`) items already in `name` may be replaced; a name in another catalog is still a 409. When tap scripts are stored in a code repository, a script file already at branch head with different content is also a clash; identical content is reused without a new commit. Pipelines are saved exactly as `POST /api/v1/pipeline` saves them; tap scripts go to the install's default script storage; taps are saved with `enabled=false` and their run status cleared, so nothing runs until a user tests and enables them. Secrets are never written: `missingSecrets` lists the secrets the archived items reference that are absent on this install. Secrets are only checked where the key may read them: a key without secret read access gets a `: not checked, this key may not read secrets` warning instead, whether or not the secret exists. Each tap's incremental state is restored for taps that did not exist before, unless `restoreState=false`. An archive with no members creates the empty catalog. Not transactional: failed members are listed under `failed` (207). Requires `pipeline:create`. Every member is scope-checked before anything is written: `tap:create` / `pipeline:create` in the target catalog as the caller, and for an item that already exists (overwrite) also `tap:update` / `pipeline:update` against its current catalog and owner. A refused member is listed under `failed` and nothing of it, including its validation schema, is written. Inline credentials (preprocessor and REST destination `bearerToken`/`apiKey`, `restEndpoint` row-function token parameters) are removed at export; the restore response's `warnings` names each one to re-enter. Refused with 409 while an ordered enable of the catalog (`POST /api/v1/catalog/{name}/enable-ordered`) is running.
# Reveal values of an encrypt-protected field
Source: https://docs.datris.ai/api-reference/pipelines/reveal-values-of-an-encrypt-protected-field
/openapi.yaml post /api/v1/protect/reveal
Decrypts `enc:v:` ciphertexts that a pipeline's `encrypt` field protection wrote to a destination. Stateless: send the ciphertexts you read from the destination; Datris never queries a destination. A ciphertext is bound to its pipeline and field, so one copied to another pipeline or column fails. Needs the `protect:reveal` capability, which no key template or editor/viewer role carries; admins, the `full-access` template and legacy unscoped keys hold it through `*:*`. Not available as an MCP tool. Every call is recorded in the audit log as `protect / reveal` (field name and counts only, never a value), with outcome `warning` when any value failed; denied calls are recorded as `security` events.
# Rotate the field protection encryption key
Source: https://docs.datris.ai/api-reference/pipelines/rotate-the-field-protection-encryption-key
/openapi.yaml post /api/v1/protect/keys/rotate
Issues a new version of the environment's `encrypt` key and makes it current; runs after this encrypt with the new version (`enc:v:`). Older versions stay in the secret, so values encrypted with them still reveal. The `hmac` key is never rotated. Needs `protect:admin`. Recorded in the audit log as `key / rotate` with resource `field-protection`.
# Stored CodeGen scripts for a pipeline
Source: https://docs.datris.ai/api-reference/pipelines/stored-codegen-scripts-for-a-pipeline
/openapi.yaml get /api/v1/pipelines/{name}/codegen-scripts
The Python scripts the pipeline's AI data-quality rule and AI transformation run. A script is generated when the pipeline is saved (from the source schema, no row values) or, when none can be used, by the first run that needs it, and every later run executes the stored script without a model call. A changed instruction or source schema regenerates it; a changed CodeGen model does not (`modelIsCurrent` is false then; use the regenerate endpoint to adopt the new model). With a code repository configured, new scripts are committed to it and runs execute the recorded commit; `drift` reports an edit made in the repository since then. Requires `pipeline:read`.
# Suggest field protection from field names and types
Source: https://docs.datris.ai/api-reference/pipelines/suggest-field-protection-from-field-names-and-types
/openapi.yaml post /api/v1/pipeline/protect/suggest
Asks the configured CodeGen model which source fields should carry `protect` (hmac, mask with an optional preserve, redact, drop, or none), with a one-line reason per field. Only field names and types are sent to the model; no row value leaves the server. Stateless: nothing is saved. Every suggestion passes the pipeline validator, so confirm the ones you want and save them as `protect` on the source fields. Pass either `pipeline` (its stored source schema is used) or `fields`. At most 150 fields per call (more returns 400 before the model is called); with `pipeline`, keyFields columns are only suggested hmac or none and non-string destination fields only drop or none. Needs the same capability as reading a pipeline.
# Unity Catalog sync state for a pipeline
Source: https://docs.datris.ai/api-reference/pipelines/unity-catalog-sync-state-for-a-pipeline
/openapi.yaml get /api/v1/pipelines/{name}/unity-catalog
Whether Unity Catalog sync is on for the pipeline (`enabledBy`: `pipeline` for its own block, `default` when DATRIS_UNITY_CATALOG_DEFAULT turned on a Databricks pipeline with no block, null when off with no block), its three-level table coordinates (Databricks destinations, or the registered table of an object-store Iceberg destination with `unityCatalog.catalog` set), the last sync (`state` is `never`, `synced` or `error`), the lineage publish status (`lineage` is `off`, `never`, `published` or `error`; lineage failures appear in `lastError` as lines prefixed `uc-lineage:`), and the Iceberg register status (`register` is `off`, `never`, `registered`, `stale`, `error`, `rest` or `refused`; register warnings appear in `lastError` as lines prefixed `uc-register:`).
# Browse Unity Catalog with a Databricks Platform secret
Source: https://docs.datris.ai/api-reference/provenance/browse-unity-catalog-with-a-databricks-platform-secret
/openapi.yaml get /api/v1/unity-catalog/browse
Read-only walk of what a Databricks Platform secret can see, one level per call: no `catalog` lists catalogs; `catalog` lists schemas; `catalog` + `schema` lists tables (tables tagged `datris_pipeline` carry `datrisPipeline`); adding `table` returns columns (name, type, comment) and tags. Catalog and schema listings are cached for 5 minutes per secret. Requires `metadata:read` plus read access to the named secret. Every call is audit-logged as `unity-catalog` / `browse`. The SQL warehouse is the `warehouse` param, else the secret's optional `warehouse` field (aliases `httpPath`, `http_path`, `DATABRICKS_WAREHOUSE`), else the warehouse of the first Databricks pipeline using the secret.
# Column-level lineage for one pipeline definition
Source: https://docs.datris.ai/api-reference/provenance/column-level-lineage-for-one-pipeline-definition
/openapi.yaml get /api/v1/lineage/columns/{pipeline}
Which destination columns come from which source fields. The deterministic tier (schema comparison: `passthrough` / `drop` edges with confidence `exact`, stamped provenance columns as `system`) is always returned. For pipelines with an AI transformation, `infer=true` runs the opt-in extraction — the codegen model reads the instruction, the field lists and the stored CodeGen script, and returns only mappings it can evidence (`rename` / `derive` / `drop`, confidence `inferred`). Inferred results are cached per definition version and never recomputed unless the definition changes.
# Find datasets by meaning (discovery, not execution)
Source: https://docs.datris.ai/api-reference/provenance/find-datasets-by-meaning-discovery-not-execution
/openapi.yaml get /api/v1/catalog/find
Ranks the pipelines the calling key may read against a natural-language query (name, description, tags, catalog, destination field names, source host). Each hit carries location, freshness, provenance handles, lineage and a pre-filled `howToQuery` hint naming an existing query/search tool — the caller still makes that call itself, under its own capabilities.
# One node's lineage neighborhood
Source: https://docs.datris.ai/api-reference/provenance/one-nodes-lineage-neighborhood
/openapi.yaml get /api/v1/lineage/{type}/{name}
The node, everything transitively upstream and/or downstream, the edges between them, freshness for the pipeline the node is (or feeds), and optionally the most recent recorded runs touching the node — what each read and wrote, per destination.
# Resolve a stamped run id to its origin
Source: https://docs.datris.ai/api-reference/provenance/resolve-a-stamped-run-id-to-its-origin
/openapi.yaml get /api/v1/provenance
Walks a `_datris_run_id` back through the chain run → job status → tap run → script commit → pipeline config version → declared source and returns one document. Pass the row's `_datris_tap_run` and `_datris_config_version` values when available for exact resolution.
# The whole lineage graph
Source: https://docs.datris.ai/api-reference/provenance/the-whole-lineage-graph
/openapi.yaml get /api/v1/lineage
The graph Source → Tap → Pipeline → Dataset → Catalog, derived from configuration plus recorded runs (cached ~1 minute). Node ids are `type:name`. Datasets a pipeline landed into under an earlier configuration appear with `historical: true` (and a dashed edge).
# Query API
Source: https://docs.datris.ai/api-reference/query-api
Query data endpoints
The Query API provides REST endpoints for querying every structured destination — PostgreSQL, MongoDB, object store, Snowflake, and Databricks — plus natural language queries and AI-powered answers. These endpoints mirror the MCP tools, making the same retrieval capabilities available via HTTP.
In the web UI, the Search tab's Traditional mode can also query Databricks and Snowflake pipelines: pick the pipeline and run read-only SQL against its destination table through the pipeline's own warehouse and credentials.
## Query PostgreSQL
Execute a read-only SQL SELECT query against PostgreSQL.
```
POST /api/v1/query/postgres
```
### Request
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `sql` | string | Yes | | SQL SELECT query to execute |
| `database` | string | No | `datris` | PostgreSQL database name to connect to |
| `limit` | integer | No | 100 | Maximum rows to return. Pass `-1` for unlimited (tap scripts use this to read every row). Omit for the preview default of 100; any positive value is honored with no cap. |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/postgres \
-H "Content-Type: application/json" \
-d '{
"sql": "SELECT symbol, date, close FROM test.stock_price WHERE volume > 1000000",
"database": "datris",
"limit": 10
}'
```
### Response
```json theme={null}
{
"results": [
{
"symbol": "AAPL",
"date": "2024-01-15",
"close": 185.92
}
],
"count": 1
}
```
### Safety
The endpoint enforces multiple layers of protection:
* **SELECT-only** — only queries starting with `SELECT` are allowed
* **Read-only connection** — the JDBC connection is set to read-only mode
* **Statement blocklist** — queries containing `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `CREATE`, `TRUNCATE`, `GRANT`, `REVOKE`, `COPY`, `CALL`, `EXECUTE`, or `EXEC` are rejected
* **No stacked queries** — semicolons are not allowed
* **No SQL comments** — `--` and `/* */` are rejected to prevent obfuscation
* **LIMIT enforcement** — if no `LIMIT` is present in the SQL, one is appended automatically using the request's `limit` value (or the default 100). Pass `limit: -1` to bypass the automatic append entirely — the caller's SQL runs as-is.
***
## Query MongoDB
Query a MongoDB collection with optional filter and projection.
```
POST /api/v1/query/mongodb
```
### Request
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `collection` | string | Yes | | MongoDB collection name |
| `database` | string | No | configured MongoDB database | MongoDB database name to query |
| `filter` | object | No | `{}` | MongoDB query filter |
| `projection` | object | No | all fields | Fields to include/exclude |
| `limit` | integer | No | 20 | Maximum documents to return. Pass `-1` for unlimited (tap scripts use this to read every document). Omit for the preview default of 20; any positive value is honored with no cap. |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/mongodb \
-H "Content-Type: application/json" \
-d '{
"collection": "orders",
"filter": {"status": "active"},
"projection": {"name": 1, "_id": 0},
"limit": 10
}'
```
### Response
```json theme={null}
{
"results": [
{
"name": "Order-001"
}
],
"count": 1
}
```
### Safety
* **Blocked operators** — filters containing `$where`, `$function`, or `$accumulator` are rejected, as these allow arbitrary JavaScript execution
* **Standard MongoDB query operators** (`$eq`, `$gt`, `$in`, `$regex`, etc.) are safe and fully supported
***
## Query Object Store
Read rows from a pipeline's object store destination (MinIO or S3). The pipeline name selects the bucket, prefix, and credentials — the caller never supplies connection details.
```
POST /api/v1/query/objectstore
```
### Request
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `pipeline` | string | Yes | | Pipeline name. Must be a pipeline with an object store destination. |
| `limit` | integer | No | 100 | Maximum rows to return. Values of 0 or less fall back to 100; capped at 10,000. |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/objectstore \
-H "Content-Type: application/json" \
-d '{
"pipeline": "sales_data",
"limit": 10
}'
```
### Response
```json theme={null}
{
"pipeline": "sales_data",
"path": "s3a://datris-bucket/sales_data",
"format": "parquet",
"columns": ["symbol", "date", "close"],
"results": [
{
"symbol": "AAPL",
"date": "2024-01-15",
"close": 185.92
}
],
"count": 1
}
```
If the pipeline exists but no runs have written data yet, the endpoint returns an empty result (`"count": 0`) rather than an error.
### Error Cases
Returns `400 Bad Request` with `{"error": "..."}` when:
* The pipeline is not found
* The pipeline does not have an object store destination
* The pipeline's object store configuration is missing a prefix key
* The read times out (default 90 seconds, tunable via the `DATRIS_OBJECTSTORE_QUERY_TIMEOUT_SEC` environment variable)
***
## Query Snowflake
Execute a read-only query against the Snowflake account a pipeline loads into. The pipeline name selects the credentials, warehouse, database, and schema — credentials never leave the server.
```
POST /api/v1/query/snowflake
```
### Request
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `pipeline` | string | Yes | | Pipeline name. Must be a pipeline with a Snowflake destination. |
| `sql` | string | No | `SELECT * FROM {destination table}` | SQL to execute. When omitted, previews the pipeline's destination table. |
| `limit` | integer | No | 100 | Maximum rows to return. Pass `-1` for unlimited (also disables the query timeout). |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/snowflake \
-H "Content-Type: application/json" \
-d '{
"pipeline": "sales_data",
"sql": "SELECT symbol, close FROM SALES_DB.PUBLIC.STOCK_PRICE WHERE volume > 1000000",
"limit": 10
}'
```
### Response
```json theme={null}
{
"pipeline": "sales_data",
"sql": "SELECT symbol, close FROM SALES_DB.PUBLIC.STOCK_PRICE WHERE volume > 1000000 LIMIT 10",
"results": [
{
"SYMBOL": "AAPL",
"CLOSE": 185.92
}
],
"count": 1
}
```
The `sql` field echoes the query that actually ran, including any automatically appended `LIMIT`.
### Safety
* **Read-only allowlist** — queries must start with `SELECT`, `WITH`, `SHOW`, `DESCRIBE`, or `DESC`. `SHOW`/`DESCRIBE` are allowed so agents can discover databases, schemas, tables, and columns.
* **Statement blocklist** — queries containing `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `CREATE`, `TRUNCATE`, `GRANT`, `REVOKE`, `CALL`, `EXECUTE`, `EXEC`, `PUT`, `REMOVE`, `COPY`, `MERGE`, or `USE` are rejected (`USE` is blocked so a query cannot re-point the session at another database or role)
* **No stacked queries** — semicolons are not allowed
* **No SQL comments** — `--` and `/* */` are rejected to prevent obfuscation
* **LIMIT enforcement** — if a `SELECT`/`WITH` query has no `LIMIT`, one is appended using the request's `limit` value. `SHOW`/`DESCRIBE` output is row-capped at the same value.
* **Query timeout** — 30 seconds, bounding both hung requests and warehouse spend. Not applied when `limit: -1`.
### Error Cases
Returns `400 Bad Request` with `{"error": "..."}` when:
* The pipeline is not found
* The pipeline does not have a Snowflake destination
* The SQL fails validation (allowlist, blocklist, semicolons, comments)
***
## Query Databricks
Execute a read-only query against the Databricks workspace a pipeline loads into. The pipeline name selects the credentials, SQL warehouse, catalog, and schema — credentials never leave the server.
```
POST /api/v1/query/databricks
```
### Request
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `pipeline` | string | Yes | | Pipeline name. Must be a pipeline with a Databricks destination. |
| `sql` | string | No | `SELECT * FROM {destination table}` | SQL to execute. When omitted, previews the pipeline's destination table. |
| `limit` | integer | No | 100 | Maximum rows to return. Pass `-1` for unlimited (also disables the query timeout). |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/databricks \
-H "Content-Type: application/json" \
-d '{
"pipeline": "sales_data",
"sql": "SELECT symbol, close FROM main.default.stock_price WHERE volume > 1000000",
"limit": 10
}'
```
### Response
```json theme={null}
{
"pipeline": "sales_data",
"sql": "SELECT symbol, close FROM main.default.stock_price WHERE volume > 1000000 LIMIT 10",
"results": [
{
"symbol": "AAPL",
"close": 185.92
}
],
"count": 1
}
```
The `sql` field echoes the query that actually ran, including any automatically appended `LIMIT`.
### Safety
Same protections as [Query Snowflake](#query-snowflake), with the blocklist extended for Databricks-specific data-movement and maintenance verbs: `OPTIMIZE`, `VACUUM`, `RESTORE`, `CLONE`, `REFRESH`, `MSCK`, and `DECLARE` are also rejected.
If the SQL warehouse is stopped, opening the connection blocks while it auto-starts — this happens before the 30-second query timeout applies, so the first query after idle time can take longer.
### Error Cases
Returns `400 Bad Request` with `{"error": "..."}` when:
* The pipeline is not found
* The pipeline does not have a Databricks destination
* The SQL fails validation (allowlist, blocklist, semicolons, comments)
***
## Kill Job
Kill a running pipeline job by its pipeline token. The job thread is interrupted and the job is marked as cancelled.
```
POST /api/v1/job/kill
```
### Request
| Field | Type | Required | Description |
| - | - | - | - |
| `pipelineToken` | string | Yes | Pipeline token of the running job to kill |
### Example
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/job/kill \
-H "Content-Type: application/json" \
-d '{
"pipelineToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}'
```
### Response
```json theme={null}
{
"status": "cancelled",
"pipelineToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```
### Error Cases
* Job not found for the given pipeline token
* Job is not in `PROCESSING` state (already completed or cancelled)
* Job thread is no longer alive
# Natural-language query (AI)
Source: https://docs.datris.ai/api-reference/query/natural-language-query-ai
/openapi.yaml post /api/v1/query/natural
Ask a question in plain English against a PostgreSQL table. The server reads the table's columns, uses AI to generate a SQL SELECT, executes it, and returns both the SQL and the results.
# Query Databricks
Source: https://docs.datris.ai/api-reference/query/query-databricks
/openapi.yaml post /api/v1/query/databricks
Execute a read-only SQL query against the Databricks destination of a pipeline (a Unity Catalog managed Delta table). Connection credentials come from the pipeline's configured `credentialsSecret`.
# Query MongoDB
Source: https://docs.datris.ai/api-reference/query/query-mongodb
/openapi.yaml post /api/v1/query/mongodb
Query a MongoDB collection with optional filter and projection. Blocks $where and $function operators.
# Query object store
Source: https://docs.datris.ai/api-reference/query/query-object-store
/openapi.yaml post /api/v1/query/objectstore
Read rows back from the files an object-store-destination pipeline has written (Parquet, ORC, CSV, or JSON), without downloading them.
# Query PostgreSQL
Source: https://docs.datris.ai/api-reference/query/query-postgresql
/openapi.yaml post /api/v1/query/postgres
Execute a read-only SQL SELECT query against PostgreSQL. Enforces SELECT-only, blocks dangerous keywords, and auto-appends LIMIT.
# Query Snowflake
Source: https://docs.datris.ai/api-reference/query/query-snowflake
/openapi.yaml post /api/v1/query/snowflake
Execute a read-only SQL query against the Snowflake destination of a pipeline. Connection credentials come from the pipeline's configured `credentialsSecret`.
# Abandon an open incident (admin / editor)
Source: https://docs.datris.ai/api-reference/recovery-agent/abandon-an-open-incident-admin-editor
/openapi.yaml post /api/v1/incidents/{id}/abandon
A person closing an incident by hand. Never permitted from an agent-initiated request.
# List recovery-agent incidents
Source: https://docs.datris.ai/api-reference/recovery-agent/list-recovery-agent-incidents
/openapi.yaml get /api/v1/incidents
Incidents the platform opened for failed, stale or anomalous data flows, newest first. Read-only — only the platform opens incidents. `enabled` is false while RECOVERY_AGENT_ENABLED is off.
# Read one incident
Source: https://docs.datris.ai/api-reference/recovery-agent/read-one-incident
/openapi.yaml get /api/v1/incidents/{id}
# Server-side Activity signals
Source: https://docs.datris.ai/api-reference/recovery-agent/server-side-activity-signals
/openapi.yaml get /api/v1/activity/signals
One definition of operational signals — failures (with the recovered flag), stale scheduled taps, and pipeline volume anomalies — shared by the dashboard, the Ops chat context and the recovery agent.
# AI Schema Generation API
Source: https://docs.datris.ai/api-reference/schema-generation-api
AI schema generation endpoints
The pipeline can automatically generate a ready-to-use pipeline configuration from an uploaded file using an AI model. The generated config can be pasted directly into `POST /api/v1/pipeline` to register the pipeline without writing any JSON by hand.
## How It Works
```
Uploaded file
|
v
File type detection (CSV / JSON / XML)
|
+-- JSON / XML --> Fixed schema (_json or _xml field)
| No AI call needed
|
+-- CSV / other --> First 100 lines sent to AI model
AI infers column names and data types
Returns JSON array of field definitions
|
v
Config builder assembles full PipelineConfig JSON
- source.schemaProperties.fields (AI-inferred or fixed)
- source.fileAttributes (csvAttributes / jsonAttributes / xmlAttributes)
- destination (Postgres for CSV, MongoDB for JSON/XML)
|
v
Response: complete PipelineConfig JSON ready to register
```
The response is **not** registered automatically — it is returned to the caller so you can review it, fill in the placeholder values, and then POST it to `/api/v1/pipeline`.
***
## Endpoint
```
POST /api/v1/pipeline/generate
Content-Type: multipart/form-data
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `file` | form-data (file) | Yes | The file to analyze |
| `pipeline` | query | No | Pipeline name. If omitted, derived from the filename (lowercased, non-alphanumeric characters replaced with `_`) |
| `delimiter` | query | No | Column delimiter for delimited files. Defaults to `,` |
| `header` | query | No | Whether the file has a header row (default `false`; pass `true` for a file with a header row) |
| `allStrings` | query | No | If `true`, all fields are typed as `string` (default: `false`) |
| `x-api-key` | header | No | API key (required if `useApiKeys: true`) |
***
## Schema Rules by File Type
| File type | Schema | Default destination |
| - | - | - |
| CSV / delimited | AI infers field names and types from file content | PostgreSQL (`usePostgres: true`) |
| JSON (`.json`) | Single field: `_json` (type `string`) | MongoDB (`useMongoDB: true`) |
| XML (`.xml`) | Single field: `_xml` (type `string`) | MongoDB (`useMongoDB: true`) |
JSON and XML files use a fixed schema because the pipeline stores them as raw documents — no column inference is needed.
**Valid AI-inferred types:** `boolean`, `int`, `bigint`, `float`, `double`, `string`, `date`, `timestamp`
### When the model declines
An AI provider's safety classifier can decline to look at a file, most often one that looks like personal data. For a CSV or other delimited file the endpoint then still returns `200` with every field typed `string`. With `header=true` and a first line that reads as column names, the fields keep the header's names exactly as `allStrings=true` writes them, except that an empty header cell (empty, plain spaces or a byte-order mark only; an empty or newline-only file included), a cell that only renders blank (such as a Hangul filler), or a name repeated ignoring case and surrounding spaces, is named `column_N` for its position, so no field name is blank or duplicated. A cell holding only other characters that are not letters (a non-breaking or zero-width space, for example) makes line 1 read as data: every column is then numbered and `csvAttributes.header` is `false`. When `header` is false (the default when the parameter is omitted) or line 1 looks like data, the columns are named `column_1`, `column_2`, … and the config sets `csvAttributes.header` to `false`. The response carries a top-level `"aiDeclined": true`, and the pipeline wizard shows a one-line note; set the types by hand. `aiDeclined` is a response-only flag: it is never saved with a pipeline. The provider's own text is not included.
| Provider family | Recognised as a decline |
| - | - |
| Anthropic (direct or AWS Bedrock) | `stop_reason: "refusal"` |
| OpenAI, Azure OpenAI, Grok and Ollama (chat completions, streamed or not) | `finish_reason: "content_filter"`, or a non-empty `message.refusal` (`delta.refusal` when streamed) |
| OpenAI Responses API models | an output item or message content part of type `refusal`, or `incomplete_details.reason: "content_filter"` |
Only response-side declines are recognised. Azure OpenAI's prompt-side content filter answers with HTTP 400 (`code: content_filter`) and is still returned as an error. Nothing else counts as a decline: a missing or wrong provider key, a timeout, a rate limit or any other provider error is still returned as an error, so a misconfiguration is not hidden.
Other AI features (search answers, CodeGen, tap generation, fix suggestions and the rest) have no local fallback: a decline there, including a `content_filter` finish that used to return the cut-off text, is reported as an error.
JSON Schema and XSD generation (`POST /api/v1/config/generate-schema`) have no local fallback: a decline there is still returned as an error, with the message "The model declined this request (…)".
***
## Example: CSV File
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline/generate \
-H "x-api-key: your-api-key" \
-F "file=@./stock_price.csv" \
-F "pipeline=stock_price" \
-F "header=true"
```
Response:
```json theme={null}
{
"name": "stock_price",
"source": {
"schemaProperties": {
"fields": [
{ "name": "symbol", "type": "string" },
{ "name": "date", "type": "string" },
{ "name": "open", "type": "double" },
{ "name": "high", "type": "double" },
{ "name": "low", "type": "double" },
{ "name": "close", "type": "double" },
{ "name": "volume", "type": "int" },
{ "name": "adj_close", "type": "double" }
]
},
"fileAttributes": {
"csvAttributes": { "delimiter": ",", "header": true, "encoding": "UTF-8" }
}
},
"destination": {
"database": {
"dbName": "DATABASE_NAME",
"schema": "SCHEMA_NAME",
"table": "TABLE_NAME",
"usePostgres": true
}
}
}
```
Replace `DATABASE_NAME`, `SCHEMA_NAME`, and `TABLE_NAME` with real values, then register the pipeline:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline \
-H "Content-Type: application/json" \
-d ''
```
***
## Example: JSON File
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/pipeline/generate \
-H "x-api-key: your-api-key" \
-F "file=@./events.json"
```
Response:
```json theme={null}
{
"name": "events",
"source": {
"schemaProperties": {
"fields": [
{ "name": "_json", "type": "string" }
]
},
"fileAttributes": {
"jsonAttributes": { "everyRowContainsObject": false, "encoding": "UTF-8" }
}
},
"destination": {
"database": {
"dbName": "DATABASE_NAME",
"table": "TABLE_NAME",
"useMongoDB": true
}
}
}
```
***
## Configuration
AI schema generation is disabled by default. To enable it:
Schema generation uses the **codegen** AI slot. As of v1.5.6, AI configuration is split into three independent self-describing Vault secrets — see [AI Configuration](/ai-configuration) for the full picture.
**`application.yaml`** (or `docker/config/application.yaml` for Docker deployments):
```yaml theme={null}
ai:
enabled: "true"
aiPrimary:
secretName: "oss/ai-primary"
codegen:
secretName: "oss/codegen"
embedding:
secretName: "oss/embedding"
```
**Vault secret** (each secret is self-describing — provider, endpoint, model, apiKey, and optionally version, all inline):
```bash theme={null}
vault kv put secret/oss/codegen \
provider="anthropic" \
endpoint="https://api.anthropic.com/v1/messages" \
model="claude-opus-4-8" \
apiKey="sk-ant-..." \
version="2023-06-01"
```
For OpenAI:
```bash theme={null}
vault kv put secret/oss/codegen \
provider="openai" \
endpoint="https://api.openai.com/v1/chat/completions" \
model="gpt-5.4" \
apiKey="sk-..."
```
The Vault secret keys:
| Key | Description |
| - | - |
| `provider` | `anthropic`, `openai`, `azure`, `bedrock`, `grok`, or `ollama` |
| `endpoint` | The AI provider API URL |
| `model` | The model name to use |
| `apiKey` | The API key for authentication (omit for local Ollama) |
| `version` | Optional API version header (e.g. Anthropic's `anthropic-version`) |
The pipeline reads the secret at startup. If `ai.enabled: true` and the codegen secret is missing or malformed, startup will fail with a descriptive error. `docker/vault-init.sh` seeds this automatically from `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in `.env`.
***
## Supported Providers
| Provider | `provider` value | Auth header |
| - | - | - |
| Anthropic Claude | `anthropic` | `x-api-key` + `anthropic-version: 2023-06-01` |
| OpenAI | `openai` | `Authorization: Bearer` |
| Azure OpenAI | `azure` | API key or Entra ID (keyless) |
| Amazon Bedrock | `bedrock` | AWS credentials |
| Grok (xAI) | `grok` | `Authorization: Bearer` |
| Ollama (local) | `ollama` | none |
Any other value for `provider` will cause startup to fail with an unsupported provider error.
# Search API
Source: https://docs.datris.ai/api-reference/search-api
Search endpoints
The Search API provides a single REST endpoint for semantic search across vector databases. The endpoint converts a natural language query into an embedding vector, searches the vector store named in the URL, and returns the most similar results with relevance scores.
Embedding and vector database connection details are stored in HashiCorp Vault and resolved **server-side**. Clients never send secret names — the server always uses its own configured secret names (in multi-tenant mode this enforces tenant isolation), so any secret-name fields in a request body are ignored.
## Search a Vector Store
```
POST /api/v1/search/{store}
```
One endpoint serves all vector stores. The `{store}` path parameter selects the store:
| Store | Container field | Container default | Store-specific fields |
| - | - | - | - |
| `qdrant` | `collection` | `documents` | |
| `weaviate` | `className` | `Documents` | |
| `milvus` | `collection` | `documents` | |
| `chroma` | `collection` | `documents` | |
| `pgvector` | `table` | `documents` | `schema` (default `public`) |
### Common Parameters
All stores share these request-body parameters:
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `query` | string | Yes | | Natural language search query |
| `topK` | integer | No | 5 | Number of results to return |
Plus the store's container field from the table above (e.g. `collection` for Qdrant, `className` for Weaviate, `table`/`schema` for pgvector).
### Common Response Format
```json theme={null}
{
"results": [
{
"text": "document chunk content",
"chunk_index": 0,
"source_pipeline": "pipeline_name",
"filename": "document.pdf",
"_score": 0.89
}
],
"count": 1
}
```
The `_score` field indicates relevance (higher is more similar, normalized to 0–1 where applicable).
### Error Responses
An unknown `{store}` value returns `400 Bad Request` with a message naming the valid stores:
```
Unknown vector store: 'faiss'. Valid stores: chroma, milvus, pgvector, qdrant, weaviate
```
Other user-actionable errors also return `400 Bad Request` with a plain-text message — most notably a vector dimension mismatch, which usually means the embedding provider was changed between ingest and query. Fix it by switching the embedding provider in Configuration to match the collection's dimension, or by re-ingesting under the current provider.
Only stores that are actually running and reachable can be searched successfully. Use [`GET /api/v1/vector-stores/available`](/api-reference/health-api#list-available-vector-stores) to discover which stores are live before searching — the UI's Search tab uses this to gate its store dropdown.
Unexpected server errors return `500 Internal Server Error`.
***
### Qdrant
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/search/qdrant \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly revenue projections",
"collection": "documents",
"topK": 5
}'
```
### Weaviate
The container field is `className` (PascalCase, default `Documents`).
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/search/weaviate \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly revenue projections",
"className": "Documents",
"topK": 5
}'
```
### Milvus
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/search/milvus \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly revenue projections",
"collection": "documents",
"topK": 5
}'
```
### Chroma
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/search/chroma \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly revenue projections",
"collection": "documents",
"topK": 5
}'
```
### pgvector
The container field is `table`, with an optional `schema` (default `public`).
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/search/pgvector \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly revenue projections",
"table": "documents",
"schema": "public",
"topK": 5
}'
```
***
## Natural Language Query
Ask a question in plain English — the AI generates and executes a SQL query against PostgreSQL.
```
POST /api/v1/query/natural
```
| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `question` | string | Yes | | Natural language question |
| `table` | string | Yes | | Target table name |
| `schema` | string | No | `public` | PostgreSQL schema |
| `database` | string | No | `datris` | PostgreSQL database |
| `limit` | integer | No | 100 | Maximum rows to return |
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/query/natural \
-H "Content-Type: application/json" \
-d '{
"question": "What are the top 5 stocks by volume?",
"table": "stock_prices"
}'
```
Returns the AI-generated SQL query and its results.
***
## AI Answer (RAG)
Answer a question using AI based on provided context — typically used after a vector search to generate a natural language answer from retrieved chunks.
```
POST /api/v1/ai/answer
```
| Field | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | The question to answer |
| `context` | string | Yes | Context text (e.g., concatenated search results) |
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/ai/answer \
-H "Content-Type: application/json" \
-d '{
"query": "What was Apple'\''s revenue last quarter?",
"context": "Apple reported revenue of $94.9 billion for Q1 2026..."
}'
```
Returns the AI-generated answer based on the provided context.
***
## Vault Secret Structure
These secrets are configured on the server (seeded by `vault-init.sh` and managed via the Configuration tab) — they are never passed in search requests.
### Embedding Secret
Self-describing — the resolver reads `provider`, `endpoint`, `model`, `apiKey`, and (optionally) `version` from inside the secret. The server uses the secret named by `ai.embedding.secretName` (`oss/embedding`). See [AI Configuration](/ai-configuration).
```json theme={null}
{
"provider": "openai",
"endpoint": "https://api.openai.com/v1/embeddings",
"model": "text-embedding-3-small",
"apiKey": "sk-..."
}
```
For Anthropic-only deployments, the bundled TEI sidecar serves `bge-m3` and `vault-init.sh` seeds the embedding secret to point at it (`provider: "tei"`, `endpoint: "http://tei:80/v1/embeddings"`, `model: "BAAI/bge-m3"`).
### Vector Database Secrets
Each store has a server-configured secret resolved by store key:
**Qdrant:**
```json theme={null}
{ "host": "localhost", "port": "6334", "apiKey": "" }
```
**Weaviate:**
```json theme={null}
{ "host": "localhost", "port": "8079", "scheme": "http", "apiKey": "" }
```
**Milvus:**
```json theme={null}
{ "host": "localhost", "port": "19530", "apiKey": "" }
```
**Chroma:**
```json theme={null}
{ "host": "localhost", "port": "8000" }
```
**pgvector:**
```json theme={null}
{ "jdbcUrl": "jdbc:postgresql://localhost:5432/datris", "username": "postgres", "password": "postgres" }
```
# Semantic search (all vector stores)
Source: https://docs.datris.ai/api-reference/search/semantic-search-all-vector-stores
/openapi.yaml post /api/v1/search/{store}
Search a vector store using natural language. One endpoint serves every supported store — the `{store}` path segment selects it. Generates an embedding from the query and returns the most similar document chunks. Connection and embedding secrets are always resolved server-side from the server's configuration; any client-supplied secret names are ignored. Request fields vary slightly per store — qdrant, milvus, and chroma name their container with `collection`, weaviate with `className`, pgvector with `table` (plus an optional `schema`).
# Secrets API
Source: https://docs.datris.ai/api-reference/secrets-api
Manage secrets in HashiCorp Vault
The Secrets API lets you list, read, update, and delete secrets stored in HashiCorp Vault. Secrets are scoped to the current environment (e.g., `oss`).
## List Secrets
```
GET /api/v1/secrets
```
Returns a JSON array of secret names for the current environment.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `type` | query | No | Filter by the secret's `_type` tag (e.g. `tap`). `platform` returns every secret *not* tagged `_type=tap`. |
### Example
```bash theme={null}
curl -s http://localhost:8080/api/v1/secrets | jq .
```
### Response
```json theme={null}
["activemq", "anthropic", "chroma", "embedding", "kafka-producer", "minio", "mongodb", "ollama", "openai", "pgvector", "postgres", "qdrant", "weaviate"]
```
***
## Get Secret
```
GET /api/v1/secrets/{name}
```
| Parameter | Type | Description |
| - | - | - |
| `name` | path | Secret name |
Sensitive fields are always masked as `••••••••`: any field whose name (case-insensitive, ignoring `_`/`-`) contains `password`, `passwd`, `pwd`, `secret`, `token`, `key`, `credential`, `signature`, `bearer`, or `private` — e.g. `apiKey`, `secretKey`, `accessKey`. Non-sensitive fields show their actual values.
### Example
```bash theme={null}
curl -s http://localhost:8080/api/v1/secrets/anthropic | jq .
```
### Response
```json theme={null}
{
"name": "anthropic",
"fields": {
"endpoint": "https://api.anthropic.com/v1/messages",
"model": "claude-opus-4-8",
"apiKey": "••••••••"
}
}
```
### Error
Returns `404` if the secret does not exist.
***
## Update Secret
Create or update a secret.
```
PUT /api/v1/secrets/{name}
Content-Type: application/json
```
| Parameter | Type | Description |
| - | - | - |
| `name` | path | Secret name |
| body | JSON | Key-value pairs to store |
Updating an existing secret replaces its fields with the body you send, with one safeguard: sending the `••••••••` mask (or any empty string) for a sensitive field keeps its current value, so you can add or correct one field without re-entering credentials. To remove a field, omit its key from the body. The shared `ai-keys` store is the exception: fields you omit are kept, and an empty string clears a field.
### Example
```bash theme={null}
curl -X PUT http://localhost:8080/api/v1/secrets/anthropic \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://api.anthropic.com/v1/messages",
"model": "claude-opus-4-8",
"apiKey": "sk-ant-..."
}'
```
### Response
```json theme={null}
{"status": "ok"}
```
***
## Delete Secret
```
DELETE /api/v1/secrets/{name}
```
| Parameter | Type | Description |
| - | - | - |
| `name` | path | Secret name to delete |
### Example
```bash theme={null}
curl -X DELETE http://localhost:8080/api/v1/secrets/my-secret
```
### Response
```json theme={null}
{"status": "ok"}
```
# Create or update a secret
Source: https://docs.datris.ai/api-reference/secrets/create-or-update-a-secret
/openapi.yaml put /api/v1/secrets/{name}
Creates or updates a secret with the provided key-value fields.
# Delete a secret
Source: https://docs.datris.ai/api-reference/secrets/delete-a-secret
/openapi.yaml delete /api/v1/secrets/{name}
# Get secret details
Source: https://docs.datris.ai/api-reference/secrets/get-secret-details
/openapi.yaml get /api/v1/secrets/{name}
Returns the key-value fields of a secret. Sensitive fields (names containing password, secret, token, key, credential, signature, bearer, or private) are always masked.
# List all secrets
Source: https://docs.datris.ai/api-reference/secrets/list-all-secrets
/openapi.yaml get /api/v1/secrets
Lists all secret names under the current environment path in Vault.
# Pipeline Status API
Source: https://docs.datris.ai/api-reference/status-api
Pipeline status endpoints
Query job processing status by publisher token, pipeline token, or pipeline name, or clear all ingestion history.
## Get Status by Publisher Token
```
GET /api/v1/pipeline/status?publishertoken={token}
```
Use this to watch every ingestion job a single caller submitted. Tap runs set a `publisherToken` on every job they spawn, so one query covers:
* Structured taps → the single job that ingested the fetched records.
* Document taps → every per-document job (one per file in a run).
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `publishertoken` | query | Yes | Publisher token returned from `/tap/run` (`publisherToken` field) or `upload_data` |
| `withrollup` | query | No | When `true`, wraps the response in a rollup object that classifies each job — see [Rollup Response](#rollup-response) below. Recommended for agents that need a single boolean to poll on. |
**Example:**
```bash theme={null}
curl "http://localhost:8080/api/v1/pipeline/status?publishertoken=pub-abc12345-..."
```
**Response:** `200 OK` — status entries from every job whose `publisherToken` matches, sorted oldest-first. Each job has its own `pipelineToken`; group rows by `pipelineToken` to see one job at a time.
If both `publishertoken` and `pipelinetoken` are supplied, `publishertoken` wins.
***
## Get Status by Pipeline Token
```
GET /api/v1/pipeline/status?pipelinetoken={token}
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `pipelinetoken` | query | Yes\* | Pipeline token from upload response |
| `withrollup` | query | No | When `true`, wraps the response in a rollup object — see [Rollup Response](#rollup-response). |
**Example:**
```bash theme={null}
curl "http://localhost:8080/api/v1/pipeline/status?pipelinetoken=pt-abc12345-..."
```
**Response:** `200 OK` - an array of status entries, one per processing stage:
```json theme={null}
[
{
"id": 1,
"dateTime": "2026-03-15T10:00:00Z",
"pipeline": "sales_data",
"processName": "StreamNotifier",
"publisherToken": null,
"pipelineToken": "pt-abc12345-...",
"filename": "sales_data",
"state": "begin",
"code": "begin",
"description": "Process started",
"epoch": 1710500400000
},
{
"id": 2,
"dateTime": "2026-03-15T10:00:03Z",
"pipeline": "sales_data",
"processName": "PostgresLoader",
"publisherToken": null,
"pipelineToken": "pt-abc12345-...",
"filename": "sales_data",
"state": "end",
"code": "end",
"description": "Process completed",
"epoch": 1710500403000
}
]
```
***
## Get Status by Pipeline Name
```
GET /api/v1/pipeline/status?pipelinename={name}&page={page}
```
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `pipelinename` | query | Yes\* | Pipeline name |
| `page` | query | No | Page number (default: 1) |
**Example:**
```bash theme={null}
curl "http://localhost:8080/api/v1/pipeline/status?pipelinename=sales_data&page=1"
```
**Response:** `200 OK` - an array of job summaries:
```json theme={null}
[
{
"createdAtTimestamp": "2026-03-15T10:00:00Z",
"createdAt": 1710500400000,
"updatedAt": 1710500403000,
"pipeline": "sales_data",
"pipelineToken": "pt-abc12345-...",
"process": "PostgresLoader",
"startTime": "2026-03-15T10:00:00Z",
"endTime": "2026-03-15T10:00:03Z",
"totalTime": "3s",
"status": "end"
}
]
```
***
## Rollup Response
Add `&withrollup=true` to a `publishertoken` or `pipelinetoken` query and the response is wrapped:
```json theme={null}
{
"rollup": {
"allDone": false,
"status": "processing",
"jobs": [
{
"pipelineToken": "pt-abc12345-...",
"pipeline": "sales_data",
"filename": "sales_data.csv",
"status": "success",
"startedAt": "2026-04-29 14:32:11.123",
"lastEventAt": "2026-04-29 14:32:47.901",
"elapsed": "36.78 sec",
"lastError": null
},
{
"pipelineToken": "pt-def67890-...",
"pipeline": "sales_data",
"filename": "orders.csv",
"status": "error",
"startedAt": "2026-04-29 14:32:11.140",
"lastEventAt": "2026-04-29 14:33:02.512",
"elapsed": "51.37 sec",
"lastError": {
"processName": "PostgresLoader",
"description": "duplicate key value violates unique constraint \"customers_pkey\""
}
}
]
},
"events": [ /* the same raw status entries as without withrollup */ ]
}
```
`rollup.allDone` flips to `true` when every job has reached a terminal state. `rollup.status` is the aggregate outcome — `success`, `warning`, `error`, or `processing`. Per-job `status` values are `success`, `warning`, `error`, `processing`, or `timed_out` (no terminal event within 8 hours of the first event). When a job's `status` is `error`, `lastError` carries the failing process and its message.
**Live Read result fields.** A job whose pipeline is a [Live Read](/destinations/live-read) pipeline (the `scratch` destination) carries its result on the rollup once the run finishes. These fields are absent on every other job:
| Field | Type | Description |
| - | - | - |
| `resultUri` | string | Where the result is held. Present only when the run produced a result. |
| `resultRowCount` | integer | Total rows in the result. |
| `resultExpiresAt` | string | When the result is deleted; reads after this return `410`. |
| `resultPreview` | array | The first rows, inline — up to the configured inline cap (`scratchInlineRows`). |
| `resultTruncated` | boolean | `true` when the result has more rows than `resultPreview` holds — page the rest with [Get a Live Read Pipeline's Result](#get-a-live-read-pipelines-result). |
Polling pattern (agents and clients):
1. Call with `withrollup=true`.
2. Re-call every few seconds until `rollup.allDone` is `true`.
3. Read `rollup.status` for the outcome and `rollup.jobs[].lastError` for any failures.
Without `withrollup=true`, all three queries return their original array shapes (no wrapper) — backward compatible.
***
\*Use one of `publishertoken`, `pipelinetoken`, or `pipelinename`. When multiple are supplied, `publishertoken` wins over `pipelinetoken` wins over `pipelinename`.
***
## Get a Live Read Pipeline's Result
```
GET /api/v1/pipeline/result?pipelinetoken={token}&offset={offset}&limit={limit}
```
Page through the rows a [Live Read](/destinations/live-read) pipeline run produced. Paging reads the stored result — it never re-runs the source. Only Live Read pipelines have a result.
**Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `pipelinetoken` | query | Yes\* | Pipeline token of one ingestion job |
| `publishertoken` | query | Yes\* | Publisher token from `/tap/run` — resolves to the Live Read job that run submitted |
| `offset` | query | No | Row offset to start from (default `0`) |
| `limit` | query | No | Rows requested for this page. Clamped server-side to the inline cap (`scratchInlineRows`), so advance by the page's `returnedCount`, never by `limit`. |
\*Exactly one of `pipelinetoken` or `publishertoken`.
**Example:**
```bash theme={null}
curl "http://localhost:8080/api/v1/pipeline/result?pipelinetoken=pt-abc12345-...&offset=200"
```
**Response:** `200 OK`
```json theme={null}
{
"records": [
{ "id": "201", "city": "c201" },
{ "id": "202", "city": "c202" }
],
"rowCount": 300,
"returnedCount": 100,
"offset": 200,
"truncated": false,
"resultUri": "...",
"resultExpiresAt": "2026-09-19T10:00:05Z"
}
```
| Field | Description |
| - | - |
| `records` | This page's rows, one object per row |
| `rowCount` | Total rows in the whole result |
| `returnedCount` | Rows on this page |
| `offset` | The offset this page starts at |
| `truncated` | `true` when more rows follow — the next page starts at `offset + returnedCount` |
| `resultUri` | Where the result is held |
| `resultExpiresAt` | When the result is deleted |
**Errors:**
| Status | Meaning |
| - | - |
| `400` | `offset` or `limit` is not an integer |
| `404` | No run matched the token, or the pipeline is not a Live Read pipeline — only Live Read pipelines have a result |
| `410` | The result has expired; run the pipeline again |
***
## Clear All Status
```
DELETE /api/v1/pipeline/status
```
Deletes **all** ingestion history — every status entry (the detail events returned by the token queries) and every job summary (the entries returned by the pipeline-name query). There are no parameters and no filtering: this is an all-or-nothing reset, typically used to clear out accumulated history on a dev or demo instance.
**Example:**
```bash theme={null}
curl -X DELETE "http://localhost:8080/api/v1/pipeline/status"
```
**Response:** `200 OK` — the total number of entries removed (detail entries plus summary entries combined):
```json theme={null}
{
"deleted": 1234
}
```
This permanently removes the processing history for every pipeline, not just one. Pipeline configurations and loaded data are untouched — only the status/history records are deleted.
***
## Status Fields
Each status entry (`PipelineStatus`) contains:
| Field | Description |
| - | - |
| `id` | Entry index (internal) |
| `dateTime` | Human-readable timestamp |
| `pipeline` | Pipeline name |
| `processName` | Processing stage name (see below) |
| `publisherToken` | Publisher identifier (if provided on upload) |
| `pipelineToken` | Pipeline job token |
| `filename` | Source filename |
| `state` | Stage state: `begin`, `processing`, `end`, or `error` |
| `code` | Same as `state` |
| `description` | Detail message. On a failed job's `Process completed, error:` entry, that prefix followed by the error and its causes |
| `detail` | Optional. On a failed job's `Process completed, error:` entry, the full stack trace; absent otherwise and on entries written by earlier versions |
| `epoch` | Unix epoch milliseconds |
## State Values
| State | Description |
| - | - |
| `begin` | Processing stage started |
| `processing` | In progress with detail message |
| `end` | Processing stage completed |
| `error` | Processing stage failed |
## Process Names
| Process | Description |
| - | - |
| `FileNotifier` | File intake from MinIO bucket |
| `StreamNotifier` | Direct upload intake (also tap-fed and Kafka-fed jobs) |
| `FileUploadAPIController` | Upload request handling before the job is created |
| `DataQuality` | Data quality validation |
| `Transformation` | Data transformation |
| `JobRunner` | Destination orchestration. When a destination loader fails, the job's `lastError` is attributed to that loader (below), not to `JobRunner`; the `JobRunner` end event carries the full stack trace |
| `PostgresLoader` | PostgreSQL loading |
| `SnowflakeLoader` | Snowflake loading |
| `DatabricksLoader` | Databricks loading |
| `MongoDBLoader` | MongoDB loading |
| `SparkObjectStoreLoader` | Object store writing |
| `KafkaLoader` | Kafka producing |
| `ActiveMQLoader` | ActiveMQ queue writing |
| `RestEndpointRunner` | REST endpoint posting (preprocessor and REST destination) |
| `PGVectorLoader` | pgvector loading |
| `QdrantLoader` | Qdrant loading |
| `WeaviateLoader` | Weaviate loading |
| `MilvusLoader` | Milvus loading |
| `ChromaLoader` | Chroma loading |
# Clear all ingestion history
Source: https://docs.datris.ai/api-reference/status/clear-all-ingestion-history
/openapi.yaml delete /api/v1/pipeline/status
Deletes every job status entry — detail rows and per-pipeline summaries — for the environment.
# Get a Live Read (scratch) pipeline's result
Source: https://docs.datris.ai/api-reference/status/get-a-live-read-scratch-pipelines-result
/openapi.yaml get /api/v1/pipeline/result
Page through the rows a Live Read (scratch destination) pipeline run produced. Paging reads the stored result and never re-runs the source. Only Live Read pipelines have a result. `limit` is clamped server-side to the inline cap, so the next page starts at `offset + returnedCount`, never `offset + limit`. Results expire after the retention window.
# Get job status
Source: https://docs.datris.ai/api-reference/status/get-job-status
/openapi.yaml get /api/v1/pipeline/status
Query job status by pipeline token or pipeline name. Returns detailed status when queried by token, or paginated summary when queried by pipeline name.
# Get server version
Source: https://docs.datris.ai/api-reference/status/get-server-version
/openapi.yaml get /api/v1/version
# Kill a running job
Source: https://docs.datris.ai/api-reference/status/kill-a-running-job
/openapi.yaml post /api/v1/job/kill
Interrupt and cancel a running pipeline job by its pipeline token.
# Create or update a tap prompt fragment
Source: https://docs.datris.ai/api-reference/tap-prompts/create-or-update-a-tap-prompt-fragment
/openapi.yaml post /api/v1/tap-prompts
Creates a new fragment or updates the existing fragment with the same `key`. Timestamps are managed server-side.
# Delete a tap prompt fragment
Source: https://docs.datris.ai/api-reference/tap-prompts/delete-a-tap-prompt-fragment
/openapi.yaml delete /api/v1/tap-prompts/{key}
# Get a tap prompt fragment
Source: https://docs.datris.ai/api-reference/tap-prompts/get-a-tap-prompt-fragment
/openapi.yaml get /api/v1/tap-prompts/{key}
# List tap prompt fragments
Source: https://docs.datris.ai/api-reference/tap-prompts/list-tap-prompt-fragments
/openapi.yaml get /api/v1/tap-prompts
Returns every prompt fragment. Fragments are per-source snippets injected into the tap code-generation system prompt when a tap's description mentions the fragment's key or an alias.
# Suggest fragment content (AI)
Source: https://docs.datris.ai/api-reference/tap-prompts/suggest-fragment-content-ai
/openapi.yaml post /api/v1/tap-prompts/suggest
Uses AI to draft (or refine) the content of a prompt fragment from its key, aliases, and any existing content.
# Taps API
Source: https://docs.datris.ai/api-reference/taps-api
API endpoints for managing taps
**Beta.** The Taps API is stable but may add fields or refine response shapes in upcoming releases. Existing endpoints will remain backward-compatible where possible.
## List Taps
```
GET /api/v1/taps
```
Returns all registered taps.
**Response:** JSON array of tap configurations.
## Get Tap
```
GET /api/v1/tap?name={tapName}
```
Returns a single tap configuration plus the Python script body from MinIO.
**Parameters:**
| Parameter | Type | Description |
| - | - | - |
| `name` | query | Tap name (required) |
**Response (success):**
```json theme={null}
{
"name": "stock-prices-tap",
"description": "Fetch daily stock prices from yfinance",
"scriptPath": "tap-scripts/stock-prices-tap_a1b2c3d4.py",
"targetPipeline": "stock-prices-pipeline",
"script": "def fetch():\n ...",
"packages": ["yfinance"],
"enabled": true,
"tapType": "structured"
}
```
**Response when the stored script is missing from object storage:**
```json theme={null}
{
"name": "stock-prices-tap",
"scriptPath": "tap-scripts/stock-prices-tap_a1b2c3d4.py",
"script": null,
"scriptMissing": true,
...
}
```
`scriptMissing` only appears when `scriptPath` is set but the object is absent — typically after an interrupted edit or re-generation wiped the old script before the new one landed. Distinguish from "never generated" (`scriptPath` null, `script` null, no `scriptMissing` key). The Edit Tap UI uses this flag to render a recoverable-state banner instead of the generic "Generate a script first" validation.
## Create or Update Tap
```
POST /api/v1/tap
Content-Type: application/json
```
**Body:**
```json theme={null}
{
"name": "stock-prices-tap",
"description": "Fetch daily stock prices from yfinance",
"scriptPath": "tap-scripts/stock-prices-tap_a1b2c3d4.py",
"targetPipeline": "stock-prices-pipeline",
"packages": ["yfinance"],
"secretName": "my-api-creds",
"cronExpression": "0 0 0 * * ?",
"enabled": true,
"tapType": "structured"
}
```
| Field | Type | Default | Description |
| - | - | - | - |
| `tapType` | string | `"structured"` | `"structured"` or `"document"`. Document taps return `{uri, filename, content}` dicts and require a `targetPipeline` with an `unstructuredAttributes` source and a vector-store destination. Cannot be changed once set. |
| `catalog` | string | `null` | Free-form label that groups related taps and pipelines in the [Data Catalog](/data-catalog). Empty or null = Uncataloged. |
**Document-tap compatibility rule.** When `tapType == "document"` and `targetPipeline` is non-empty, the server validates that the pipeline has:
* `source.fileAttributes.unstructuredAttributes != null`
* A vector-store destination (one of `qdrant`, `pgvector`, `weaviate`, `milvus`, `chroma`)
Mismatches return **HTTP 400** with a message naming the violation. Also enforced at run time in `TapRunner.feedDocumentPipeline` in case the pipeline is reshaped after the tap is saved.
**Test-before-cron rule (HTTP 409).** A tap may only carry a `cronExpression` once the exact script (or, for HTTP taps, the exact `endpointUrl`) that will run on that schedule has passed a test run (`POST /api/v1/tap/run` with `mode: "test"`). The save is refused with **HTTP 409** and nothing is persisted (no new version is minted) when either:
* the `cronExpression` is being set or changed and the stored tap has no successful test for the script being saved, or
* the `cronExpression` is unchanged but the script (new `scriptPath` / `scriptCommitSha` / `endpointUrl`) differs from the one that last passed a test.
```json theme={null}
{
"error": "Tap 'stock-prices-tap' cannot be scheduled: its script has never passed a test run. Remedy: save the tap without `cronExpression`, call `test_tap`, then `update_tap` with the cron."
}
```
Remedy, in order: save the tap **without** `cronExpression`; run it with `mode: "test"` until it succeeds (the server stamps the tested script on the tap as `lastTestRunScriptId`); then save again with the `cronExpression`. Clearing a cron, and cadence-only or metadata edits (description, tags, `targetPipeline`, `enabled`) on a tap whose current script has passed a test, are always accepted. Test state is read from the stored tap only — `lastTestRun*` fields in the request body are ignored. Pre-existing scheduled taps keep running and stay editable; the first cron or script change on such a tap is accepted if the tap has ever tested or run successfully, otherwise it needs a test run first. The same rule applies to `POST /api/v1/tap/version/restore`. `POST /api/v1/tap/script` applies the rule up front: storing different bytes for a tap that currently has a `cronExpression` is refused with the same **HTTP 409** body before anything is written to the object store or code repository (so the repository never drifts ahead of the tap's pinned commit). Clear the cron first (save with `cronExpression: null`), then store, test, and set the cron. Storing identical bytes, or storing for an unscheduled tap, is always accepted. The AI script endpoints (`/tap/generate`, `/tap/fix`, `/tap/review`, `/tap/optimize`) still return the new script and its `scriptPath` with HTTP 200, but do not move a scheduled tap onto the new bytes; when that happens the body also carries the gate message under `error`.
## Delete Tap
```
DELETE /api/v1/tap?name={tapName}
```
Deletes the tap configuration and its script from MinIO. For document taps, also clears the ledger entries and deletes all staged MinIO objects for that tap.
## Brainstorm (AI Chat)
```
POST /api/v1/tap/brainstorm
Content-Type: application/json
```
Multi-turn conversational endpoint that helps the user refine a vague tap idea into a clear instruction. The UI calls this on every message in the brainstorm chat panel of the tap creation wizard. The AI:
* Asks one focused clarifying question at a time
* Suggests specific data sources (e.g., yfinance, Alpha Vantage, Open-Meteo)
* Recognizes Datris platform tables and uses the metadata/query endpoints
* Returns an updated **instruction draft** on every turn so the UI can keep the instruction box in sync
* Returns **suggestedEnvVars** when an external API requires authentication
**Body:**
```json theme={null}
{
"messages": [
{"role": "user", "content": "I'm looking for company earnings data"},
{"role": "assistant", "content": "Which companies?..."},
{"role": "user", "content": "Tickers from the consumer_discretionary_earnings table on Datris"}
],
"currentDescription": "Optional: the current draft instruction so the AI has context",
"tapType": "structured"
}
```
| Field | Type | Default | Description |
| - | - | - | - |
| `tapType` | string | `"structured"` | Selects the system prompt. `"document"` uses a document-tap-aware prompt that asks about document sources (SharePoint, S3, URL listings, etc.) and never asks about chunking, embedding, or vector-store details (those are pipeline concerns). |
**Response:**
```json theme={null}
{
"reply": "Got it. Which earnings data source — yfinance (free), Alpha Vantage, or Polygon.io?",
"description": "Query the consumer_discretionary_earnings table via /api/v1/query/postgres to get the ticker list. (Source TBD.)",
"suggestedEnvVars": []
}
```
After the user picks Alpha Vantage, a subsequent call returns:
```json theme={null}
{
"reply": "Good choice. The script will need an Alpha Vantage API key — please create a tap secret with ALPHA_VANTAGE_API_KEY in the credentials section below.",
"description": "Query the consumer_discretionary_earnings table via /api/v1/query/postgres to get tickers, then for each ticker call the Alpha Vantage EARNINGS endpoint and return historical earnings records. Use os.environ.get('ALPHA_VANTAGE_API_KEY') for authentication.",
"suggestedEnvVars": ["ALPHA_VANTAGE_API_KEY"]
}
```
The AI never suggests `DATRIS_POSTGRES_DATABASE`, `DATRIS_MONGODB_DATABASE`, `DATRIS_PLATFORM_HOST`, or `DATRIS_PLATFORM_PORT` in `suggestedEnvVars` — those are always injected by the platform.
## Generate Script (AI)
```
POST /api/v1/tap/generate
Content-Type: application/json
```
Uses AI to generate a Python `fetch()` script from a plain-English description. The system prompt branches on `tapType` — document taps get instructions to return `{uri, filename, content}` dicts and a different set of rules (no chunking, no embedding, no local-filesystem fallback).
**Body:**
```json theme={null}
{
"description": "Discover all PDFs in the S3 bucket 'contracts' under prefix 2026/",
"tapName": "contracts-tap",
"oldScriptPath": "tap-scripts/old_script.py",
"secretName": "aws-creds",
"tapType": "document"
}
```
| Field | Type | Default | Description |
| - | - | - | - |
| `tapType` | string | `"structured"` | Selects the system prompt. Set to `"document"` when generating a document-tap script. |
**Response:**
```json theme={null}
{
"script": "import requests\n\ndef fetch():\n ...",
"packages": ["yfinance"],
"scriptPath": "tap-scripts/stock-prices-tap_a1b2c3d4.py",
"injectedPrompts": ["rate-limits"]
}
```
`injectedPrompts` lists the names of the [tap prompt fragments](#tap-prompt-fragments) that were injected into the generation prompt (empty when none matched).
## Fix Script (AI Diagnosis)
```
POST /api/v1/tap/fix
Content-Type: application/json
```
Uses AI to fix a script based on a diagnosis of what went wrong.
**Body:**
```json theme={null}
{
"tapName": "stock-prices-tap",
"script": "current script content...",
"diagnosis": "The API endpoint is wrong...",
"logs": "script output logs...",
"error": "error message...",
"oldScriptPath": "tap-scripts/old_script.py"
}
```
**Response:** Same format as Generate Script.
## Test Tap
```
POST /api/v1/tap/test?testLimit={N}
Content-Type: application/json
```
Executes the tap script without sending data to a pipeline. Returns results, logs, and AI diagnosis if issues detected.
**Query parameters:**
| Name | Type | Description |
| - | - | - |
| `testLimit` | integer (optional) | When `> 0`, the runner injects `DATRIS_TAP_TEST_LIMIT=` into the script process. Well-written tap scripts honor this env var to cap their source reads and iteration loops, making tests faster and lower-cost. Cron runs and `mode=run` runs never set this — production runs always read every row. (A `/tap/run` call with `mode=test` caps at 20 records by default; see [Run Tap](#run-tap).) The UI's "Limit test sample to 20 records" checkbox in Create Tap → step 2 sends `testLimit=20`. |
**Body:** A TapConfig JSON object (same as Create).
**Structured response** (`dataType: "csv"` shown):
```json theme={null}
{
"records": [{"ticker": "AAPL", "close": 255.92}, ...],
"recordCount": 503,
"dataType": "csv",
"columns": ["ticker", "date", "open", "high", "low", "close", "volume"],
"logs": "Retrieved 503 tickers...\nBatch 1/11...",
"error": null,
"aiExplanation": null,
"durationMs": 12400
}
```
**Document-tap response** (`dataType: "document"`):
```json theme={null}
{
"records": [
{
"uri": "s3://contracts/2026/acme-msa.pdf",
"filename": "acme-msa.pdf",
"content": "JVBERi0xLjQKJe…",
"content_hash": "2f1c…",
"metadata": {"source": "s3"}
}
],
"recordCount": 1,
"dataType": "document",
"columns": null,
"logs": "Listed 1 object in s3://contracts/2026/",
"error": null,
"durationMs": 3100
}
```
If errors or 0 records are detected, `aiExplanation` contains an AI-generated diagnosis. `durationMs` is the end-to-end test wall time in milliseconds — this is what the [Optimize Script](#optimize-script) endpoint uses as the baseline timing for its perf rewrite.
For `dataType: "csv"`, **column names in `columns` and the keys inside each record in `records` are normalized** by the platform: lowercase, `[a-z0-9_]` only, with `%` rewritten to `percent`. Source data with names like `EPS Estimate` or `Surprise(%)` will appear as `eps_estimate` and `surprise_percent`. JSON/XML results destined for MongoDB are not normalized. See [Schema Definition → Column Naming Rules](/schemas#column-naming-rules). Document-tap records pass through unchanged — the platform never rewrites URI, filename, content, or metadata keys.
## Review Script (Post-Run)
```
POST /api/v1/tap/review
Content-Type: application/json
```
After a successful test, asks the LLM to scan the script's captured stderr/stdout for signals that the script *itself* should change — not for performance. The reviewer looks for rate-limit / throttle / burst warnings, deprecation hints, pagination / partial-response cues, and schema-drift / auth warnings. When a signal is found, the script is regenerated with the appropriate fix (add `time.sleep`, switch to the recommended endpoint, add pagination, update parsing for renamed fields) and persisted to MinIO as a new version. When no signal is found, the script is returned unchanged.
The UI invokes this automatically **before** [Optimize Script](#optimize-script) — if the reviewer rewrites the script, the optimizer is skipped on that pass (correctness from output outranks speed). Callers integrating directly should follow the same order: call `/tap/review` first, re-test on `rewritten=true`, and only call `/tap/optimize` when `rewritten=false`.
**Body:**
```json theme={null}
{
"tapName": "stock-prices-tap",
"script": "current working script content...",
"recordCount": 503,
"durationMs": 298000,
"logs": "Retrieved 503 tickers...\nSource warning: Burst pattern detected...",
"oldScriptPath": "tap-scripts/stock-prices-tap_v3.py"
}
```
| Field | Type | Description |
| - | - | - |
| `tapName` | string | Tap identifier. Used for script path versioning. |
| `script` | string | The current working script that passed its test. |
| `recordCount` | integer | How many records the last test returned. |
| `durationMs` | integer | Wall time of the last test run in ms (from `/tap/test` response). |
| `logs` | string | Captured script stderr/stdout — the reviewer matches signals liberally against this text. |
| `oldScriptPath` | string | MinIO path of the current script. If rewritten, the new script is stored as a new version and the old path is retained for rollback. |
**Response:**
```json theme={null}
{
"script": "regenerated Python 3 script...",
"packages": ["extra-package"],
"scriptPath": "tap-scripts/stock-prices-tap_v4.py",
"changes": [
"Added 0.25s sleep between API calls — source reported burst pattern (5 rps limit)",
"Lowered max_workers from 10 to 3"
],
"rewritten": true
}
```
When `rewritten` is `false`, `script` / `scriptPath` match the input and `changes` is empty — the reviewer found nothing in the output worth acting on. Prompt fragments configured via [Tap Prompt Fragments](#tap-prompt-fragments) are auto-injected into the reviewer's system prompt when their key or any alias appears in the script.
## Optimize Script
```
POST /api/v1/tap/optimize
Content-Type: application/json
```
After a successful test, asks the LLM to restructure a working script for performance — e.g. swap serial HTTP calls for a `ThreadPoolExecutor`, reuse a `requests.Session()`, or drop unnecessary `time.sleep()` — while preserving correctness (`fetch()` signature, `DATRIS_TAP_TEST_LIMIT` handling, Vault env-var reads, retry/backoff behavior, and `raise_for_status` calls).
The UI invokes this automatically after a green test in the Create Tap wizard, with a regression guard that auto-reverts if the rewrite runs ≥20% slower. Callers integrating this endpoint directly should implement the same re-test + revert pattern.
**Body:**
```json theme={null}
{
"tapName": "stock-prices-tap",
"script": "current working script content...",
"recordCount": 503,
"durationMs": 298000,
"logs": "Retrieved 503 tickers...",
"oldScriptPath": "tap-scripts/stock-prices-tap_v3.py"
}
```
| Field | Type | Description |
| - | - | - |
| `tapName` | string | Tap identifier. Used for script path versioning. |
| `script` | string | The current working script that passed its test. |
| `recordCount` | integer | How many records the last test returned. Used in the user prompt so the LLM can reason about per-record timing. |
| `durationMs` | integer | Wall time of the last test run in ms (from `/tap/test` response). |
| `logs` | string | Recent script stderr output (last \~40 lines used). |
| `oldScriptPath` | string | MinIO path of the current script. The optimized script is stored as a new version; the old path is retained so the caller can revert. |
**Response:**
```json theme={null}
{
"script": "optimized Python 3 script...",
"packages": ["extra-package"],
"scriptPath": "tap-scripts/stock-prices-tap_v4.py",
"changes": [
"Parallelized ticker fetches with ThreadPoolExecutor(10)",
"Removed per-item 0.25s sleep"
]
}
```
If the LLM concludes the script is already well-optimized, `changes` is an empty array and `script` / `scriptPath` match the input. In that case no re-test is needed.
## Run Tap
```
POST /api/v1/tap/run
Content-Type: application/json
```
Executes a saved tap. Optionally sends data to the configured pipeline.
**Body:**
```json theme={null}
{
"name": "stock-prices-tap",
"mode": "run",
"params": {"since": "2026-05-01"}
}
```
| Field | Type | Default | Description |
| - | - | - | - |
| `name` | string | (required) | Tap name |
| `mode` | string | `"test"` | `"run"` feeds records into `targetPipeline`; `"test"` executes the script and returns sample results without persisting. A successful `mode=test` also records `lastTestRunStatus: "success"` and `lastTestRunScriptId` (the identity of the tested script or endpoint) on the tap, which is what allows a `cronExpression` to be saved — see the test-before-cron rule under Create or Update Tap |
| `testLimit` | integer (optional) | `20` when `mode=test` | Max records a test pulls before the runner stops the script. Applies to `mode=test` only: the runner injects `DATRIS_TAP_TEST_LIMIT=` and stops pulling from a generator `fetch()` after `N` records. `0` (or a negative value) disables the cap and streams the whole source. Ignored for `mode=run`, which always reads every row |
| `params` | object | `{}` | Optional per-run parameters exposed to the script as environment variables (nested values JSON-encoded) |
**Response:**
```json theme={null}
{
"tap": "stock-prices-tap",
"description": "Fetch daily stock prices...",
"status": "success",
"mode": "run",
"targetPipeline": "stock-prices",
"persisted": true,
"publisherToken": "5b2f4a1d-8c7e-4f0a-9b3d-6e1c2a4f8b9e",
"pipelineTokens": ["a1b2c3d4-..."],
"recordCount": 503,
"dataType": "csv",
"columns": ["ticker", "date", "open", "high", "low", "close", "volume"],
"logs": "...",
"error": null
}
```
`columns` lists the column names the script produced (normalized for `dataType: "csv"`, see [Test Tap](#test-tap)); it is `null` when the run produced no records.
**`mode=run` does not return the records themselves** — the data is in transit to `targetPipeline`, and the agent / caller should verify completion via `get_pipeline_status`, not read from the response body. `recordCount` summarizes how many records were submitted. To preview what a script produces, call the same endpoint with `mode=test`, which pulls only the first 20 records by default (raise or remove the cap with `testLimit`) and returns up to 20 sample rows in a `records` array (with `recordsTruncated: true` set when the script produced more than that). Because of the cap, a test’s `recordCount` is the preview size, not what a `mode=run` call will produce.
`persisted: true` means the records were submitted to `targetPipeline`. When `persisted: false`, a `persistedReason` field names the cause — one of `test_mode`, `run_error`, `no_records`, `no_target_pipeline`, or `debounced`. On persisted runs, `publisherToken` groups every ingestion job this run submitted; `pipelineTokens` lists each. Document taps fan out to many `pipelineTokens` but share one `publisherToken`.
### Run debounce (mode=run only)
`/tap/run` debounces `mode=run` requests per tap on a 5-second window to suppress accidental duplicates — agents that emit parallel `tool_use` blocks, UI buttons that get double-clicked, or transport-level retries. If a second run hits within the window, the response is HTTP 200 with `status: "skipped"`, `persisted: false`, `persistedReason: "debounced"`, and an `error` string explaining how long ago the previous run started. The previous run keeps executing — pivot via `get_tap_logs` and `get_pipeline_status` to track its outcome. `mode=test` is read-only and is never debounced.
### Watching a tap run
A `/tap/run` response comes back as soon as records are handed off to the async ingestion pipeline — the actual load is still in progress. Use `publisherToken` to poll:
```
GET /api/v1/pipeline/status?publishertoken={publisherToken}&withrollup=true
```
Returns a `{rollup, events}` wrapper covering every ingestion job this tap run submitted (for structured taps that's one job; for document taps it's one per submitted document). Poll until `rollup.allDone` is `true`, then read `rollup.status` for the outcome and `rollup.jobs[].lastError` for any failure. See the [Pipeline Status API → Rollup Response](/api-reference/status-api#rollup-response) for the full shape.
## Generate CRON Expression (AI)
```
POST /api/v1/tap/cron
Content-Type: application/json
```
Converts a plain-English schedule description to a Quartz CRON expression.
**Body:**
```json theme={null}
{
"description": "Every weekday at 4pm ET"
}
```
**Response:**
```json theme={null}
{
"cronExpression": "0 0 16 ? * MON-FRI"
}
```
## Run History
```
GET /api/v1/tap/logs?name={tapName}
```
Returns the last 50 run log entries for a tap, sorted by most recent first.
**Response:**
```json theme={null}
[
{
"tapName": "stock-prices-tap",
"runTime": "2026-04-04T20:23:22Z",
"status": "success",
"recordCount": 503,
"dataType": "csv",
"logs": "Retrieved 503 tickers...",
"error": null,
"mode": "test",
"durationMs": 45000,
"publisherToken": null,
"aiSummary": null,
"aiDiagnosis": null,
"aiSuggestion": null,
"scriptCommitSha": null,
"pipelineRollup": null
}
]
```
| Field | Description |
| - | - |
| `publisherToken` | Set on `mode=run` entries that fed a pipeline; groups the ingestion jobs the run submitted |
| `aiSummary`, `aiDiagnosis`, `aiSuggestion` | AI fix suggestion filled in after a scheduled run exhausts its automatic retries; `aiSummary` is the one-line headline, the other two carry the full explanation |
| `scriptCommitSha` | Repo-backed taps only — the commit the run's script was read at; `null` for MinIO-stored scripts |
| `pipelineRollup` | Downstream pipeline job rollup (same shape as `rollup` in the [Status API](/api-reference/status-api)) for runs that have a `publisherToken`; `null` otherwise |
## Document Ledger
Document taps track which files they've already processed in a **ledger** — a MongoDB collection keyed by `{tapName}|{uri}`. See [Document Taps → The Document Ledger](/taps#the-document-ledger) for the concept; these endpoints manage it.
### Read ledger
```
GET /api/v1/tap/ledger?name={tapName}
```
Returns every ledger entry owned by the tap.
**Response:**
```json theme={null}
[
{
"uri": "s3://contracts/2026/acme-msa.pdf",
"tapName": "contracts-tap",
"stagedPath": "tap-docs/contracts-tap/a1b2c3d4_acme-msa.pdf",
"filename": "acme-msa.pdf",
"contentHash": "2f1c…",
"firstSeenAt": "2026-04-18 13:42:38 EDT",
"lastSeenAt": "2026-04-19 07:00:11 EDT",
"status": "processed",
"metadata": {"source": "s3", "region": "EU"}
}
]
```
### Delete one entry (force re-process)
```
DELETE /api/v1/tap/ledger?name={tapName}&uri={uri}
```
Removes a single entry and its staged MinIO object. The next tap run will re-ingest that specific document from source.
### Clear the entire ledger (force full re-scan)
```
DELETE /api/v1/tap/ledger?name={tapName}
```
Removes every entry for the tap plus all staged MinIO objects. The next run re-ingests every document the source exposes.
Also triggered automatically when:
* The tap is deleted (`DELETE /api/v1/tap`).
* The target pipeline's data is cleared (`DELETE /api/v1/pipeline?pipeline=X&deleteData=true`). Without this, the tap would skip docs it already "processed" and the pipeline would stay empty.
## Available Vector Stores
```
GET /api/v1/vector-stores/available
```
Returns the subset of `[qdrant, weaviate, pgvector, milvus, chroma]` whose Vault secret is present **and** whose service is currently reachable. Used by the document-tap pipeline wizard to drive the store picker. Secret presence alone isn't sufficient — the dev stack seeds placeholder secrets for every store, so the endpoint actually probes each service.
**Response:**
```json theme={null}
["pgvector"]
```
Probes use the same logic as `/api/v1/health/services`, with a 2-second timeout per store (\~10 s worst case if everything is unreachable).
## Tap Prompt Fragments
User-configured system prompt fragments that auto-inject into tap AI flows when the user's text matches the fragment's key or any alias. See [Tap Prompt Fragments](/tap-prompts) for the full concept; this section covers the REST endpoints.
Fragments are per-tenant, stored in MongoDB at `{env}-tap-prompt`, and matched case-insensitively with word boundaries.
### List Fragments
```
GET /api/v1/tap-prompts
```
**Response:** Array of `TapPromptFragment` objects.
```json theme={null}
[
{
"key": "AWS",
"aliases": ["S3", "EC2", "Lambda", "boto3"],
"content": "When fetching data from AWS services, prefer boto3 with IAM credentials...",
"enabled": true,
"createdAt": "2026-04-20 14:05:00 UTC",
"updatedAt": "2026-04-20 14:05:00 UTC"
}
]
```
### Get Fragment
```
GET /api/v1/tap-prompts/{key}
```
Returns a single fragment or HTTP 404 if `{key}` is not found. `{key}` should be URL-encoded.
### Create or Update Fragment
```
POST /api/v1/tap-prompts
Content-Type: application/json
```
Create-or-update semantics — if a fragment with the same `key` already exists, it is overwritten. `createdAt` is preserved across updates; `updatedAt` is refreshed on every write. Writing through this endpoint invalidates the in-process fragment cache immediately, so the next tap generation / fix / optimize / brainstorm call sees the change without restart.
**Body:**
```json theme={null}
{
"key": "Polygon",
"aliases": ["polygon.io"],
"content": "Use the requests library with os.environ.get(\"POLYGON_API_KEY\") and pass auth via the Authorization: Bearer {key} header (query param ?apiKey= also works but prefer the header).",
"enabled": true
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `key` | string | Yes | Primary match keyword. Unique per tenant. Matched case-insensitively with `\b` word boundaries. |
| `aliases` | string\[] | No | Additional match keywords. Same matching rules as `key`. |
| `content` | string | Yes | System-prompt text appended under a `## User-provided context` block when a match fires. |
| `enabled` | boolean | No (default `true`) | When `false`, the fragment stays in storage but is excluded from matching. |
**Response:** `{"status": "ok"}` on success.
### Delete Fragment
```
DELETE /api/v1/tap-prompts/{key}
```
Deletes the fragment and invalidates the cache. URL-encode `{key}`. Returns `{"status": "ok"}`.
### Suggest Fragment Content
```
POST /api/v1/tap-prompts/suggest
Content-Type: application/json
```
Asks the LLM to draft a content body from the fragment's key and aliases. Useful as a starting point when creating a fragment — the UI exposes this via the **Suggest** button next to the Content field. If `content` is non-empty in the request, the LLM is instructed to refine/expand it rather than replace.
**Body:**
```json theme={null}
{
"key": "Stripe",
"aliases": ["stripe.com"],
"content": ""
}
```
**Response:**
```json theme={null}
{
"content": "Use the stripe Python SDK (pip install stripe). Set stripe.api_key = os.environ.get(\"STRIPE_API_KEY\"). Use auto_paging_iter() for list endpoints to transparently handle pagination..."
}
```
The response is plain text — the UI drops it into the Content textarea for the user to review before saving.
## Authentication
All endpoints require the `x-api-key` header if API key authentication is enabled.
# Brainstorm chat (AI)
Source: https://docs.datris.ai/api-reference/taps/brainstorm-chat-ai
/openapi.yaml post /api/v1/tap/brainstorm
Multi-turn conversational endpoint that helps refine a vague tap idea into a clear instruction.
The AI asks one focused clarifying question at a time, suggests data sources, recognizes Datris
platform tables, and returns an updated instruction draft on every turn.
# Clear the ledger (force re-scan) or delete one entry (force re-process)
Source: https://docs.datris.ai/api-reference/taps/clear-the-ledger-force-re-scan-or-delete-one-entry-force-re-process
/openapi.yaml delete /api/v1/tap/ledger
With only `name`, clears the entire ledger for the tap and deletes every staged MinIO object — the next run re-ingests every document from source. With `name` and `uri`, deletes one entry and its staged object so that specific document is re-processed on the next run.
# Create or update a tap
Source: https://docs.datris.ai/api-reference/taps/create-or-update-a-tap
/openapi.yaml post /api/v1/tap
Creates a new tap or updates an existing tap with the same `name`.
Cron validation: `cronExpression` is a Quartz expression of 6 fields
(seconds minutes hours day-of-month month day-of-week) plus an optional
year. A malformed expression — including the 5-field Unix form — is
refused with 400 and nothing is persisted; for a 5-field input the error
carries the 6-field equivalent to resend. A blank cron means unscheduled.
Test-before-cron: a `cronExpression` may only be saved once the exact script
(or HTTP endpoint) that will run on it has passed a `mode=test` run. Setting or
changing a cron on an untested script, or changing the script under an existing
cron, is refused with 409 and nothing is persisted. Remedy: save without
`cronExpression`, run `POST /api/v1/tap/run` with `mode=test`, then save again
with the cron.
# Delete a tap
Source: https://docs.datris.ai/api-reference/taps/delete-a-tap
/openapi.yaml delete /api/v1/tap
Deletes the tap configuration and its script from MinIO.
# Fix tap script (AI diagnosis)
Source: https://docs.datris.ai/api-reference/taps/fix-tap-script-ai-diagnosis
/openapi.yaml post /api/v1/tap/fix
Uses AI to fix a script based on a diagnosis of what went wrong.
# Generate CRON expression (AI)
Source: https://docs.datris.ai/api-reference/taps/generate-cron-expression-ai
/openapi.yaml post /api/v1/tap/cron
Converts a plain-English schedule description to a Quartz CRON expression.
# Generate tap script (AI)
Source: https://docs.datris.ai/api-reference/taps/generate-tap-script-ai
/openapi.yaml post /api/v1/tap/generate
Uses AI to generate a Python `fetch()` script from a plain-English description. The system prompt branches on `tapType` — document taps get a prompt that instructs the LLM to return `{uri, filename, content}` dicts, never chunk or embed, and never fall back to scanning the local filesystem.
# Get a tap configuration
Source: https://docs.datris.ai/api-reference/taps/get-a-tap-configuration
/openapi.yaml get /api/v1/tap
Returns a single tap configuration, including the script content from MinIO.
# Get run history across all taps
Source: https://docs.datris.ai/api-reference/taps/get-run-history-across-all-taps
/openapi.yaml get /api/v1/tap/logs/all
Returns run log entries for every tap, newest window first. Feeds the Ops activity dashboard's aggregation. Defaults to the last 30 days, capped at 2000 rows.
# Get tap run history
Source: https://docs.datris.ai/api-reference/taps/get-tap-run-history
/openapi.yaml get /api/v1/tap/logs
Returns the last 50 run log entries for a tap, sorted by most recent first.
# List all taps
Source: https://docs.datris.ai/api-reference/taps/list-all-taps
/openapi.yaml get /api/v1/taps
Returns all registered tap configurations.
# Live progress of an in-flight script generation
Source: https://docs.datris.ai/api-reference/taps/live-progress-of-an-in-flight-script-generation
/openapi.yaml get /api/v1/tap/generate/status
Reports what a blocking `/tap/generate` call is currently doing so clients can show real progress during the 1–3 minute wait. Poll every few seconds while a generate request is in flight. `active: false` means no generation is running for this tap.
# Move a tap script between storage backends
Source: https://docs.datris.ai/api-reference/taps/move-a-tap-script-between-storage-backends
/openapi.yaml post /api/v1/tap/migrate-storage
Reads the script from its current backend, writes it to the target (built-in object storage or the code repository), and stamps the tap as a versioned edit. Idempotent when the tap is already on the target. Built-in objects are never deleted on migrate-out — version snapshots pin them.
# Optimize tap script (AI)
Source: https://docs.datris.ai/api-reference/taps/optimize-tap-script-ai
/openapi.yaml post /api/v1/tap/optimize
Uses AI to optimize a working script for speed and efficiency (based on the latest run's record count and duration). Always stores the returned script; if the tap already exists, its `scriptPath` is updated.
# Overwrite a tap's incremental-sync state
Source: https://docs.datris.ai/api-reference/taps/overwrite-a-taps-incremental-sync-state
/openapi.yaml post /api/v1/tap/state
Sets the state object the next run receives via DATRIS_TAP_STATE — e.g. rewind a cursor so a window is re-fetched. The state shape is defined by the tap's own script. Committed state is capped at 64 KB.
# Pull a repo-backed tap script from its branch head
Source: https://docs.datris.ai/api-reference/taps/pull-a-repo-backed-tap-script-from-its-branch-head
/openapi.yaml post /api/v1/tap/script/pull
Re-reads a repository-stored tap script at the configured branch head and reports drift against the tap's pinned commit. With `apply=true`, also advances the pin (a versioned edit).
# Read a document tap's ledger
Source: https://docs.datris.ai/api-reference/taps/read-a-document-taps-ledger
/openapi.yaml get /api/v1/tap/ledger
Returns every ledger entry owned by the tap. The ledger records which documents the tap has already staged so re-runs skip unchanged files.
# Read a tap's incremental-sync state
Source: https://docs.datris.ai/api-reference/taps/read-a-taps-incremental-sync-state
/openapi.yaml get /api/v1/tap/state
Returns the bookmark/cursor the tap's script saved after its last successful run — the object the next run receives via the DATRIS_TAP_STATE environment variable. `state` is null when the tap has never committed state (non-incremental tap, or no successful run yet).
# Reset a tap's incremental-sync state
Source: https://docs.datris.ai/api-reference/taps/reset-a-taps-incremental-sync-state
/openapi.yaml delete /api/v1/tap/state
Deletes the stored state entirely. The next run sees no DATRIS_TAP_STATE and performs a full first-run fetch.
# Review tap script (AI)
Source: https://docs.datris.ai/api-reference/taps/review-tap-script-ai
/openapi.yaml post /api/v1/tap/review
Uses AI to review a script after a successful test run — correctness, robustness, and adherence to tap conventions. May return the script rewritten. When it rewrites and the tap already exists, the tap's `scriptPath` is updated to the new object.
# Run a tap
Source: https://docs.datris.ai/api-reference/taps/run-a-tap
/openapi.yaml post /api/v1/tap/run
Executes a saved tap. Optionally sends data to the configured pipeline.
# Store a tap script
Source: https://docs.datris.ai/api-reference/taps/store-a-tap-script
/openapi.yaml post /api/v1/tap/script
Stores a (typically hand-edited) Python script in the resolved storage backend and, if the tap already exists, updates its script reference. Backend resolution: explicit `storage` in the body, else the existing tap's backend, else the tenant default (the code repository when one is enabled, built-in storage otherwise).
# Test a tap
Source: https://docs.datris.ai/api-reference/taps/test-a-tap
/openapi.yaml post /api/v1/tap/test
Executes the tap script without sending data to a pipeline. Returns a preview of the
records (the first 20 of a record list, with `recordsTruncated: true` when there were
more), logs, and an AI-generated diagnosis if errors or zero records are detected.
# Version API
Source: https://docs.datris.ai/api-reference/version-api
Version information endpoints
Returns the pipeline server version, the feature flags this install runs with, and the tap budgets in force (read those before sizing a large tap run).
## Get Version
```
GET /api/v1/version
```
**Example:**
```bash theme={null}
curl http://localhost:8080/api/v1/version
```
**Response:** `200 OK`
```json theme={null}
{
"version": "1.28.0",
"environment": "oss",
"multiTenant": "false",
"hosted": "false",
"useUserAuth": "false",
"useApiKeys": "false",
"useAuditLog": "false",
"useAgentPolicy": "false",
"recoveryAgentEnabled": "false",
"postgresDatabase": "datris",
"mongodbDatabase": "datris",
"useTapRunner": "false",
"pipelineMaxPayloadMB": "4096",
"pipelineMaxPayloadMBSource": "default",
"tapScriptTimeoutSeconds": "300",
"tapScriptTimeoutSecondsSource": "default",
"tapRunTimeoutSeconds": "3600",
"tapRunTimeoutSecondsSource": "default"
}
```
| Field | Description |
| - | - |
| `version` | Server version |
| `environment` | Environment name (e.g., `oss` for self-hosted, or tenant prefix for managed hosting) |
| `multiTenant` | Whether the server is running in multi-tenant mode |
| `hosted` | Whether this is a managed/hosted deployment |
| `useUserAuth` | Whether user authentication is enabled |
| `useApiKeys` | Whether API key authentication is enabled |
| `useAuditLog` | Whether the audit log is enabled |
| `useAgentPolicy` | Whether agent policy enforcement is enabled |
| `recoveryAgentEnabled` | Whether the recovery agent is enabled |
| `useTapRunner` | Whether tap scripts run in the separate tap-runner service |
| `postgresDatabase` | Canonical Postgres database name. User-facing; UI reads this as authoritative and doesn't let users edit it. |
| `mongodbDatabase` | Canonical MongoDB database name (user-facing). In multi-tenant mode this is the tenant environment name; in single-tenant it comes from `mongodb.database` in application.yaml. Distinct from the internal platform database (`mongodb.internalDatabase`, default `oss`) which holds pipeline/tap configs and run state and is never surfaced in the UI. |
| `pipelineMaxPayloadMB` | Per-run payload disk budget in force on this install, in MB (`0` = unlimited). Agents size a tap run against this value, not against the documented default of 4096. |
| `pipelineMaxPayloadMBSource` | Where the disk budget came from: `env` (`PIPELINE_MAX_PAYLOAD_MB`), `deprecated-alias` (`TAP_MAX_OUTPUT_MB`) or `default` |
| `tapScriptTimeoutSeconds` | Wall-clock ceiling in force for a tap test, in seconds |
| `tapScriptTimeoutSecondsSource` | Where the test ceiling came from: `env` (`TAP_SCRIPT_TIMEOUT_SECONDS`) or `default` |
| `tapRunTimeoutSeconds` | Wall-clock ceiling in force for a real or scheduled tap run, in seconds |
| `tapRunTimeoutSecondsSource` | Where the run ceiling came from: `env` (`TAP_RUN_TIMEOUT_SECONDS`), `derived` (unset, resolved to `max(3600, tapScriptTimeoutSeconds)`) or `default` |
# Diff two pipeline versions
Source: https://docs.datris.ai/api-reference/versions/diff-two-pipeline-versions
/openapi.yaml get /api/v1/pipeline/version/diff
# Diff two tap versions
Source: https://docs.datris.ai/api-reference/versions/diff-two-tap-versions
/openapi.yaml get /api/v1/tap/version/diff
Server-computed, ready-to-render diff of the tap configuration and script between two versions.
# Get a pipeline version snapshot
Source: https://docs.datris.ai/api-reference/versions/get-a-pipeline-version-snapshot
/openapi.yaml get /api/v1/pipeline/version
# Get a tap version snapshot
Source: https://docs.datris.ai/api-reference/versions/get-a-tap-version-snapshot
/openapi.yaml get /api/v1/tap/version
# List pipeline definition versions
Source: https://docs.datris.ai/api-reference/versions/list-pipeline-definition-versions
/openapi.yaml get /api/v1/pipeline/versions
Lists the saved definition versions of a pipeline, newest first.
# List tap definition versions
Source: https://docs.datris.ai/api-reference/versions/list-tap-definition-versions
/openapi.yaml get /api/v1/tap/versions
Lists the saved definition versions of a tap, newest first. Every versioned save (create, update, restore) appends a snapshot.
# Restore a pipeline version
Source: https://docs.datris.ai/api-reference/versions/restore-a-pipeline-version
/openapi.yaml post /api/v1/pipeline/version/restore
Restores the pipeline's definition from the given version snapshot, preserving ownership. The restore itself is saved as a new version.
# Restore a tap version
Source: https://docs.datris.ai/api-reference/versions/restore-a-tap-version
/openapi.yaml post /api/v1/tap/version/restore
Restores the tap's DEFINITION from the given version snapshot, preserving the live tap's creation metadata, ownership, and run-status fields. The restore itself is saved as a new version.
# Assistant
Source: https://docs.datris.ai/assistant
Chat with an in-product agent that finds data sources and builds taps and pipelines for you.
The **Assistant** tab is an in-product agent that wires up taps and pipelines
for you. You describe what data you want; the agent finds a source, generates
the tap script, creates the pipeline, links them, and shows you the result —
all in one chat.
It uses the same MCP tools that external agents (Claude Desktop, Cursor) see
when they connect to your Datris MCP server. There is one canonical workflow,
shared between in-product and external agents.
## What you can ask
The Assistant is open-ended. Some examples:
* *"Find a source of US public-company SEC filings and build a tap and pipeline."*
* *"Pull current weather data from a public API and create a pipeline for it."*
* *"Ingest the most recent NYC taxi trip data into a pipeline."*
* *"List the taps that are currently scheduled and tell me which haven't run in over a week."*
* *"Show me the rows in the `weather` pipeline."* (works for MongoDB, PostgreSQL, Snowflake, Databricks, **Object Store (Parquet / ORC)**, or vector destinations — the agent picks the right query tool for the destination)
The agent will reach for tools as it needs them: web search to find a source,
`list_taps` / `list_pipelines` to check for existing work, `create_tap_secret`
when credentials are needed, `create_tap`, `test_tap`, `create_pipeline`, and
so on.
## Drop a file to build a pipeline
You can drag a file straight into the chat — or attach one with the file
picker — and the Assistant will build a pipeline around it for you. Drop a CSV,
JSON, XML, or document file onto the chat window (or click the attach control
and pick it), then describe what you want done with it.
When you send a message with a file attached, the agent:
1. **Reads a sample** of the file — a text extract from the attached content —
so it can see the columns and data shape.
2. **Proposes a destination**, defaulting sensibly for the data and naming the
alternatives (Postgres, MongoDB, an object store, Snowflake, Databricks, or a
vector store) so you
can pick a different one.
3. **Confirms before building** — it tells you what it's about to create and
waits for your go-ahead.
4. **Asks about an upsert key** when relevant, so repeated loads update rows in
place instead of duplicating them.
5. **Creates the pipeline, loads the data, and reports the result** — including
how many rows landed in the destination.
This is the fastest path from "here's a file" to a working pipeline: no wizard,
no manual schema entry — just drop it in and confirm.
## What you'll see in real time
Three live layers in every assistant turn:
1. **💭 Thinking** — the model's chain-of-thought, streaming character-by-character.
Anthropic tenants see the full reasoning; OpenAI tenants see a high-level
summary instead. Click the block to expand.
2. **Tool cards** — inline within the response. Each card shows a human-friendly
label (*"🔍 Searching the web for SEC EDGAR API…"*, *"🛠 Creating tap sec-edgar…"*)
with a spinner while running, then a ✓ or ✕. Click to expand the raw input
and result.
3. **Streaming prose** — the response text appears as the model writes it, with
tool calls inlined where the model invoked them.
When the agent finishes, **Open tap** and **Open pipeline** links appear at the
bottom of the turn so you can jump straight to anything it created.
## Safety
The agent has access to every MCP tool, including destructive ones
(`delete_tap`, `delete_pipeline`, `delete_tap_secret`, `update_secret`). It is
instructed to **never invoke a destructive tool without asking you first** —
if you say "delete the broken tap," the agent will name what it's about to
delete and wait for your confirmation in the chat before proceeding.
The Stop button at the bottom of the chat aborts the loop server-side at the
next checkpoint, so you can interrupt anytime. Stop halts the in-flight model
call within a fraction of a second — cancelled responses cost only what was
already streamed.
## Platform-tab secrets are visible but read-only
When a pipeline destination needs operator-owned credentials (AWS S3, an
external Postgres, a vector-store endpoint, etc.), the agent can **discover**
secrets you've created on Configuration → Secrets → Platform and verify their
field shape — but it cannot create, modify, or delete them. If nothing
suitable exists, the agent will name the field shape the destination needs and
ask you to add the secret in the Secrets tab, then give it the name to use.
Secret values are never exposed to the agent.
## Long-running runs are carried to completion
When the agent runs a tap (`run_tap` or `upload_data`), it owns that work all
the way to a terminal `success` / `warning` / `error` outcome. It will not
kick off the job and tell you to check back later — that defeats the point of
delegating.
The polling pattern it uses:
1. Poll `get_pipeline_status` immediately after the run is submitted (most
small structured taps finish in 1–5 seconds; no point sleeping first).
2. If still in progress, sleep with `wait_seconds` using exponential backoff
(5s → 10s → 20s → 30s → 60s → 60s …, capped at 60s normally, 120s only if
things are genuinely glacial).
3. Reset to a short wait (5–15s) the moment a poll shows new jobs flipping to
a terminal state — progress means it's about to finish.
4. Emit a one-line progress update each cycle ("12 of 28 jobs done, 16 still
processing — checking again in 30s") so you can see the run advance.
5. After \~20 polls, if the run is still in progress, pause and ask whether to
keep waiting, kill the run, or check later — never silently stop.
The same pattern applies to `upload_data` → `get_job_status`. If the chat
seems to be "hanging" mid-run, it's waiting between polls on purpose.
## Reasoning visibility by provider
The Assistant names the chat provider and model it is using (for example *Anthropic · claude-fable-5-1 · extended thinking on*) on the empty conversation and in the top bar once you start chatting, with a link to **Configuration → AI Providers** where you change it. The line refreshes when you return to the Assistant tab or start a new chat, so it follows a change you just saved there.
| Provider | Streaming text | Tool cards | Reasoning block |
| - | - | - | - |
| Anthropic Claude (Opus 5.5, Fable 5.1) | ✅ | ✅ | Full chain-of-thought (extended thinking) |
| OpenAI GPT-5.x / GPT-6 Astra | ✅ | ✅ | Reasoning summary |
| Azure OpenAI | ✅ | ✅ | None (reasoning happens server-side but isn't surfaced) |
| Claude via Amazon Bedrock | ✅ | ✅ | Full thinking blocks (same as direct Anthropic; responses arrive at the end of each turn rather than streaming) |
| Grok (xAI) | ✅ | ✅ | None (reasoning happens server-side but isn't surfaced) |
| Ollama / Llama | ✅ | If the model supports tool calling | None |
Set `ai.extendedThinking: "false"` in `application.yaml` to disable the
reasoning block platform-wide. Default is on. Extended thinking adds roughly
\$0.05/turn and 2–8s of latency on Opus — cheap insurance for transparency
on an agent that's writing production-bound config.
## Configuration
The Assistant uses your tenant's **AI primary** config (the chat slot). Tap
script generation, AI data quality, and AI transformations use the separate
**codegen** slot. On a fresh install the Anthropic default is Claude Opus 5.5
(`claude-opus-5-5`) for both slots; the OpenAI default is `gpt-5.5`.
There is no per-tenant Assistant configuration to manage beyond what you
already have for AI primary. See [AI Configuration](/ai-configuration) for the
underlying AI primary / codegen / web search / embedding settings.
### Authentication
The Assistant inherits the identity of the user running it. How that maps
to permissions depends on which auth mode the platform is running in:
* **`USE_USER_AUTH=true`** (recommended): the Assistant runs as your
logged-in user. Audit log entries and capability checks both show
`session:`. Permissions follow your role — admin gets
full access, editor gets create/update/run on data resources, viewer
gets read-only. See [User Authentication](/user-auth) for the role-to-
capability mapping.
* **`USE_USER_AUTH=false` + `USE_API_KEYS=true`**: the Assistant uses
whatever value you pasted into the Connect prompt (typically the seeded
`ui` key). Same identity for everyone using the browser.
* **`USE_API_KEYS=false`**: no auth on tool calls; the Assistant runs as
the platform's anonymous identity.
The Assistant doesn't have its own dedicated API key — its server-side
MCP-bound tool calls use the same identity the chat endpoint was
authenticated under. Want to give the Assistant *narrower* permissions
than the human user has? That's not exposed today; the workaround is to
log in as a different user (an editor or viewer) when running the
Assistant, so the role mapping applies. See [API Keys](/api-keys) for
the full capability model.
## Limits
* **50 iterations per turn.** The agent loop is hard-capped to bound cost. If
the agent runs out of iterations, it stops and asks you to send a follow-up
message to continue.
* **No server-side conversation history.** The transcript lives in the
browser tab's session storage — it survives a refresh but is gone once the
tab is closed. Durable persistence is on the roadmap.
* **32K output tokens per LLM call.** Sufficient for thinking + reasoning +
tool selection on every public model we support.
## How it relates to other tabs
* **Chat panels** — four panels, one job each: the Assistant (build taps and
pipelines), Ops chat (runs, failures, incidents — see [Incidents](/incidents)),
Catalog chat (curation — see [Data Catalog](/data-catalog)) and the
Configuration assistant (settings — see [Configuration Assistant](/configuration-assistant)).
* **MCP tab** — static catalog of the tools the Assistant uses. Read this if
you want to understand what each tool does.
* **Agent Monitor (pop-out)** — read-only activity monitor of *external* MCP
clients (Claude Desktop, Cursor, scripts) calling your server. Opens in its
own browser window via the pop-out icon (open-in-new) in the Agent Monitor
header under **MCP → Activity**, so you can keep an eye on Connections and
the Activity Log while you work elsewhere. The
Assistant is the in-product counterpart — same tools, different surface.
* **Catalog tab** — anything the Assistant creates (taps and pipelines) lands
here, grouped by catalog. The "→ Open tap" / "→ Open pipeline" links at the
end of each conversation route you there.
# Audit log for AI agent data access
Source: https://docs.datris.ai/audit-log
With the audit log on, a durable record of creates, changes, runs, and deletes: humans by login, agents by API key, the Assistant on the user's behalf. Best effort by default, strict mode on request. Filterable in the UI, exportable to CSV, mirrored to your SIEM.
The audit log answers questions like *who deleted that pipeline last Tuesday*, *which key rotated that secret*, *did anyone log in from a new address overnight*, and *what did the `claude-desktop` agent do against pipeline X*.
It is separate from the [Agent Monitor](/monitoring#agent-monitor), which is a live, in-memory view of the last 200 MCP tool calls. The audit log is durable, admin-only, and queryable. With the audit log on it covers every platform write (not just MCP). By default delivery is best effort: if the config store cannot keep up, entries can be dropped and are counted. [Strict mode](#strict-mode) refuses a request instead of losing its entry.
## Enabling it
Off by default — existing installs are unchanged until you opt in.
1. Add the flag to your `.env`:
```bash theme={null}
USE_AUDIT_LOG=true
# optional — days to keep entries (0 = keep forever)
AUDIT_LOG_RETENTION_DAYS=90
# optional — also record reads (queries, searches, listings)
AUDIT_LOG_READS=false
# optional — refuse audited requests rather than drop entries (see Strict mode)
AUDIT_LOG_STRICT=false
```
2. Recreate the Datris container:
```bash theme={null}
docker compose up -d --force-recreate datris
```
3. Open **Configuration → Audit Log**. With user authentication on, the sub-tab is visible to admins only.
A restart is required to turn the flag on or off — it is read once at startup, like the other auth flags.
## Who is recorded
Every entry names an actor. The type tells you which kind of identity it was:
| Actor type | How it is identified | Example |
| - | - | - |
| `user` | The logged-in session (requires [User Authentication](/user-auth)) | `tfearn`, role `admin` |
| `api-key` | The API key's **label** plus a stable **key id** | `claude-desktop`, `k_7f3a9c1b2d4e` |
| `assistant` | The in-platform Assistant (or Ops / Catalog chat, or the Configuration assistant) acting **on behalf of** the logged-in user | `tfearn via Assistant` |
| `tap` | A tap script reading platform data during a run, via its per-run token | `crypto-prices` |
| `system` | No request behind it: scheduled runs, Kafka / object-store triggers, server start | — |
Agents are identified by their API key, humans by their session. The key's **label** is the identity you already see in the API-Keys tab, version history, and the Agent Monitor. The key's **value** is a secret and is never written anywhere — not hashed, not truncated.
The **key id** exists because a label can be revoked and re-issued. Rotating a key keeps the identity (same label, new secret). Revoking `claude-desktop` and issuing a new `claude-desktop` next week is a *different* key wearing the same name; the id tells them apart. Keys issued before this feature have no id and are attributed by label only.
Actions the Assistant takes for you show up as `assistant` with your username — not as the platform's internal `ui` key. The same attribution applies to pipeline and tap version history, so "who changed this?" has the same answer in both places.
In the legacy no-auth mode (`USE_USER_AUTH=false`, `USE_API_KEYS=false`) there is no identity to record; entries are attributed to `anonymous`.
## What is recorded
With the audit log on: writes, runs, deletes, logins, key and user management, and every denied request (best effort by default; see [Strict mode](#strict-mode) for the complete guarantee). Reads are not recorded by default — they are high-volume and rarely what an audit is about — with one exception: reading a **secret** is always logged.
| Category | Actions |
| - | - |
| `pipeline` | create, update, delete, version restore, destination type changes; `purge-source` (system event) when a [protected](/transformation/field-protection) run deletes its raw ingest object(s), with outcome `warning` if a delete failed |
| `tap` | create, update, delete, run (manual and scheduled), script and state changes |
| `secret` | read, write, delete |
| `config` | AI provider, embedding, and platform configuration changes |
| `job` | kill |
| `document` | upload (bursts are folded into one row with a count) |
| `code-repo` | repository connection changes |
| `auth` | login (success and failure), logout, password change |
| `user` | create (including the first-boot `admin` seed, with reason `bootstrap`), update (role change), reset-password (an admin cleared the password), rotate-password (startup rotation of an account with no password, with reason `no-password-hash`; recorded just before that boot's `system` / `start` entry), delete |
| `key` | issue, revoke, rotate; `issue` with resource `field-protection` (system event) when the server issues the environment's [field protection](/transformation/field-protection) hmac or encryption key on the first protected run; `rotate` with resource `field-protection` when an operator rotates the field protection encryption key or adds or changes an `enc.v` through the secrets API; `retire` and `set-current` with resource `field-protection` when a `protect:admin` key removes an `enc.v` or moves `encCurrent` through the secrets API (resource name lists the changed fields, never values) |
| `protect` | reveal: an [`encrypt`-protected value](/transformation/field-protection#revealing-a-value) read back, with the pipeline, the field name, and the counts requested, revealed, and failed (never a value); outcome `warning` when any value failed |
| `security` | any denied request — insufficient role, missing capability, expired session, and a non-`ui` key attempting to act on behalf of a user |
| `system` | server start and stop |
| `audit` | CSV export of the audit log itself |
| `mcp` | Clearing the Agent Monitor activity log |
Each entry also carries the outcome (`success`, `failure`, `denied`, or `warning` for an action that succeeded but needs follow-up, such as a pipeline delete that leaves a Unity Catalog table behind), the HTTP status, how long the request took, the caller's IP address (first hop of `X-Forwarded-For` when the UI sits behind a proxy) and user agent, and the name of the resource touched.
To record reads as well — every query, search, and listing an agent runs — set `AUDIT_LOG_READS=true` in `.env` and recreate the container. Expect a lot more rows: each query is its own entry, so filter by category when you're looking for changes rather than activity.
### What is never recorded
Passwords, password hashes, API-key values, session cookies, secret values, and credentials embedded in connection strings. Request bodies are not stored. Where an entry does carry extra detail (a role change records `from` and `to`, for example), every field whose name looks like a credential is masked before it is written. With user authentication on, entries made through the [Configuration assistant](/configuration-assistant) carry `via: config-chat` in their detail and are attributed to the signed-in admin.
## Reading it
**Configuration → Audit Log** shows the newest entries first with filters for time range, category, action, actor type, actor, outcome, and resource name. Click a row for the full entry. **Export CSV** downloads the current filter (up to 50,000 rows); the export itself is recorded as an `audit / export` event.
The same data is available programmatically:
```bash theme={null}
curl "http://localhost:8080/api/v1/audit-log?category=pipeline&action=delete&since=2026-08-01T00:00:00Z" \
-H "x-api-key: $DATRIS_API_KEY"
```
Parameters: `since`, `until` (ISO-8601 or epoch millis), `category`, `action`, `actor` (label), `actorType`, `outcome`, `resource` (substring match), `limit` (default 100, max 1000), and `cursor` from the previous page's `nextCursor`. `/api/v1/audit-log/facets` lists the values seen in the last 30 days; `/api/v1/audit-log/export` returns CSV with the same filters.
Reading requires the `admin` role for a user session, or the `audit:read` capability for an API key. Full-access keys hold it implicitly; a scoped key must be issued with it. Entries cannot be edited or deleted through the API.
## Retention and long-term storage
Entries live in the platform's MongoDB config store (collection `{environment}-audit-log`) and expire after `AUDIT_LOG_RETENTION_DAYS` (default 90). Set it to `0` to keep them until you remove them yourself.
Every entry is **also written to the server log** on the logger `ai.datris.audit`, one line per event. Under the `production` Spring profile that line is a JSON object, so any log aggregator or SIEM already reading the container's output gets the full audit trail with no extra integration. That stream is the long-term record; Mongo is the queryable window. Turn the mirror off with `auditLog.emitLogLine: false` if you only want the database copy.
Two Prometheus metrics track health on the existing `/actuator/prometheus` endpoint: `datris_audit_events_total{category,outcome}` and `datris_audit_dropped_total`. By default (best effort) entries are written asynchronously and never slow a request; if the config store cannot keep up, the oldest pending entries are dropped and counted in `datris_audit_dropped_total`, and the Audit Log tab shows a warning. If you need no entry to be lost, turn on [strict mode](#strict-mode).
## Strict mode
By default the audit log favours availability: a request is never blocked, and an entry that cannot be written is dropped and counted. If you need the opposite trade, a request that cannot be recorded does not happen, set:
```bash theme={null}
USE_AUDIT_LOG=true
AUDIT_LOG_STRICT=true
```
and recreate the Datris container. `AUDIT_LOG_STRICT` does nothing while `USE_AUDIT_LOG` is off (`datris doctor` warns about that combination under `audit.strict`).
With strict mode on:
* **Nothing is evicted.** A full queue never pushes an older entry out, and `datris_audit_dropped_total` stays at 0.
* **A failed write is retried, not skipped.** While the config store (MongoDB) is down, the writer retries the same entry with backoff and keeps the rest of the queue in order. When the store comes back, every queued entry is written once, in the order it was accepted.
* **Audited requests are refused while the queue is backed up.** Once the pending queue reaches its high-water mark (9,000 of 10,000 entries), every request that would be audited (writes; reads too with `AUDIT_LOG_READS=true`; secret reads always) gets HTTP **503** with a `Retry-After` header and a JSON message naming `AUDIT_LOG_STRICT`, before the request is handled. Tap callbacks and HTTP ingest triggers fail with the same 503. Triggers that do not come over the API wait instead: the MinIO bucket webhook answers 503 and MinIO redelivers the event later (this relies on MinIO's webhook event queue: the bundled `minio-init` sets `queue_dir` with a limit of 1,000 undelivered events, past which MinIO drops events, and a MinIO you configured yourself without `queue_dir` does not redeliver at all), the Kafka consumer stops reading (no offset is committed past an unprocessed message), and scheduled tap runs are skipped until the queue drains, then fire on the next tick. Unaudited reads keep working, and `GET /api/v1/audit-log/status` is never refused, so the UI banner and scripts can see the state (that status read is not recorded while the gate is closed). Requests are accepted again as soon as the queue drains.
* **An entry that still cannot be queued is counted.** A request refused by authentication or authorization while the gate is closed is not held: its denial entry is counted straight away. If a request already past the gate waits more than a few seconds for room, its entry is counted in `datris_audit_unrecorded_total` and written whole at ERROR level on the `ai.datris.audit` logger, so the log stream still has it.
`GET /api/v1/audit-log/status` reports `strict`, `unrecorded`, and `acceptingWrites` alongside `dropped` and `queueDepth`.
**Residual risk.** Accepted entries wait in an in-memory queue until the writer stores them. A hard kill of the server (out-of-memory kill, `docker kill`, host crash) loses entries that were accepted but not yet written; the `ai.datris.audit` log line is emitted when an entry is written, not when it is accepted. A normal stop (`docker stop`, `docker compose stop` or `down`) flushes the queue first. The flush is bounded to 10 seconds, even when the config store hangs rather than fails: entries it cannot store in that time are counted in `datris_audit_unrecorded_total` (strict mode), and, under strict mode or whenever an entry was left behind, the total is printed to the container's stderr as `audit-unrecorded total=N at shutdown`, so it survives the server's logging shutdown. The Kafka consumer stops before the flush, and an entry submitted after the flush (for example by a tap run still finishing) is refused and counted the same way. Compose gives the server 30 seconds to stop before killing it.
## Cross-referencing the Agent Monitor
Audit entries produced by an MCP agent carry the same key label the Agent Monitor displays, so you can match a tool call in the monitor to the platform-side record of what it did. The audit log is the durable record that something happened; the monitor is the ephemeral detail of the tool call, kept only while the MCP server is running.
# Changelog
Source: https://docs.datris.ai/changelog
Release history for Datris
**Busy hosts stay up, and Claude Opus 5.5 becomes the default chat model.**
* **Busy hosts stay up.** Tap scripts and AI rule scripts now run a bounded number at a time, inside CPU, memory and process limits. When the runner is full, a run waits its turn instead of piling on; its run history shows how long it queued, and `datris doctor` warns when runs had to wait or were turned away. All limits are adjustable in `.env`.
* **Claude Opus 5.5 is the default chat model for Anthropic.** New installs on the Anthropic and Bedrock paths seed Claude Opus 5.5 for the Assistant and other chat features (it was already the code-generation default), and the model picker recommends it. Existing installs keep the model they have; change it any time on Configuration → AI Providers.
**Upgrading:** `docker compose pull && docker compose up -d`; the server, UI, MCP server and script runner images changed. The new compose file gives both script runners CPU, memory and process ceilings and each script a 1 GB memory limit (ceilings, not reservations). Raise them in `.env` for scripts that need more; see [Tap Execution & Isolation](/tap-execution-isolation#configuration). No other configuration changes are required.
**Bring a restored catalog back to life in one click, and let assistants read whole scripts.**
* **Enable a restored catalog in the right order.** Taps that read tables produced by other taps in the same catalog are recognised from their scripts (or declared by you), and each catalog can show the order to enable its taps in, with what still blocks each one. Disabled taps on the Catalog tab show their level or the sources they need, **Run now** lands a tested tap's data without enabling it, and the restore result says how many restored taps depend on others. Agents get the same enable order through a new read-only MCP tool.
* **Enable a whole catalog in order with one click.** A new **Enable in order** action on a catalog's card tests, enables and runs each tap in its enable order, waiting for each tap's data to land before starting the taps that read it. Taps whose sources failed are skipped with the reason, progress shows on the card, and the job can be cancelled. Agents can start the same job, and an approval-required agent policy asks for one approval for the whole catalog.
* **Updated object store image.** The bundled MinIO image moves to a newer build with current security patches. Run `docker compose pull && docker compose up -d --force-recreate minio minio-init`; buckets and data are kept.
* **In-app assistants read whole tap scripts.** In-app assistants can now read whole tap scripts and larger tool results, so they can review and fix a long script without asking you to paste it. A tap version's script now also shows for taps stored in a code repository.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate minio minio-init`; the server, UI, MCP server and object store images changed. No configuration changes are required; the new endpoints, tools and settings are additive.
**Back up a catalog as one file and restore it anywhere.**
* **Download a catalog.** The download icon on a catalog's card in the Catalog tab saves the whole catalog as a zip archive: every tap and pipeline definition, each tap's script and incremental state, and the validation schemas its pipelines reference. Secret values, destination data, run history and version history are never included.
* **Import a catalog.** The **Import catalog** button restores an archive on the same install or another one, under the original name or a new one. A restore never merges into an existing catalog: clashes are refused and listed with nothing changed. Choose **Overwrite catalog** and type its name to replace an existing catalog's contents, or skip the incremental tap state to make a clone that starts from scratch.
* **Secrets stay yours.** The archive lists the secret names the catalog needs, and the restore result names any missing on the target. Inline REST bearer tokens and API keys on a pipeline are removed on export; re-enter them after the restore.
* **Restored taps start disabled.** Taps come back with a **Disabled** badge in place of their schedule; click it to enable the tap or to test it first.
* **CLI and agents.** `datris catalog export` and `datris catalog restore`, plus the `export_catalog` and `restore_catalog` MCP tools, with the same refuse-on-clash and confirm-to-overwrite rules.
**Upgrading:** `docker compose pull && docker compose up -d`; the server, UI and MCP server images changed. No configuration changes are required; the new endpoints and tools are additive.
**See which AI model is answering.**
* **The Assistant names its model.** The top of the Assistant conversation shows the chat provider and model in use (for example "Anthropic · claude-fable-5-1 · extended thinking on"), and the line stays visible after the first message. Administrators get a link to change it on Configuration → AI Providers; the line updates as soon as the configuration is saved.
* **Generated tap scripts say who wrote them.** When the Assistant or an agent generates a tap script, the result reports which CodeGen provider and model produced it. The tap generation status carries the provider and model from the first poll.
* **Consistent provider names.** The Assistant, tap generation and the Configuration tab use the same display names for AI providers.
**Upgrading:** `docker compose pull && docker compose up -d`; the server, UI and MCP server images changed. No configuration changes are required; the additions are new fields only.
**Server framework update.**
* **Current, supported server framework.** The server now runs on Spring Boot 4, Spring Framework 7 and Tomcat 11, a line that still receives security fixes. This closes two open vulnerability reports against the web framework; neither affected path is used by Datris. The API, responses, sign-in sessions, Assistant streaming, metrics and audit log behave exactly as before.
**Upgrading:** `docker compose pull && docker compose up -d`; only the server image changed. No configuration changes are required.
**Single sign-on through your identity provider, and security updates.**
* **Sign in with SSO.** With user authentication on, people can sign in through your identity provider (Okta, Microsoft Entra ID, Google Workspace, Keycloak, Auth0, or any OpenID Connect provider) from a new **Sign in with SSO** button on the login screen. OpenID Connect authorization code flow with PKCE, so MFA, password rules and lockout come from your provider. SSO users are ordinary Datris users with one of the three roles; sessions, role checks, the Users screen and the audit log work as for password users. API keys, the CLI and MCP clients are unchanged. See [Single sign-on (OIDC)](/user-auth#single-sign-on-oidc).
* **You decide who gets in.** Users already on **Configuration → Users** sign in with their role; anyone else is refused unless `OIDC_DEFAULT_ROLE` is `viewer` or `editor`. The built-in `admin` account is never reachable through SSO. The first sign-in binds the user to the provider's subject id; a different identity with the same username is refused.
* **Never in the way of password login.** A wrong or incomplete setting switches SSO off with one ERROR line and the server starts as usual; an unreachable provider fails the SSO button with a clear message while password login keeps working, and SSO recovers on its own.
* **Audited.** Every SSO sign-in and refusal is an `auth` / `login` entry with `method: oidc`.
* **Security updates.** Refreshed server and UI build dependencies to clear the open vulnerability reports. No behaviour change.
* **MinIO health check** no longer depends on a config file inside the container.
**Upgrading:** refresh the compose file (`git pull` or re-download `docker-compose.standalone.yml`), then `docker compose pull && docker compose up -d`; the server and UI images changed. SSO is off by default. To turn it on, set `USE_USER_AUTH=true`, `OIDC_ENABLED=true`, `OIDC_ISSUER`, `OIDC_CLIENT_ID` and `OIDC_REDIRECT_URI`, put the client secret in Vault under `oss/oidc` (key `clientSecret`), and recreate the server. A secret named `oidc` is now platform-managed (taps and pipelines cannot reference it). The version endpoint gains `oidcEnabled`. SSO is single-tenant; no SAML or provider group-to-role mapping yet.
**Strict evidence mode, a complete record of password changes, a data-flow matrix, and graceful shutdown.**
* **Strict evidence mode.** `PROVENANCE_STRICT=true` makes a stamping pipeline fail its run instead of loading rows it could not stamp (XML sources are exempt and say so). `AUDIT_LOG_STRICT=true` refuses an audited request with a clear 503 while the audit log cannot record it, instead of dropping an older entry; bucket drops, Kafka messages and scheduled tap runs wait the same way, and everything replays in order when the store returns. Both default to off. `datris doctor` reports the mode; the audit-log status endpoint shows `acceptingWrites`. See [Audit Log](/audit-log#strict-mode) and [Provenance](/provenance#strict-mode).
* **Password resets and rotations are audited.** `reset-password` names the account and the acting session or key; role changes carry the new and previous role; the startup bootstrap rotation is `rotate-password` with the `system` actor (the password never enters the audit log); the first-boot `admin` seed is `create` with reason `bootstrap`. See [Recovering admin access](/user-auth#recovering-admin-access).
* **Data Flows.** A new [page](/production/data-flows) lists, for each kind of data, every place it can go and the switch that changes it, with a no-cloud-exit recipe. Evidence claims across the docs now say when they hold.
* **Graceful shutdown.** `docker stop` now reaches the server: requests drain, the Kafka consumer closes cleanly, queued audit entries are written, exit code 143. Previously every stop was a hard kill after ten seconds.
**Upgrading:** `docker compose pull && docker compose up -d`; only the server image changed. Refresh the compose file for the new 30 s stop grace period. Stops are now graceful (exit 143, usually 1-3 s; up to \~12 s with the audit log on and the store down). Strict mode is opt-in; with both switches off, behaviour is unchanged. Audit action names changed: password reset is `reset-password` (was `update`), first-boot seed is `create` + reason `bootstrap` (was `seed-admin`); existing rows unchanged. New: status fields `strict`/`unrecorded`/`acceptingWrites`, doctor checks `audit.strict`/`provenance.strict`, metric `datris_audit_unrecorded_total`.
**AI rule and transformation scripts are generated once, stored with your pipeline, and run in an isolated container.**
* **One script per instruction, written when you save.** Saving a pipeline with an AI data-quality rule or an AI transformation generates its Python script then and stores it. Every run executes that stored script: an unchanged instruction behaves the same on every run, runs make no model call, and a model outage no longer stops ingestion. A later save regenerates only when the instruction or the source schema changed; pipelines created before the upgrade generate and store on their first run. See [AI rules](/data-quality/ai-rules#when-the-script-is-generated) and [AI transformation](/transformation/ai-transformation#when-the-script-is-generated).
* **Read, review and replace the script.** The save response, a new API endpoint and the Lineage panel show each script, when it was generated and by which model. Regenerate on demand; a failing script is never silently replaced, and the run error says when it was generated and how to replace it.
* **Scripts in your code repository.** With a code repository configured, pipeline scripts are committed under `pipelines//` next to your taps, and runs execute the recorded commit. Hand edits are adopted deliberately with one call; a save that would overwrite one is refused with a warning, and runs keep the recorded commit until you resolve it. See [Pipeline scripts](/tap-github-storage#pipeline-scripts).
* **Generated scripts run outside the server.** On compose, they execute in a new hardened container, `datris-codegen-runner`: no platform secrets, no server filesystem, no route to the database, object store, secrets store, internet or the Datris API; a dedicated scratch volume emptied after every run. `datris doctor` reports the mode. See [Execution isolation](/tap-execution-isolation#generated-data-quality-and-transformation-scripts).
* **Blank AI instructions are ignored** instead of calling the model.
* **Doctor covers the new container** in the environment-drift check and a new `codegen.isolation` row.
**Upgrading:** refresh `docker-compose.yml` (`git pull` or re-download `docker-compose.standalone.yml`), then `docker compose pull && docker compose up -d`. All four images changed; one new container, one new internal network (`codegen-net`) and one new named volume (`codegen-scratch`) appear; no `.env` change. Pulling images without refreshing the compose file keeps scripts in-process with a startup and doctor warning; refreshing the compose file without pulling fails loudly on every AI rule and transformation, never silently in-process. Scripts are now written from column names and types, not sample rows. A failing script stays until you change the instruction or regenerate. Scripts that reached the network or server files now fail; `USE_CODEGEN_RUNNER=false` restores in-process execution. Keep `codegen-net` `internal: true`.
**`datris doctor` tells you when a newer version is available.**
* **Know when you are behind.** A new `version.update` row reads "up to date" or names the newer version and the upgrade command. To find out, doctor tells datris.ai your Datris version, operating system and CPU architecture, and nothing else, each time you run it. Never from the server, on a timer, or during `--pre-upgrade`; nothing is stored on your machine. See [Doctor](/doctor#update-check).
* **Off with one line:** `DATRIS_UPDATE_CHECK=0` in `.env` (run doctor from the install directory or pass `--project-dir`, or set it in the environment). A fresh install prints a short notice, and the installer writes the line when given `DATRIS_UPDATE_CHECK=0`.
* **Never in the way.** If datris.ai cannot be reached within three seconds the row is skipped and doctor's result is unaffected.
* **How to verify:** [Security Architecture](/production/security-architecture#no-telemetry) lists both outbound requests Datris can make and commands to confirm there are no others.
* **Security updates.** Refreshed the server, UI and tap-runner dependencies and the tap-runner base image to clear the open vulnerability reports. No behaviour change.
**Upgrading:** run `docker compose pull && docker compose up -d --force-recreate` (all four images changed). The update check is on after you upgrade the CLI (`pip install -U datris-mcp-server` / `brew upgrade datris`), so each `datris doctor` run now makes one request to datris.ai; add `DATRIS_UPDATE_CHECK=0` to `.env` to keep it off. On a deployment behind the latest release, `datris doctor` now exits 1 where it exited 0. `datris doctor --pre-upgrade` is unchanged.
**One switch to turn on the governance controls, a doctor check that reports when they are off, and corrected first-login instructions.**
* **Switch on the governance controls in one step.** A fresh install takes `DATRIS_GOVERNED=1` to turn on user login, API keys, the audit log and the agent policy; an interactive install asks once, defaulting to no. An existing install adds four documented lines to its `.env`. Nothing changes for installs that do not ask for it. See [Quick Start](/quick-start#2-switch-on-the-governance-controls).
* **`datris doctor` reports the governance controls.** A new `governance.controls` row names any control that is off, with the lines to add. A warning, never an error. See [Doctor](/doctor).
* **First-login instructions corrected.** `admin` is created with a bootstrap password printed once to the server log, not a blank password. The docs and login screen say where to read it and to save it before recreating the container. A new [Recovering admin access](/user-auth#recovering-admin-access) section covers the case where it is gone.
* **Fixed: a headless install on Debian or Ubuntu stopped silently** right after fetching the runtime files. It now continues.
* **Governed installs skip a check that needs a key.** The installer no longer waits on the post-boot store check when API keys are enforced; it prints the command to run once a key exists.
* **Docs and website wording** now say the controls are switched on for production rather than "on by default".
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate`. The server, UI and MCP server images changed. No defaults change. `datris doctor` now exits 1 on an install with any of the four controls off, where it exited 0. Before turning the controls on, save the `admin` bootstrap password from the server log (`docker compose logs datris | grep "Bootstrap login"`); if nothing is printed, see [Recovering admin access](/user-auth#recovering-admin-access).
**A HIPAA Safe Harbor preset, a switch that keeps row values away from AI helpers, clearer errors, and an installer agents can run.**
* **HIPAA Safe Harbor preset for field protection.** Datris recognises the Safe Harbor identifier classes from field names with a fixed table and no AI call, proposes a method for each, and lists what needs a person's review. When enforced, a pipeline cannot be saved with an unprotected identifier and new columns are protected before they land. Available in the wizard, on the pipeline page and to agents. An aid, not a certification. See [Field Protection](/transformation/field-protection).
* **One switch so helpers send no row values to a model.** With `DATRIS_AI_SAMPLE_VALUES=false`, schema generation, profiling, Assistant attachments and AI rule and transformation generation work from structure only. The default is unchanged, and `datris doctor` reports the setting.
* **A new mask option** keeps the first three characters.
* **Schema generation and profiling no longer fail when the model declines.** You get a usable result with a note.
* **An invalid pipeline is reported as an invalid request**, with the same message, shown as plain text.
* **Failed runs show the error, not a Java stack trace.** The trace is behind a toggle in the Ops activity view and in the server log.
* **Fixed: a column the destination already declares is no longer added twice** when it later arrives as a new source column.
* **Fixed: editing a pipeline in the wizard keeps its protection settings.**
* **Install Datris from an agent.** See [Install for Agents](/install-for-agents). The installer also stops clearly when no AI provider is given and cleans up after a failed first run.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate`. The server, UI and MCP server images changed. A refused pipeline save now returns HTTP 400 where it returned 500, with the same message. A failed run's stack trace moved from the status description to a separate detail field. To use `DATRIS_AI_SAMPLE_VALUES` on an existing install, add it to your `.env`.
**Field protection for PII and PHI, reveal for authorized users, and tighter rules for secrets.**
* **Protect sensitive columns before they land anywhere.** Mark a field and its original value never reaches a destination, a preview, or an AI step. Choose a stable one-way pseudonym, a mask that keeps the part you pick, a fixed redaction, dropping the column, or encryption. Protection runs on your server before data quality and transformation, and the raw copies Datris held for the run are removed afterwards. See [Field Protection](/transformation/field-protection).
* **Encrypt what an authorized person may need to see again.** Encrypted fields are stored as ciphertext. Someone with the reveal permission can get the original back; every reveal is audited and no user ever holds a key. The permission is in no built-in role or key template. Keys can be rotated without losing older rows.
* **Reveal from the Search tab.** Encrypted values show as a lock in results for Databricks, Snowflake, object store, PostgreSQL and MongoDB, and an admin can reveal the current result with one click. Nothing is kept afterwards. Assistants and agents cannot reveal.
* **Suggestions for what to protect.** Datris proposes a method per field from its name and type alone. No data is sent and nothing is applied until you accept it.
* **Protection in the wizard and for agents.** The schema step has a Protect column with an explanation of each option. Agents can set protection and ask for suggestions, and confirm the fields with you first. Lineage shows protected and dropped columns.
* **Security: stronger protection for stored secrets.** Taps can now only use tap secrets, the keys Datris manages for itself cannot be used by a tap or pipeline, and secret names are checked more strictly. We recommend upgrading.
* **Databricks loads are no longer reported as failed when they succeeded.**
* **MongoDB results read as a table in Search** when the documents are flat.
* **`datris doctor` checks tap secrets.**
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate`. The server, UI and MCP server images changed. A tap whose secret is not a tap secret now fails its next run and cannot be saved; create a tap secret for it, or set `DATRIS_TAP_SECRET_SCOPE=any` to keep the previous behaviour. Secret names with spaces or URL-special characters are refused. Protected pipelines delete their source file after a run unless they opt out. See [Taps](/taps#taps-can-only-use-tap-secrets).
**Security dependency updates and a skill that teaches your coding agents to use Datris.**
* **Security: updated third-party libraries.** Dependency updates close several published vulnerabilities, including high-severity ones, in libraries the server and the UI build use. No Datris behaviour changes. Shared installs should upgrade soon.
* **A skill for your project's coding agents.** The new `datris-platform` skill tells Claude Code, Codex, Hermes, OpenClaw and similar agents to use Datris for data work in your own projects, with MCP tools first and the REST API only for code written into the project. See [Agent Skill](/agent-skill) for install commands.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate`. The server, UI and MCP server images changed. No configuration changes are required.
**Iceberg tables that work on Databricks, Unity Catalog metadata on by default, and a security fix for API keys.**
* **Iceberg pipelines can now target Databricks Unity Catalog.** A new managed mode lets the catalog choose where the table lives while Datris writes to it through the catalog, so Iceberg output on Databricks is governed, queryable and kept current after every run. The pipeline status shows the table and its location. Deleting the pipeline leaves the catalog's data in place.
* **Unity Catalog metadata is on by default for Databricks pipelines.** Descriptions, tags, properties and lineage are written without a per-pipeline opt-in. A pipeline can still turn it off explicitly, and a single environment setting restores the previous opt-in behaviour. The status endpoint tells you where the setting came from.
* **Clearer guidance when a catalog cannot do what you asked.** Deleting a catalog reports any Unity Catalog cleanup it could not complete, and saving a pipeline that points an Iceberg register mode at Databricks warns right away and names the mode that works.
* **Safer Iceberg history for adopted tables.** Tables Datris adopts from an existing catalog keep a longer metadata history and record where they came from.
* **Security: revoked API keys are now rejected immediately.** Revoke a key and every request made with it is refused from that moment, and the refusal is recorded in the audit log under the key's name. We recommend upgrading.
* **API errors are returned as JSON.** Server errors no longer come back as raw text; clients, the UI and the CLI get a short, readable message. Agents calling tools over MCP see failures flagged as errors instead of ordinary results, and the CLI explains when its key was rejected.
**Upgrading**
Run `docker compose pull && docker compose up -d --force-recreate`. The server, UI and MCP server images changed. Databricks pipelines that never configured Unity Catalog now get metadata written after each load; set `DATRIS_UNITY_CATALOG_DEFAULT=disabled` to keep the old opt-in behaviour, or add `unityCatalog: {enabled: false}` to an individual pipeline. See [Unity Catalog](/destinations/unity-catalog).
**Unity Catalog integration for Databricks, warehouse queries in the Search tab, and less to configure for Databricks pipelines.**
* **Datris annotates your Databricks tables in Unity Catalog.** Turn on Unity Catalog for a pipeline and, after every successful load, its table gets a description, stable tags (which pipeline loaded it, its data-quality status, managed by Datris) and run details in the table properties, all visible in Catalog Explorer. Nothing is written unless a pipeline opts in, and a metadata problem is reported as a warning while the load still succeeds.
* **Lineage appears in Unity Catalog too.** The same opt-in publishes source → pipeline → table lineage, with column mappings, to the table's Lineage tab.
* **Agents can look around Unity Catalog before building a pipeline.** A new tool lists the catalogs, schemas, tables and columns a connection can see, and data discovery can include warehouse tables in its results, marking the ones Datris loaded.
* **Iceberg tables can be registered with, or committed through, an Iceberg REST catalog.** Object-store pipelines writing Iceberg can keep an external catalog current. Databricks Unity Catalog does not yet accept tables at a location you choose; Datris detects this, warns, and keeps writing safely. Use the Databricks destination for governed Databricks tables.
* **Query Databricks and Snowflake from the Search tab.** Pick a pipeline, run read-only SQL and see the results in place. Error messages across every query type are now short and specific.
* **Less to configure for Databricks.** A pipeline no longer needs the SQL warehouse in its configuration when the connection secret provides it, and assistants stop asking for it.
* **Deleting a pipeline tells you when a catalog still references its table.** The CLI shows the same notice. Its keep-data option, which never kept data, now refuses instead of deleting silently.
* **Editors and viewers can run read-only queries** against object-store, Snowflake and Databricks destinations.
* **Fix: uploads to pipelines that leave out the CSV delimiter no longer fail.**
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. All four images changed. No configuration changes are required; Unity Catalog features stay off until a pipeline opts in. To write tags and lineage, the Databricks service principal needs two additional grants. See [Unity Catalog](/destinations/unity-catalog).
**Security maintenance release.**
* **Security update.** The JSON processing library used by the server is updated to close three recently disclosed advisories, one rated high. No functional changes.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. Only the server image changed. No configuration changes required.
**Catalogs can now be renamed, and deleting one asks whether to keep or delete its items.**
* **Rename a catalog in place.** A named catalog can be renamed from the Catalog tab; every tap and pipeline in it moves to the new name in one step. If API keys were limited to the old name, you are told which ones to update.
* **Deleting a catalog keeps its items by default.** You choose between keeping the items (they move to Uncataloged) and deleting them together with their data, which requires typing the catalog name. Items that could not be changed are listed with the reason.
* **Catalog names may now use capital letters, when created or renamed.**
* **Renaming to an existing name is refused.** Renaming a catalog to a name that already exists is refused; use Move all contents to combine catalogs.
* **No near-duplicate names.** Creating a catalog whose name differs from an existing one only by capitalisation is refused.
* **Uncataloged is protected.** It can no longer be renamed or deleted as a group; its items are deleted one at a time.
* **The assistant can rename catalogs.** Ask the Catalog assistant, or any connected agent, to rename or remove a catalog and it does so in one step. Agents can only remove a catalog while keeping its items. See [Data Catalog](/data-catalog).
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. The server and UI images must be updated together: an older server does not support catalog rename or delete from the new UI.
**Clearer errors for files whose names cannot be parsed.**
* **The real ingest error is reported.** When an uploaded file's name cannot be matched to a pipeline, the log now says so instead of reporting an unrelated internal error. Files that fail later in ingestion still record their status exactly as before.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. Only the server image changed. See [Object-store ingestion](/ingestion/object-store).
**Object-store ingestion is more forgiving: a bad file no longer blocks its neighbours, and root-level uploads work.**
* **One bad file no longer stops the other files uploaded at the same time.** When several files land in the raw bucket together and one of them cannot be read, the others still run in the same pass. The failure is logged with its bucket and file name.
* **Uploads dropped in the root of the raw bucket are now ingested.** A file placed directly in the bucket, with no folder prefix, is picked up like any other instead of failing with a naming-convention error.
* **Every file in a multi-file event is ingested.** An event carrying several files for the same bucket now processes each distinct file; previously only one of them was kept.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. Only the server image changed. See [Object-store ingestion](/ingestion/object-store).
**Installs no longer depend on a third-party registry staying public.**
* **MinIO ships from the same place as the rest of the platform.** The object store image is now published alongside the other Datris images, so a fresh install or `docker compose pull` no longer depends on a third-party registry staying public. Existing object-storage data is kept.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. The `minio` and `minio-init` containers are recreated because the image changed; object-storage data is kept.
**Change your platform's settings by asking: every change waits for your Confirm, and secrets never touch the chat.**
* **A Configuration assistant.** The Configuration tab has an Ask panel at its right edge that reads and changes what the sub-tabs cover: AI providers, secrets, data sources, the code repository, users, API keys and agent policy. It proposes one change at a time on a card, and nothing is written until you click Confirm; a confirmation works once and expires after ten minutes. Credentials are entered in a masked form on the card, never in the conversation. A new user's temporary password or a new API key is shown once on the card, with a copy button. The panel is for admins, needs a signed-in session when user auth is on, and is not shown on trial installs. See [Configuration Assistant](/configuration-assistant).
* **Ask the audit log.** Ask the assistant who changed what and when in plain language, and it answers from the audit entries themselves.
* **Doctor from chat.** Ask the assistant to run the health checks, optionally with a test request to each AI provider, and it summarizes what needs attention.
* **Deep links to Code Repository.** A link that opens Configuration on the Code Repository sub-tab now lands there instead of on the default sub-tab.
* **Changes made through the assistant are marked in the audit log.** With user auth on, changes made through the assistant are attributed to the signed-in admin and marked `via: config-chat` in the audit log. See [Audit Log](/audit-log).
* **More chats at once.** Several chats can stream at the same time without one failing on a connection-pool timeout; the limits are configurable, see [Configuration Reference](/configuration-reference).
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update all images together, including the UI. No configuration changes. With API keys and user auth on, the assistant is available in the UI only.
**Installs and upgrades work again after MinIO closed its public image registry.**
* **MinIO images now come from Chainguard.** MinIO withdrew its images from Docker Hub earlier this month and, as of September 24, put its quay.io images behind a login, so every fresh install and every `docker compose pull` failed with "unauthorized: access to the requested resource is not authorized". The Compose files now pull a multi-arch MinIO build from Chainguard's public registry, pinned to a digest. Running deployments were never affected. The installer, the standalone Compose file and a fresh clone all pick this up automatically, and existing object-storage data is kept.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`.
**Agents get live, validated reads from any source, and only land what's worth keeping.**
* **The scratch destination is now called Live Read.** Same behaviour, same configuration; only the name changed in the wizard, the Pipelines list, the run detail page, the docs, the Assistant and the CLI. The first mention on each surface still says which JSON key it maps to, so nothing you have written needs to change. See [Live Read](/destinations/live-read).
* **The Assistant offers Live Read.** When it asks where data should go, it now names Live Read alongside the structured destinations, with a one-line description, and still never picks it unless you do. It leaves Live Read out when you have asked for a schedule.
* **The docs page moved.** Live Read has its own page under Destinations; the old address redirects.
* **The CLI signs in to the MCP server.** On installs with API keys turned on, every CLI command now sends the key you already export for `datris doctor`. A missing key gives a one-line message instead of a connection error, and a rejected key gives a one-line message and a non-zero exit instead of raw output and success. The query-string form still works. See [CLI](/cli).
* **A clean exit on every command.** The CLI no longer prints a stray traceback after a successful command, and commands that make several calls no longer reuse a stale connection.
* **MCP clients see the real version.** The MCP server now reports its own version instead of the library it is built on.
* **Claude Opus 5.5 and Grok 4.7.** Both are in the model catalog. Opus 5.5 is the recommended and first-boot default for code generation on Anthropic and Bedrock; Grok 4.7 is the recommended and first-boot default for Grok. The chat default is unchanged.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update all images together, including the UI and the MCP server. Existing installs keep their models; the new defaults apply only on first boot. If you use API keys and run the CLI, export `DATRIS_API_KEY` once.
**Taps handle sources of tens of millions of rows: faster batches, honest timeouts, and an estimate before anything is built.**
* **A tap can yield batches.** `fetch()` may yield DataFrames or Arrow record batches as well as single records. Batches are written natively, roughly ten times faster than one record at a time, and land exactly what yielding the same rows one by one would land: integers stay integers, nulls stay null, timestamps keep their precision. Frames read with the pyarrow backend are the fastest of all. Scripts that yield or return records are unchanged. See [Taps](/taps).
* **Tests preview, runs run.** A tap test started from the Assistant, an MCP client or the CLI now stops after 20 records, exactly like the UI's Test button, instead of streaming the whole source and timing out. The tools say plainly that a test's record count is not what a run will produce. Pass a limit of 0 to test the whole source.
* **Real runs get their own timeout.** Tests still stop at five minutes; real and scheduled runs now get an hour by default. Both are settable, and a timed-out run says which mode it ran in and which setting to raise. A mistyped value warns at startup and falls back instead of stopping the server.
* **A timed-out run keeps its logs.** The error now ends with how many records were streamed before the kill and the last lines the script printed, and the run log holds the script's full output. A script that yields prints progress every 100,000 records, so a slow-but-healthy run reads as slow, not broken.
* **Agents estimate before they build.** For a large source the Assistant and the MCP tools show rows, bytes per record, disk and minutes, compare them against the budgets actually set on your install rather than the documented defaults, and when one is exceeded stop and offer three choices: raise it, narrow the window, or chunk the range. Column projection is named as the lever that cuts both numbers.
* **Malformed schedules are refused at save.** A 5-field Unix cron is rejected with the 6-field equivalent to use, with weekday numbers translated. Taps already stored with a bad schedule log once instead of every 30 seconds.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update all images together, including the UI and the MCP server. Two new optional settings, `TAP_SCRIPT_TIMEOUT_SECONDS` (300) and `TAP_RUN_TIMEOUT_SECONDS` (3600); see [Configuration](/configuration-reference).
**GPT-6 Astra support, and an Assistant that stays connected through long tool calls.**
* **GPT-6 Astra works everywhere on the OpenAI provider.** Pick or type `gpt-6-astra` for the Assistant or for code generation and it works for chat, tap script generation, AI data-quality rules, AI transformations, schema generation, brainstorming and natural-language queries; previously every non-chat call to it failed. It appears in the model dropdowns; GPT-5.6 Sol remains the recommended OpenAI model. See [AI Configuration](/ai-configuration).
* **OpenAI reasoning models show their reasoning.** Every Assistant turn on an OpenAI reasoning model now asks for a reasoning summary, so a thinking block appears where before there was none. Models or accounts that decline the summary fall back to the previous behaviour.
* **The Assistant no longer stops silently during long tool calls.** A tool call that ran for several minutes could end the conversation mid-turn with no message. The connection now stays alive for as long as the work runs, and if it is ever cut, the chat says so. The same applies to the Ops, Catalog and Search chats.
* **Tap scripts stream their sources instead of downloading them.** The AI helpers, the Assistant and the documentation now state that a tap has only a small amount of scratch space and must read a remote file directly rather than save it first, and that a run's size should be estimated against the per-run disk budget before anything is built. See [Taps](/taps).
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update all images together, including the UI and the MCP server. No configuration changes required.
**Taps stream large sources, and failures say what went wrong.**
* **A tap can yield its records.** `fetch()` may return a generator or any iterator instead of a list; records are written to disk as they are produced, so a tap over millions of rows runs in a few tens of megabytes of memory. Scripts that return a list are unchanged. See [Taps](/taps).
* **A tap killed for memory says so.** The error explains that the script was killed, almost certainly out of memory, and that `fetch()` should yield records instead of building the whole result. The Assistant and MCP guidance carry the same rule, stop to report after two failed tests, and no longer probe the runner environment.
* **Rows returned as lists land in CSV pipelines.** A tap returning each row as a list with a header row now loads into a CSV pipeline with the header applied and names normalised, or fails with a message naming the problem, instead of corrupting the pipeline schema.
* **A bad first line no longer changes the pipeline.** A CSV upload or object-store pickup whose header does not parse fails before schema evolution, leaving the schema and version untouched. Quoted header names resolve to their plain names.
* **A blank schedule is no schedule.** Saving a tap with an empty schedule stores none and the scheduler stays quiet; taps already saved that way are quiet on upgrade.
* **Testing a tap that yields stops at the test limit** instead of draining the source.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update all images together, including the MCP server. No configuration changes required.
**A pipeline run is no longer limited by memory.**
* **Payload size no longer limits a run.** Files uploaded over the API, archives uploaded as a batch, files picked up from the object store and records returned by taps all stream to disk as they arrive instead of being held in the server's memory. A multi-GB upload or tap run now completes on the default 2 GB heap, and memory use no longer grows with the size of the data passing through.
* **The tap output cap becomes a per-run disk budget.** `PIPELINE_MAX_PAYLOAD_MB` (default 4096 MB, `0` = unlimited) is the most one run may stage on disk; a run that exceeds it stops with a message naming the variable and leaves nothing behind. Installs that never set the old `TAP_MAX_OUTPUT_MB` go from a 100 MB in-memory ceiling to 4 GB of disk. The old name is honoured for one more release, with a warning at startup. See [Taps](/taps).
* **Uploads honour the same budget.** An uploaded file, or the combined contents of a batch archive, above the run budget fails with the same message. The upload ceiling stays at 1 GB by default and is now configurable with `DATRIS_MAX_UPLOAD_MB`.
* **Features that must read the whole payload say so.** Deduplication, JavaScript row functions, JSON/XML schema validation, single-call REST preprocessors and destinations, and single-message Kafka, ActiveMQ and scratch destinations keep working exactly as before below `PIPELINE_MATERIALIZE_MAX_MB` (256 MB). Above it the run stops with a message naming the feature and the setting, instead of running out of memory. CodeGen rules and transformations, provenance stamping and every database, warehouse and object-store destination stream and are never subject to it.
* **REST preprocessors and destinations can batch.** A `batchSize` sends the rows in batches of that size, each call carrying its batch number and the batch count; a REST row function in batch mode accepts a batch size as its sixth parameter. Without a batch size, both keep today's single call. See [Preprocessor](/preprocessor).
* **Record counts are right for more shapes of data.** Newline-delimited JSON input reports its record count instead of 1; a quoted value with an embedded newline counts and loads as one row, including through a CodeGen transformation; a quoted value containing the delimiter lands in one column of an object-store destination instead of being split.
* **Bad input fails at the door.** Invalid JSON on a JSON pipeline is rejected when it is received, before a run is created; a file whose header contains none of the pipeline's columns fails with a message naming both sides instead of loading blank rows.
* **Testing a tap returns a sample.** A tap test returns the first 20 records and says whether more were cut, instead of every record.
* **`datris doctor` watches the staging area.** Two new checks report whether the staging area is writable with room for one run, and whether run directories left behind by a crash are accumulating. See [Doctor](/doctor).
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Update the server and tap-runner images together; an older tap-runner still works but keeps its previous in-memory path. The compose file adds one named volume, `datris-staging`, for staged payloads — it is created on first start and needs no action. The bundled compose file now needs Docker Compose v2. No configuration changes are required; if `.env` sets `TAP_MAX_OUTPUT_MB`, rename it to `PIPELINE_MAX_PAYLOAD_MB` before the next release.
**A schedule only ever runs code that has passed a test.**
* **Test before you schedule.** A tap can carry a schedule only once the exact script that will run on it has passed a server-side test run. Saving a schedule on a never-tested or just-edited script is refused with a message naming the tap and the remedy: save unscheduled, test, then add the schedule. Cadence changes, clearing the schedule, and metadata edits on a tested tap need no re-test. See [Taps](/taps).
* **Editing a scheduled tap never repoints the schedule at untested code** — by hand, by an agent over MCP, through the AI generate, fix, review and optimize helpers, or by restoring an earlier version. The scheduled script keeps running as it was until the new one is tested and the schedule re-applied.
* **The wizard handles it for you.** Saving a scheduled tap saves it unscheduled, runs the test, and applies the schedule on success; on failure the tap is kept without its schedule and the wizard says so.
* **Existing scheduled taps are untouched.** They keep running and stay editable; their first schedule or script change is accepted if the tap has ever tested or run successfully.
* **Fix:** the CLI works on Python 3.14 (every command previously failed with an event-loop error).
* **Fix:** upload, generate and profile return a plain error message instead of a stack trace.
* **Fix:** the MCP Registry listing now names the settings the server actually reads; a Glama directory listing was added.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. Agents editing a scheduled tap's script must clear the schedule first and re-test (the tools explain this on refusal). CLI users on pip or Homebrew: upgrade for the Python 3.14 fix.
**Run a pipeline and get the rows back, without landing them.**
* **Scratch destination.** For when you, or an agent, want an answer now rather than a dataset: what a source returns for these ids, whether today's file passes its rules, what a transformation produces. The run goes through the same preprocessing, data quality and transformation chain as any other pipeline; the rows are held briefly so the caller can read them back. Nothing is landed, catalogued or added to lineage, and the result expires after a retention window you control. Structured sources only; cannot be combined with another destination. See [Scratch](/destinations/live-read).
* **Reading the result.** The first rows ride on the run's status. Larger results page from the run detail page's **View result** panel, from the CLI (`datris pipeline result`), and from the new `get_pipeline_result` MCP tool. Paging never re-runs the source.
* **Agents know when to keep and when to scratch.** The MCP server and the Assistant follow a keep-or-scratch rule: data to be kept, observed, queried later or refreshed on a schedule goes to a real destination; a one-off answer goes to scratch. Fetch-only taps target a scratch pipeline; promote it later by changing the destination — the tap, its schedule and its incremental position do not move.
* **Scratch in the UI.** The pipeline wizard offers Scratch under Destination; the Pipelines list badges scratch pipelines.
* **Fix:** editing a secret keeps the values you did not change — saving one field no longer blanks the masked sensitive fields you left alone, in the UI and over the API.
* **Fix:** the destination's authoritative flag set over the REST API was silently ignored; it is now applied and validated.
* **Hardening.** S3 object-store endpoints pass the same network safeguards as other outbound connections; pipelines writing to the built-in object store can be restricted to an allowlist of buckets (off by default, reported by the doctor); query and status error responses no longer include internal diagnostic detail.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. Scratch results are held for 24 hours by default; shorten it on shared hosts. CLI users on pip or Homebrew: upgrade to pick up `datris pipeline result`.
**Pipelines can now land data as Apache Iceberg tables.**
* **Iceberg as an output format.** Set the file format to `iceberg` on an [object-store](/destinations/object-store) or [S3](/destinations/s3) destination and each run commits a real Iceberg table at the pipeline's location: Parquet data files plus the table metadata any Iceberg-aware engine reads. One atomic commit per run; a failed run leaves the previous snapshot intact. No new destination type, no extra service. See [Iceberg](/destinations/iceberg).
* **Upserts by key.** A new `merge` write mode updates rows matching the configured key fields and inserts the rest; re-running the same data leaves the row count unchanged.
* **Partitioning and schema evolution.** Partition by columns; new nullable columns are added automatically, while type changes, dropped columns, and new required columns are refused before anything is written.
* **Readable without Datris.** Standard Iceberg tooling opens the table by path. Time travel, catalog registration, table maintenance, and Iceberg as a tap source come later; tables written today will not need rewriting.
* **Snapshot in query results** for Iceberg tables; the wizard, MCP tools, and Assistant know the new format and modes; lineage labels object-store destinations with their format.
* **Fix:** querying an object-store pipeline that has never run returns an empty result instead of an error (a pipeline whose bucket does not exist also reads as empty).
* **Overlapping runs** writing the same object-store location now run one after the other, for every object-store format.
* **S3 credential changes apply immediately.** Correcting or rotating S3 credentials in a secret now takes effect on the next run without a restart.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required; existing Parquet and ORC pipelines are unaffected. Switching an existing pipeline to Iceberg in place is refused unless `deleteBeforeWrite` is set; use a new prefix to keep the old files. The server image is about 50 MB larger.
**A smaller install, and docs written for the way people search.**
* **Minimal install.** The [installation guide](/installation) now leads with a minimal path that skips the bundled embedding server and bundled PostgreSQL and fits in a 4 GB Docker memory allocation; the full stack needs about 8 GB. Numbers measured on a live stack.
* **OpenAI embeddings by default when you have a key.** Fresh installs with an OpenAI key present use OpenAI embeddings instead of the local embedding server, whichever provider handles chat. Existing installs are unchanged; set the embedding provider explicitly to keep embeddings local.
* **New recipe:** [Give Claude Code a safe way to load data into PostgreSQL](/recipes/claude-code-postgres) — MCP connection, approval gates, a load, and the audit and lineage records it leaves.
* **Docs pages named for the task.** Destination, MCP, policy, audit, lineage, and Airflow pages are titled for what you are trying to do. URLs unchanged.
* **More reliable release builds** for the arm64 UI image.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required.
**The doctor now runs itself.**
* **Upgrades check themselves first.** Re-running the installer on an existing install runs the pre-upgrade self-check before pulling, when the `datris` CLI is on the machine. A warning continues; an error stops the upgrade with the fix on screen (and can be bypassed deliberately). Without the CLI the installer says so and continues as before.
* **Continuous checks.** Set an interval and the server re-runs its self-checks on that cadence, posting to the incident webhook when a check flips to an error or recovers. Off by default; AI probes never run on the timer. See [Doctor](/doctor).
* **The recovery agent can consult the doctor.** When an incident looks like the platform rather than the tap, the agent runs the quick self-check and hands the incident to a person with the doctor's fix.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. **CLI users: upgrade to 1.29 or later** (`pip install -U datris-mcp-server` / `brew upgrade datris`) — the installer's pre-upgrade check needs it and will keep reminding you until you do.
**Installs and upgrades work again.**
* **MinIO images now come from quay.io.** MinIO removed its images from Docker Hub, so every fresh install and every `docker compose pull` failed with "pull access denied for minio/minio". The Compose files now pull the identical images from quay.io; running deployments were never affected. The installer, the standalone file and a fresh clone pick this up automatically.
* **Docs: the production trail.** A new Production section walks a deployment from first install to a governed, audited, agent-operated setup; the Quick Start now ends on an approval and a provenance lookup.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. Compose recreates `minio` on the new image name; the data volume re-attaches. No configuration changes required.
**Datris can now diagnose its own deployment.**
* **Doctor.** A built-in operational self-check finds the problems that used to be discovered only after an outage: a Vault token about to expire or silently capped, an AI provider secret that is missing or incomplete so the server would not come back after an upgrade, an embedding model that was never pulled, a disk about to fill, and a mixed-version stack. Every finding comes with the command that fixes it; nothing is changed automatically.
* **Four places to run it.** The cheap checks run at every server start as `DOCTOR` log lines (a healthy install logs no warnings). The full set runs on demand from Configuration → Doctor, from the new `run_doctor` MCP tool, and from the new `datris doctor` CLI command.
* **Host checks from the CLI.** On the machine running Docker, `datris doctor` also catches data on an anonymous volume, data left behind on a dangling volume, a container still running with an old `.env`, containers from a previous Compose file, a stale from-source build, and an unreachable MCP server. `datris doctor --pre-upgrade` works with the server stopped and is step 0 of the upgrade guide.
* **Optional AI probe.** On request, the doctor sends a minimal request through each configured AI model to confirm the key and model id still work. Never run automatically.
* **Version stamps.** The server reports the exact build it is running and the UI image carries its version, so drift between components is visible.
* **Fresh-install AI defaults refreshed.** New installs seed Claude Fable 5.1 (Anthropic assistant), GPT-5.6 Sol (OpenAI) and Claude Sonnet 5 (overload fallback); code generation stays on Claude Opus 5. Existing installs are unchanged.
* Upgrade: `datris doctor --pre-upgrade`, then `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. CLI users on pip or Homebrew: upgrade to 1.29.0.
**Failed loads now say which destination failed, and why.**
* **Destination failures are named in the job status.** When a run fails while writing to a destination, the job's error identifies that destination and carries its own error message instead of a generic orchestration failure with a stack trace. The full stack trace remains in the run's event stream.
* **Agents and the CLI act on the real cause.** The Assistant, MCP clients and the recovery agent read the same job status field, so they see the failing destination directly. The `datris` CLI prints the failing destination with the error on a failed upload.
* **Multi-destination runs** still report as a single job; when one destination fails and another succeeds, the error names the one that failed.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. CLI users on pip or Homebrew: upgrade to 1.28.2.
**Security and documentation maintenance release.**
* **Security update.** The embedded web server that serves the API is updated to close three recently disclosed critical advisories affecting authentication and authorization handling. The UI and tap-runner images pick up the latest OS package security fixes. No functional changes.
* **Hardening switches now take effect.** `DATRIS_ENV`, `DATRIS_ALLOW_PLAINTEXT_DB`, `DATRIS_ALLOW_PRIVATE_EGRESS` and `TAP_MAX_OUTPUT_MB` were described in `.env.example` but not passed through to the server by the bundled Compose files. They are now honored and documented in the Configuration Reference.
* **Documentation accuracy pass.** Every page on docs.datris.ai was checked against the current release and corrected, including the MCP tool list, CLI flags, Assistant defaults, tap runtime behaviour, ingestion and destination options, the API reference and OpenAPI spec, and the examples.
* **Examples.** Two examples that depended on files no longer in the repository were removed.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. CLI users on pip or Homebrew: upgrade to 1.28.1.
**Lineage now reflects what actually ran, not just what is configured — with an interactive graph and an agent tool to traverse it.**
* **Run-level lineage.** Every pipeline run is recorded as it completes: what it read, which destinations it wrote — each with its own success or failure — how many records, and the definition version it ran under. Automatic for every pipeline, independent of provenance stamping, and never fails or delays a run.
* **History in the graph.** A dataset a pipeline used to land into under an earlier configuration stays in the lineage graph marked historical, with the runs that wrote it.
* **Lineage graph.** Catalog → Lineage opens an interactive Source → Tap → Pipeline → Dataset → Catalog view, filterable by catalog, tag or name. Click any node for what feeds it, what depends on it, freshness and recent runs, with links to the tap or pipeline.
* **Recent runs per node**, plus upstream-only / downstream-only / bounded-depth traversal on the lineage neighborhood endpoint.
* **New MCP tool: `get_lineage`** for impact analysis before changing or deleting something, and for seeing what a pipeline actually wrote lately.
* **Real sources in lineage.** A tap's source node names where the data actually comes from: the tap's new optional `source` field, else the endpoint host, else the host its script calls most. Taps sharing a provider share one source node.
* **Evidence on every lineage hop**: runs, records, failures and last run per edge (last 90 days); claims without runs draw faint; numbers on the panel chips and in `find_data`.
* **Source of authority**: datasets labeled system of record / derived / undeclared, declared never inferred; single destinations authoritative by default; `authoritative: false` for derived copies, `destination.authoritative` for multi-destination pipelines; find\_data prefers the authoritative copy; "Canonical only" graph filter.
* **Column-level lineage** in the graph's side panel and via a new endpoint: exact schema-derived mappings always, system columns for provenance stamps, and an opt-in AI-inferred tier for pipelines with a transformation that returns only evidenced mappings, cached per definition version.
* **Fix:** AI transformations that add, remove or reorder CSV columns now land correctly; the pipeline picks up the transformed column list instead of projecting by the old one.
* **Provenance resolution** for recorded runs now includes per-destination outputs and resolves the definition version from the record when the row's stamp is unavailable.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. Runs are recorded from the first run after upgrading. CLI users on pip or Homebrew: upgrade to 1.28.0.
**Claude Fable 5.1 is now the recommended chat model, plus security and dependency updates.**
* **Claude Fable 5.1 support.** Anthropic's newest model is selectable for both the Assistant and code generation, on the direct Anthropic provider and on Amazon Bedrock. It is the recommended chat model on both; code generation keeps Claude Opus 5. Fresh Bedrock installs default to Fable 5.1; existing installs keep their configured models. On Bedrock it needs the same one-time data-retention opt-in as Fable 5.
* **Security update.** The UI's build tooling picks up fixes for several recently disclosed high-severity advisories in a third-party component. No functional changes.
* **UI framework update** to the latest Angular patch release.
* **CLI and MCP server via pip.** The published package now pins the MCP SDK to the supported major version, so a fresh `pip install datris-mcp-server` no longer pulls an incompatible SDK that fails at startup.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required. CLI users on pip or Homebrew: upgrade to 1.26.1.
**Provenance on your data: any row can say which run, which configuration, which script, and which source produced it — and agents can find datasets by meaning.**
* Opt-in **provenance stamping** per pipeline: every row, document, message, and vector chunk landed carries the run that produced it, when it landed, the definition version, the tap run that fed it, the exact script commit, and the declared source. Existing rows are never rewritten — provenance starts with the first stamped run; SQL destinations gain the columns automatically.
* Every stamp resolves back to its origin — the run, the feeding tap run, the script commit, the definition version with its change note, and the source — via the platform or the new **get\_provenance** agent tool. Vector search results carry provenance per chunk so RAG answers can cite the run and source behind each passage.
* A **lineage view** shows source → tap → pipeline → landed dataset → catalog, derived from configuration alone. Pipeline detail pages show what feeds them, where they land, and how fresh they are, using the same staleness judgment as the Activity dashboard.
* **Tags** on taps and pipelines: free-form labels shown in the catalog and ranked by discovery.
* **find\_data**: agents discover datasets by meaning — the best-matching pipelines their key can read, each with location, freshness, provenance handles, and a ready-to-run query hint. Discovery only: the query stays the agent's own call, under its own permissions. AI answers can carry the provenance handles along.
* Deleting a tap or pipeline from the catalog now shows progress while the platform finishes cleaning up.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required; provenance stamping is off by default — enable it per pipeline in the pipeline editor's Destination step.
**The recovery agent: Datris notices broken data flows, diagnoses them, and — within limits you set — repairs and verifies them.**
* The opt-in recovery agent opens an **incident** when a scheduled tap keeps failing after its automatic retries, a pipeline load errors, a tap goes quiet past its cadence, or a pipeline's volume swings against its baseline — then diagnoses it with the same judgment as the Ops chat and closes it with a step-by-step story.
* The [Agent Policy](/agent-policy) gains a **recovery mode**: `off`, `propose` (every repair waits for one-click approval), or `autopilot` (repairs follow your per-action policy) — with per-tap / per-pipeline overrides.
* Repairs are narrow and verified: re-run, script fix (as a new version), test — never deletes, secrets, or schema changes. Every fix must prove itself with a clean real run, or the platform reverts it and says so. Budgets are enforced by the platform, not a prompt.
* A new **Incidents** panel on the Activity page shows state, classification, and timelines with approval links; the Ops chat explains open incidents instead of re-diagnosing; two new agent tools read them; the audit log carries the full per-incident ledger; an optional webhook reports milestones.
* The MCP page's tool catalog now lists every available tool, including the destination-typing pair missing since v1.22.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required; set `USE_AGENT_POLICY=true` + `RECOVERY_AGENT_ENABLED=true` and pick a recovery mode to opt in.
**Security update for the UI and MCP server images.**
* The `datris-ui` and `datris-mcp-server` container images ship with updated OpenSSL packages, picking up upstream fixes for several recently disclosed issues, including one rated high severity. No functional changes.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required.
**Agent Policy: decide what agents may do on their own, what waits for your approval, and what is refused.**
* A new opt-in Agent Policy lets an administrator set, per action — run a tap, delete a pipeline, migrate destination column types, write a secret, and every other change an agent can make — whether agents do it unattended, queue it for a person to approve, or are refused. People using the UI are never gated; they are the approvers. Nothing changes until you set a rule, and the policy applies to every agent: the built-in Assistant and any connected MCP client alike.
* Queued actions appear under Activity → Approvals with who asked, the reason they gave, exactly what would run, and one-click Approve / Reject. Approving performs the original request on your behalf and records the whole chain — request, decision, and result — in the audit log. Approvals go stale if the pipeline or tap changed in the meantime, expire on their own, and can never be approved by an agent.
* Agents are told the truth: a gated action comes back "waiting for approval" (never silently done), a refused one says so plainly, and the chat shows matching status cards. Agents can read the policy up front, list what they have queued, and poll for your decision; every change-making agent tool now accepts a short reason that shows up on the approval card and in the audit log.
* Configuration → Agent Policy manages it all, with a recommended starting point and per-pipeline / per-tap overrides that can only tighten. The Configuration → Environment tab has been removed.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required; set `USE_AGENT_POLICY=true` to turn the agent policy on.
**Audit log: who did what, by login or API key.**
* A new opt-in Audit Log records every create, change, run, delete, login, and denied request — humans by their login, agents by their API key, scheduled runs as system. Admins read it under Configuration → Audit Log with filters by time, category, actor, and outcome, a detail view per entry, and CSV export. Entries are also written to the server log so an existing log aggregator picks them up.
* Actions the Assistant takes for you are attributed to you — in the audit log and in pipeline and tap version history — instead of to the platform's internal key. Every issued API key now carries a stable id, so a key that is revoked and later re-issued under the same name is never confused with its predecessor.
* Reads are left out by default to keep the trail focused on changes; turn them on to see every query and search an agent runs. Reading a secret is always recorded.
* Tap scripts that read platform data now work when API keys are required. Each run gets a short-lived, read-only credential that is attached automatically — scripts need no changes and no platform credential in their secret — and the audit log names the tap that made each call.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required; set `USE_AUDIT_LOG=true` to turn the audit log on.
**Set real column types on pipelines that landed as text.**
* Pipelines created through agents store every destination column as text. The Catalog now marks such pipelines (PostgreSQL, Snowflake, and Databricks destinations) with a quiet "text" badge — click it to review proposed column types and apply them. The same action is available from the pipeline view.
* Proposed types are inferred from the data already loaded, and every column shows real sample values so you can check the proposal at a glance. When a column stays text because of a stray value, the dialog shows the offending value and the type it blocked (for example: found "N/A", would otherwise be a number) so overriding is an informed choice.
* Applying is safe by design: every loaded value is validated first, the destination table is retyped, and the pipeline definition is updated as a new version. A value that won't convert fails the whole apply with the column named and nothing changed. From then on, incoming data is type-checked on every load.
* Agents get the same capability through two new MCP tools, `get_dest_types` and `apply_dest_types` — applying always requires the user's explicit approval.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required.
**Live progress feedback across the Assistant and tap building.**
* The Assistant shows what it's thinking about while it reasons — a live summary streams next to the progress indicator with an elapsed timer, instead of silent dots. The full reasoning stays available in the expandable thinking block.
* AI tap-script generation shows live progress: the tap wizard and the Assistant's tool cards report what the generation is doing and how long it has been running.
* The Assistant sets time expectations before long-running operations, and when AI script generation fails for a well-known API it writes the script directly instead of retrying — turning a multi-minute failure into a quick recovery.
* The Assistant only reports an action as completed when it actually ran and observed the result — tightened guarantees against overstated progress reports.
* Dependency updates across the UI and build tooling.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required.
**Security refresh across all container images.**
* All four container images rebuilt on updated bases with refreshed OS packages and Python tooling, clearing every published high- and medium-severity vulnerability finding against the images.
* Every container now reports its own health status, so `docker ps` (and any orchestrator) shows healthy/unhealthy per service out of the box.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. No configuration changes required.
**MCP tool catalog now matches API-key permissions.**
* With API keys enabled, each MCP session's tool list is filtered to what its key is actually allowed to do — agents no longer see tools that could only fail with a permission error at call time. Full-access keys and installs without API keys keep the complete catalog.
* The `rag-builder` key template gained the read permissions its workflow was missing (list taps, manage its own tap credentials). Existing issued keys are unchanged — re-issue or edit rag-builder keys to pick up the additions.
* With API keys enabled, MCP sessions that present no key are refused instead of silently connecting. Installs without API keys are unaffected.
* Permission denials return a clear, structured error instead of a generic server error, so agents recognize a permission boundary rather than retrying.
* Tightened API-key permission enforcement on the server.
* Upgrade: `docker compose pull && docker compose up -d --force-recreate`. If API keys are enabled, ensure MCP clients send their key (header for remote connections, `DATRIS_API_KEY` environment variable for stdio).
**Opt-in Postgres TLS enforcement.**
* Deployments marked as production now refuse to start when the platform's Postgres connection points at an external host without TLS (`sslmode=require` or stricter), with an explicit opt-out for deployments that intentionally use plaintext. The bundled in-network Postgres is exempt; existing deployments are unaffected unless production mode is enabled. Plaintext connections to external databases now log a startup warning either way.
**Observability: Prometheus metrics and structured logs.**
* Prometheus metrics are now exposed for scraping, reachable only from inside the deployment's network (the bundled edge proxy blocks them publicly). See [Audit Log](/audit-log) for the metrics endpoint.
* Production deployments emit one JSON log line per event for SIEM / log-aggregator ingestion; development logs stay human-readable.
**Tap isolation on by default for docker compose.**
* All bundled Docker Compose configurations now default to `USE_TAP_RUNNER=true`. The runner token is minted automatically on first boot (including upgrades where it was missing or still the placeholder value). The server fails fast if isolation is on and no valid token exists — a minted token is accepted.
* Running the server outside Compose (no runner sidecar) stays in-process and logs a warning. Isolated taps still cannot open direct connections to internal DB / object store / Vault — they return records; this is existing behavior, called out because compose now isolates by default.
* Set `USE_TAP_RUNNER=false` to keep in-process on compose. No host allowlist in this change.
**Dependency security cleanup across the server and UI.**
* Updated server and UI dependencies to resolve every remaining published medium-severity advisory (and one low). One low-severity advisory remains open upstream with no fixed release available. No functional changes intended.
* The Activity tab's charts moved to the current major version of the charting library as part of the security updates — same charts, slightly refreshed default colors.
* Standard upgrade: `docker compose pull && docker compose up -d`. Existing pipelines, taps, and schedules are unaffected.
**Server security updates and a friendlier vector-store default in the installer.**
* Upgraded the server's embedded web framework to its current supported release line, resolving all remaining published high-severity advisories against server dependencies (and a number of medium- and low-severity ones). No functional changes intended.
* **The installer now presents pgvector as the default vector store.** It ships inside the bundled Postgres, so vector search works out of the box with no extra container. Additional vector stores (Qdrant, Weaviate, Chroma, Milvus) remain opt-in additions at install time. Existing installs are unaffected — prompts and install summary only.
* Standard upgrade: `docker compose pull && docker compose up -d`. Existing pipelines, taps, and schedules are unaffected.
**Security updates across the UI and server, and a pipeline-delete fix.**
* Upgraded the web UI's underlying framework to the current long-term-support line, resolving all published critical- and high-severity advisories affecting the UI. No visual or functional changes intended.
* Updated bundled server dependencies to resolve the remaining published high-severity advisories against third-party libraries. No functional changes.
* **Deleting a pipeline now also removes its data from the built-in object store destination**, matching every other destination type. Previously the files were left behind, and recreating a pipeline with the same output prefix silently picked them up. Shared output prefixes are detected and safely skipped. Files orphaned by deletes on earlier versions are not removed retroactively.
* Hardened the UI container image configuration. No behavior change.
* Standard upgrade: `docker compose pull && docker compose up -d`. Existing pipelines, taps, and schedules are unaffected.
**Dependency security updates and an install fix.**
* Updated bundled server dependencies to resolve all critical published advisories against third-party libraries, and removed an unused legacy dependency tree from the server entirely. No functional changes.
* Fresh installs now create the default object-store output bucket automatically — previously the first pipeline writing to the built-in object store with default settings failed until the bucket was created by hand.
* Standard upgrade: refresh your checkout (or re-download `docker-compose.standalone.yml`), then `docker compose pull && docker compose up -d`. If you update images without refreshing the Compose files, create a bucket named `{environment}-data` (default: `oss-data`) once in the MinIO console.
**Security hardening.**
* A broad security pass across authentication, authorization, credential handling, and input validation, plus updated bundled dependencies for published advisories. A standard upgrade needs no configuration changes.
* **Initial admin login** (when user login is enabled): the first admin password is now generated and printed once to the server logs — `docker compose logs datris`, sign in, and change it.
* **Taps calling internal addresses** are blocked by default, with an opt-out for endpoints that legitimately run on an internal network (see the [Tap Execution & Isolation](/tap-execution-isolation) guide).
* **Bundled Vault** uses a unique per-install token; for manual `vault` commands read it with `docker compose exec -T datris cat /vault-token/token`.
* **Fix:** the default Docker Compose config pulls the published images again (a recent change had it building from source).
* Standard upgrade: `docker compose pull && docker compose up -d`.
**Build taps in any language — HTTP taps.**
* **HTTP taps.** A tap can now be a service you host — Rust, Go, TypeScript, a serverless function, or an existing internal system. Datris calls your endpoint on every run and everything else works exactly like a Python tap: schedules, per-run params, incremental sync state, run history, retries. Your service keeps its own upstream credentials, and no tap code runs on the platform. Wire contract and a complete Rust example: [HTTP Tap Contract](/tap-http-contract). Create one from the wizard, the CLI, or an agent over MCP.
* **A more focused tap wizard.** Two choices: paste your own Python script, or point at an HTTP endpoint. AI generation stays available through the Assistant and connected agents. Structured vs. document output is detected automatically from the test run, and a passing script is kept exactly as tested — the automatic performance-rewrite pass is gone.
* **GitHub-stored taps open at the latest code.** The editor loads the newest repo version on open (runs keep the previously saved version until you test and save). False "script was changed" warnings caused by unrelated repository commits are fixed, and the AI test diagnosis now works for repository-stored scripts.
* **Catalog quality of life.** Drag-resizable columns on the taps and pipelines tables (layout remembered), and background refresh no longer interrupts column resizing, move-to-catalog menus, or delete confirmations.
* Standard upgrade: `docker compose pull && docker compose up -d` — everything is additive; existing taps and schedules work unchanged.
**New AI provider: Grok (xAI).**
* **Grok for chat and CodeGen.** Grok joins Anthropic Claude and OpenAI as a third model family — the Assistant, tap script generation, AI data quality rules, transformations, and natural-language → SQL can all run on it. Configure it in Configuration → AI Providers with one key from console.x.ai; Grok 4.6 is the recommended model, with Grok Code Fast 1 available for CodeGen. Mix providers freely across sections. See [AI Configuration](/ai-configuration#grok-xai-path).
* **Install-time support.** The installer prompts for an xAI key alongside Anthropic and OpenAI, and first-boot `.env` seeding supports Grok as the chat/CodeGen provider.
* **Embeddings unaffected.** xAI has no embeddings API, so semantic search stays on OpenAI, Azure, or the bundled local model — a Grok-only setup works out of the box with the bundled embedder.
* Standard upgrade: `docker compose pull && docker compose up -d` — no configuration changes required.
**Assistant fix: taps that read data already in Datris no longer ask for database credentials.**
* **No more credentials form for platform data.** When a tap's fetch logic depends on data a pipeline already maintains (e.g. an id list kept fresh in a Datris table), the Assistant previously asked for database credentials or offered to freeze a snapshot of the data into the script. It now builds the tap to read the platform data live on every run — no extra credentials, and the tap's secret stays limited to the external source's API key.
* **Same guidance for MCP agents.** Agents connected over MCP (Claude Desktop, Claude Code, and others) receive the same instructions.
* Standard upgrade: `docker compose pull && docker compose up -d`. Start a new Assistant conversation after upgrading — existing conversations keep the older behavior until reopened.
**Azure OpenAI without API keys — Microsoft Entra ID authentication.**
* **Keyless auth for Azure OpenAI.** Required for resources with `disableLocalAuth` (API keys disabled by policy), and useful anywhere you'd rather rotate credentials centrally. Chat, CodeGen, embeddings, and the Assistant all honor it. See [AI Configuration](/ai-configuration#keyless-auth-microsoft-entra-id).
* **Service principal mode.** Enter your Entra Tenant ID, Client ID, and Client Secret once in Configuration → AI Providers; tokens are acquired and refreshed automatically.
* **Managed identity mode.** Zero stored secrets when Datris runs on Azure compute (VM, AKS, App Service) — the server authenticates as its managed identity.
* **Safe mode switching.** A stored API key always takes precedence, so the auth-mode selector clears the mode you're leaving — a stale key can never shadow Entra auth.
* **Install-time support.** Keyless setup works from `.env` too: provide the service-principal values, or nothing at all on Azure compute. Grant the identity the "Cognitive Services OpenAI User" role on the resource.
* Existing Azure OpenAI API-key setups keep working exactly as before — no configuration changes on upgrade.
**Reliability: long AI generations complete, and the Assistant shows live progress.**
* **Large script generations finish reliably.** Generating a big tap script can legitimately take more than five minutes — previously these could time out or hang partway. Generation responses now stream under the hood with a much larger time budget, so long generations run to completion.
* **Live progress in the Assistant.** Running steps show an elapsed-time counter once they take more than a few seconds, plus a note after about a minute explaining that long generations are normal and temporary provider errors retry automatically.
* **Unresponsive AI providers no longer hang requests.** AI calls fail fast during a provider outage and feed the automatic retry ladder, instead of waiting indefinitely on a response that will never come.
Standard upgrade: `docker compose pull && docker compose up -d` — no configuration changes required.
**Amazon Bedrock support — run Claude through your AWS account.**
* **New AI provider.** Amazon Bedrock joins Anthropic, OpenAI, Azure OpenAI, and Ollama for the chat assistants and CodeGen — Claude models served from your own AWS account with IAM authentication and AWS billing. Configure it in Configuration → AI Providers. See [AI Configuration](/ai-configuration#amazon-bedrock-path).
* **IAM-native auth.** Enter AWS credentials once, or leave them blank on AWS infrastructure to use the server's IAM role — zero stored secrets.
* **Live model discovery.** The model dropdown shows what your AWS account and region can actually invoke — including cross-region inference profiles — rather than a hardcoded list.
* **Claude Fable 5 support.** Chat recommends Claude Fable 5; CodeGen recommends Claude Opus 5. Fable 5's one-time AWS data-retention opt-in is explained in-product and in the docs.
* **Deliberate opt-in.** Bedrock is only ever selected explicitly — AWS credentials used for S3 destinations never silently become your AI provider. The installer offers Bedrock when you don't have a direct provider key.
* **Clearer credentials panel.** The Configuration → AI Providers key panel is now grouped per provider with a short explainer; credentials hide immediately after saving.
* **Fix: provider-scoped model lists.** A model saved under one provider no longer appears in another provider's dropdown after switching a section between providers.
* **Refreshed recommendations.** Claude Fable 5 is now the recommended chat model for the Anthropic provider.
**Azure OpenAI support — run Datris AI on your Azure resource.**
* **New AI provider.** Azure OpenAI joins Anthropic, OpenAI, and Ollama as a first-class provider for all three AI slots (AI Provider, CodeGen, Embedding) — mix it freely with the others. Configure it in Configuration → AI Providers with your key, resource endpoint, and deployment name.
* **Deployment-name aware.** Azure addresses models by your deployment names; name deployments after the model they serve so model-specific handling applies automatically.
* **Installer support.** The one-command installer offers Azure OpenAI when you don't have a direct Anthropic or OpenAI key, and first-boot seeding configures everything from your Azure details. See [AI Configuration](/ai-configuration#azure-openai-path).
* **Built for how Azure behaves.** Responses stream under the hood so long reasoning calls complete reliably; both current and legacy Azure endpoint styles work.
* **Fix: session-expiry login.** Signing back in after a session expires no longer requires a full page refresh.
* **Fix: per-provider key updates.** Updating one provider's API key through the API no longer clears the other providers' stored keys.
* **Fix: agent-uploaded content.** Data uploaded through the AI assistants can no longer be silently garbled when the model sends plain text where encoded content was expected.
**Fix: the tap sync-state view is now reachable.**
* **Sync-state button restored.** v1.14.0's per-tap sync-state view (current bookmark, commit time, one-click reset) was missing its button on the Catalog page, so it couldn't be opened in the UI. Each tap row now has a bookmark icon that opens it. The MCP sync-state tools were unaffected.
**Incremental taps — scheduled taps now fetch only what's new.**
* **Taps remember where they left off.** After each successful run the platform stores a bookmark; the next run pulls only records added or changed since. Recurring syncs run at constant size instead of re-fetching everything.
* **No gaps, no duplicates.** The bookmark only advances when a run succeeds — failed runs leave it in place so the retry re-fetches the same window; key-field destinations absorb overlap by updating in place.
* **Visible and resettable.** Each tap's sync state can be viewed and reset from the Taps page; agents get the same via two new MCP tools. Manual backfills with an explicit date range never move the bookmark.
* **AI-built taps are incremental by default.** Described a recurring source? The generated script now uses change-detection automatically when the source supports it (modified-since filters, ID watermarks, cursors, or content hashing). See the [Taps guide](/taps#incremental-sync-state).
* **MongoDB upsert.** MongoDB pipelines can declare key fields, so revised records update in place instead of duplicating — previously blocked by over-strict validation.
* **MCP playground fixed.** All tools in the MCP tab's playground now execute (28 previously errored when run there); 63 tools total including the two new sync-state tools.
**Store your tap scripts in GitHub — every save is a commit.**
* **GitHub script storage.** Connect a repository under Configuration → Code Repository and tap Python scripts are stored there instead of built-in storage — every save from the wizard, the Assistant, or the API becomes a reviewable commit.
* **Runs pinned to the saved version.** Scheduled runs execute the exact commit that was saved, from a local cache — they keep working even if GitHub is unreachable, and each run records which version it executed.
* **Edit in your own IDE.** Commit changes to the repository directly; Datris detects the newer version when the tap is opened and offers to load it. Conflicting edits are stopped with a clear message instead of overwritten.
* **Move taps either way, one click.** Existing taps stay on built-in storage until moved; each Taps row shows where its script lives. Fine-grained token scoped to the one repository; connection test built in; GitHub Enterprise Server supported. See the [GitHub Script Storage guide](/tap-github-storage).
**Claude Opus 5 support, and a fix for a month-after-install credential expiry.**
* **Claude Opus 5.** Anthropic's newest model is fully supported and is now the recommended Anthropic model for chat and code generation — same price as Opus 4.8 with better results. Fresh installs default to it; existing installs keep their configured model.
* **Fix: internal credential expired a month after install.** An internal service credential was silently issued with a 32-day lifetime, after which restarting the platform could fail. Credentials are now long-lived and refreshed on every startup. Applies automatically on upgrade.
**Curate the data sources your assistant recommends.**
* **Approved data sources registry.** A new Configuration → Data Sources tab lets you maintain a curated list of approved external data sources in one editable document. When the AI assistant or the tap brainstormer suggests where to pull data from, registry sources are offered first, by name; anything outside the registry is clearly flagged. Leave it empty and behavior is unchanged.
* **You stay in control.** Registry entries don't skip confirmation — the assistant still confirms source, scope, and destination before building anything.
**Failed scheduled runs now retry themselves — and tell you how to fix what they can't.**
* **Automatic retry for scheduled runs.** Transient failures (source briefly unreachable, rate limits, network blips) are re-run automatically a few times with a short delay before being flagged. Only runs that are provably safe to repeat are retried, so a retry can never duplicate data. On by default and configurable; manual runs pause the retry ladder.
* **AI fix suggestions, right where failures show.** Persistent failures get a one-line suggested fix directly on the failed run in Ops → Ingestion and tap Run History, with the full diagnosis and concrete change one click away. Advisory only — nothing changes unless you apply it.
* **Agents see suggestions too.** Agents polling pipeline status get the same one-line fix suggestion for failed jobs.
* **Sign-in session fix.** With user login enabled, an expired session now ends cleanly and returns you to the login screen right away, instead of requiring a page refresh.
**Search shows only what's installed, plus internal consolidation.**
* **The Search tab now lists only active data stores.** Stores that aren't installed or reachable no longer appear in the query-type menu, and the list recovers automatically after a server restart.
* **Clearer errors for unavailable stores.** Searching a store your deployment doesn't have returns a clear message naming the valid options.
* **Internal consolidation.** Vector-store loading and search, AI provider integration, and tap AI workflows were restructured onto shared foundations — same behavior, less duplication, faster future vector-destination support.
**Reliability and hardening.**
* **Faster repeated queries.** Postgres connections are now pooled and reused across pipeline loads and interactive queries instead of being opened fresh each time.
* **Connection details are masked in logs.** Database connection information no longer appears in logs or job status output.
* **More stable long-running deployments.** Fixed several resource leaks in file ingestion and data-reading paths that could slowly accumulate open connections.
* **Clearer diagnostics.** Many background failures that were previously silent are now logged with context.
* **Every release is now test-gated.** An automated test suite runs on every change and before every release build.
**Installer hardening.**
* **Clear guidance when a previous installation is in the way.** If containers from an earlier Datris install (in another directory) would block a new one, the installer detects this before downloading anything and prints exactly what is blocking, where it lives, and how to remove or upgrade it.
* **API keys are no longer echoed.** The installer reads provider keys with input hidden and confirms with a masked preview, so keys never land in your terminal scrollback.
* **Best model out of the box.** Fresh installs choosing Anthropic default to Claude Opus 4.8 for chat, NL→SQL, and analysis — matching code generation. Existing installs keep their configured model.
**Choose your databases at install — run less, connect what you already have.**
* **Pick and choose at install.** The installer now asks which databases and stores you want instead of installing everything. Each can run bundled, connect to a service you already operate, or be skipped — and pressing Enter at every prompt still gives the standard full install.
* **Bring your own infrastructure.** Point Datris at an existing Postgres, Kafka, or managed vector store (Qdrant, Weaviate, Chroma, Milvus) during install, and optionally store Snowflake or Databricks destination credentials up front so pipelines can use them on day one.
* **Lighter installs.** Skipping the bundled semantic-search server avoids a multi-gigabyte model download — the installer recommends OpenAI embeddings, which need no local container and cost pennies. Vector stores and a local Kafka test broker are now a one-line opt-in instead of commented-out YAML.
* **Both AI providers, each at its best.** The installer asks for an Anthropic key (chat, code generation, AI data quality) and an OpenAI key (embeddings) — both optional, with sensible behavior when only one is present.
* **Everything knows what's installed.** The pipeline wizard greys out destinations that aren't available, the AI assistant offers only the destinations your deployment actually has, and creating a pipeline against a missing database returns a clear explanation instead of a timeout.
* **Install-time validation.** After first boot the installer checks every store you configured — bundled or external — and reports each by name, so a mistyped hostname or bad credential surfaces immediately.
* **Change your mind anytime.** Enable or disable any store after install with a one-line edit and a restart. Disabling keeps the data; re-enabling brings it back.
* **Safer upgrades.** Upgrading an existing install never changes which services run — everything you had keeps running, no action required. Data services now use durable named storage, so containers can be removed and recreated without losing data.
**Upgrading:** Re-run the installer, or `docker compose pull && docker compose up -d --remove-orphans` after refreshing the compose file. Your current services and data are preserved automatically. See [Installation](/installation) for the new options.
**Databricks destination — load, upsert, and query your Databricks workspace.**
* **Load into Databricks.** Point any structured pipeline at your own Databricks workspace and Datris loads it as governed Delta tables in Unity Catalog: tables are created automatically, the schema evolves as new columns appear, and natural-key upserts keep scheduled re-runs duplicate-free. Loaded tables are immediately queryable across the workspace — notebooks, dashboards, and lineage all see them natively. Available in the pipeline wizard and through the AI assistant.
* **Secure by default.** Connects as a dedicated service principal you control, with least-privilege access to a single catalog; credentials live in a platform secret, never in pipeline configs. The [Databricks destination guide](/destinations/databricks) includes a copy-paste grant script. Works out of the box with serverless SQL warehouses — nothing runs in your cloud account and there's no cluster to manage.
* **Safe full refreshes.** Pipelines that replace their table on each run swap the contents atomically — readers never see a half-loaded or empty table, and a failed run leaves the previous data intact.
* **Forgiving setup.** Pasted workspace URLs and warehouse connection paths are cleaned up automatically in whatever shape they arrive, and connection problems come back as plain-language errors that say exactly what to fix.
* **The assistant can verify and query Databricks.** Ask it to confirm a load landed or answer questions over the data — read-only, using the pipeline's own credentials — and it can browse your catalogs, schemas, tables, and columns along the way.
* **Cleaner deletes.** Removing a pipeline together with its data also cleans up the table it created in Databricks.
* **CLI catch-up.** `datris ingest` can now target Snowflake and Databricks directly, and pipeline listings label Snowflake and Databricks destinations.
**Upgrading:** Existing installs: `docker compose pull && docker compose up -d`. The CLI: `brew upgrade datris`. Using Databricks requires a one-time setup in your workspace — see the [Databricks destination guide](/destinations/databricks).
**Snowflake destination — load, upsert, and query your Snowflake account.**
* **Load into Snowflake.** Point any structured pipeline at your own Snowflake account and Datris bulk-loads it: tables are created automatically, the schema evolves as new columns appear, and natural-key upserts keep scheduled re-runs duplicate-free. Available in the pipeline wizard and through the AI assistant.
* **Secure by default.** Connects with key-pair authentication under a dedicated service user and least-privilege role you control; credentials live in a platform secret, never in pipeline configs. The [Snowflake destination guide](/destinations/snowflake) includes a copy-paste setup script that creates everything — role, warehouse, database, service user — in one run.
* **Forgiving setup.** Pasted private keys and account URLs are cleaned up automatically in whatever shape they arrive, and connection problems come back as plain-language errors that say exactly what to fix.
* **The assistant can verify and query Snowflake.** Ask it to confirm a load landed or answer questions over the data — read-only, using the pipeline's own credentials — and it can browse your databases, tables, and columns along the way.
* **Watch the assistant work.** Long assistant turns now show live progress: a size counter while it composes larger pieces of work, a running glimpse of its reasoning, and a continuous activity indicator between steps — no more silent pauses.
* **Faster pipeline creation.** The assistant sends only a small schema sample when creating pipelines, trimming minutes off longer build conversations.
* **Cleaner deletes.** Removing a pipeline together with its data now also cleans up the table it created in Snowflake.
**Upgrading:** Existing installs: `docker compose pull && docker compose up -d`. The CLI: `brew upgrade datris`. Using Snowflake requires a one-time setup in your Snowflake account — see the [Snowflake destination guide](/destinations/snowflake).
**Claude Sonnet 5 support.**
* **Choose Claude Sonnet 5.** Anthropic's new Claude Sonnet 5 model is now available as a choice for the AI assistant and code generation. Select it in the Configuration tab like any other model.
**Upgrading:** Existing installs: `docker compose pull && docker compose up -d`. The CLI: `brew upgrade datris`.
**Switch AI providers freely, and an assistant that finishes the job.**
* **Switch providers without losing your keys.** Your Anthropic and OpenAI API keys are now kept per provider, so you can change the provider for any section — AI, CodeGen, or Embedding — and switch back later without re-entering the key you already saved. Enter each provider's key once and mix providers however you like.
* **The assistant runs to completion.** Longer assistant replies that used to stop partway — leaving you to type "continue" — now finish on their own.
**Upgrading:** Existing installs: `docker compose pull && docker compose up -d`. Keys you've already entered carry over; no re-entry needed. The CLI: `brew upgrade datris`.
**Your secrets and configuration now persist across restarts and rebuilds.**
* **Settings that stick.** Connection secrets, AI provider and model choices, and other changes you make in the Configuration tab now survive a rebuild (`docker compose up --build`), a restart, and host reboots. Previously some of these could quietly revert on a rebuild because they were held only in memory — now they're stored durably, and the Configuration tab is the source of truth.
**Upgrading — one-time, please read:** with this release the secrets store moves from in-memory to durable on-disk storage. Any secret you had set **only** in the app's UI — **including tap connection secrets** — does not carry over and must be entered once more after upgrading. From then on your secrets persist across every future rebuild and restart. (Settings supplied through your environment configuration are restored automatically.) Existing installs: `docker compose pull && docker compose up -d`. The CLI: `brew upgrade datris`.
**Version history for taps and pipelines, plus a faster assistant.**
* **Version history for taps and pipelines.** Every create or change to a tap or pipeline is now saved as a snapshot. Open Version History to see what changed and when, compare any two versions side by side, and roll back — or forward — to an earlier version. Nothing is overwritten; restoring simply adds a new version. AI agents can review, compare, and restore versions too.
* **Configuration that sticks.** AI provider and model changes in the Configuration tab now save reliably and persist across restarts.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. Existing taps and pipelines are seeded into version history automatically on first start; no manual migration needed. The CLI: `brew upgrade datris`.
**Stronger isolation for tap scripts, plus a catalog readability fix.**
* **Optional sandboxed execution for taps.** Tap scripts can run in a dedicated, isolated runner that's cut off from the platform's credentials and internal services — defense-in-depth for the code your taps execute. Opt-in; see the Tap Execution & Isolation guide.
* **Tighter defaults for tap scripts.** Platform credentials are kept out of tap code, and any extra packages a tap needs install in isolation rather than into the shared environment.
* **Catalog: full names on hover.** Long tap and pipeline names in the catalog are no longer cut off — hover to see the full name.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Drop a file into the Assistant and it builds the pipeline for you.**
* **Drag a file into the chat.** Attach a CSV, JSON, XML, or document right in the Assistant conversation. The assistant reads a sample, works out the shape, and proposes where to put it — then, once you confirm, creates the pipeline, loads your data, and reports how many rows landed. No wizard, no manual schema step.
* **It confirms before it builds.** The assistant picks a sensible default destination for your file and names the alternatives, so you can steer it to Postgres, MongoDB, an object store, or a vector store before anything is created — and it asks about an upsert key if that makes sense for your data.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**A more decisive assistant, and taps that tell you when a credential is missing.**
* **No more "type continue to keep going."** The in-product assistants (build, ops, catalog, and search) used to sometimes end a turn by announcing the next step and then stopping — making you nudge them to carry on. Now, when the assistant has already decided what to do next, it just does it, while still pausing where it genuinely needs your decision or approval.
* **Taps surface missing credentials instead of silently returning nothing.** If a tap's API key or other credential has been deleted or is missing the field the tap needs, the run is now reported as a clear failure naming what's missing — rather than quietly completing with zero records and hiding the real cause. Runs that legitimately have no new data are unaffected.
* **Clearer MCP activity graph.** The connection graph in the MCP Activity monitor now renders at a readable size.
* **"Move to catalog" shows everything.** The per-item move menu in the Catalog now lists all your catalogs, not just the first one.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Claude Fable 5 — Anthropic's most capable model, now selectable.**
* **New model option.** Claude Fable 5 now appears in the model picker for both the in-product assistant and code generation, alongside the existing Claude and OpenAI choices. Pick it from Configuration when you want Anthropic's most capable model for demanding reasoning and long-horizon work.
* **A note on data retention.** Fable requires standard (30-day) data retention on the Anthropic account and isn't available on zero-data-retention organizations. If you bring your own Anthropic key on a zero-data-retention plan, choose a different model — you'll now see a clear message explaining why, instead of a generic error.
* **Clearer model errors.** When a model declines a request — wrong account settings, a rejected key, or unsupported options — the assistant now surfaces a plain-language explanation naming the model, rather than a raw provider error.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**A domain-neutral assistant — guidance that fits whatever data you work with.**
* **No more finance-flavored examples.** The in-product assistant, the tap generator, and the prompt suggestions used to lean on stock-market wording — tickers, symbols, financial filings — in their examples and defaults. They're now domain-neutral, so the guidance and sample values match the data you're actually working with, whatever the domain.
* **Generic search defaults.** Vector search now defaults to a `documents` collection (and a `Documents` class) instead of finance-specific names. If you already pass your own collection, table, or class name, nothing changes — only the placeholder defaults differ.
* **Refreshed starter prompt fragments.** The built-in tap prompt fragments now describe general patterns — rate-limited APIs and APIs that require a User-Agent header — rather than specific named data providers.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Organize your catalog by chatting with it.**
* **Catalog assistant.** The Catalog tab has a new side-panel assistant for tidying things up. Ask it to group your taps and pipelines into catalogs, move items around, or suggest a cleaner structure — it lays out a plan first and only makes changes once you approve them, and the catalog updates as it works. It sticks to organizing: questions about your data go to Search, and running or fixing pipelines stays in Ops.
* **Right where you're working.** The assistant lives in a collapsible right rail (toggle it with Cmd/Ctrl + backslash) so the catalog stays in view while you reorganize. Clicking **Describe to Assistant** on a catalog now opens this panel in place instead of jumping to another tab, and the conversation sticks around as you navigate away and back.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Ask your data a question — conversational search comes to the Search tab.**
* **Chat search.** The Search tab has a new **Chat** mode, with a **Traditional** toggle for the structured query UI you already know. Ask a question in plain language and Datris finds the answer across all your pipelines and taps — cataloged or not — querying tables, searching documents, and replying with citations to where each answer came from. It's read-only: it looks, it never changes anything.
* **Scope to a catalog.** Narrow a chat to a single catalog (or to Uncataloged data) from the dropdown, or leave it on All to search everything.
* **Conversations survive a refresh.** Your Search chat and Assistant conversations now persist across a browser refresh, so reloading the page no longer clears the transcript.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Orchestrate Datris taps from Apache Airflow.**
* **Run taps from Airflow.** A new `airflow-provider-datris` package adds an operator that triggers a tap, waits for the pipeline to finish, streams Datris logs into the Airflow task log, and reports run tokens and row counts back to Airflow. Cancelling the DAG run cancels the Datris job.
* **Date-windowed backfills.** Taps can now take per-run parameters, so an Airflow DAG can pass its logical date (or any window) into the tap for that run — backfills and incremental loads work without editing the tap.
* **No double-firing.** A tap is scheduled by Datris or Airflow, never both: if a tap has a Datris cron, the Airflow operator declines to trigger it. To drive a tap from Airflow, leave its cron empty.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. Install the provider where Airflow runs: `pip install airflow-provider-datris`.
**Write to AWS S3, query Parquet and ORC from the Assistant and Search, and stop chats actually stop.**
* **AWS S3 as a first-class destination.** The Object Store destination now writes Parquet or ORC directly to S3 alongside the built-in MinIO. Pick *Object Store (MinIO or S3)* in the pipeline wizard, point at your bucket, and reference a credentials secret you've created in Configuration → Secrets → Platform. Multiple S3 destinations with different IAM keys coexist in the same deployment — each pipeline carries its own credential reference, applied per bucket at write time.
* **Region lives with the credential.** AWS credentials and region travel together in the credentials secret rather than on the pipeline config. One source of truth, one place to rotate, no more silent `us-east-1`-says-the-config-but-the-key-is-`us-west-2` failures. Field names are flexible — `accessKey` / `AWS_ACCESS_KEY` / `AWS_ACCESS_KEY_ID` all work.
* **Query Parquet and ORC from the Assistant.** "Show me the weather data" now works against pipelines whose destination is Object Store. The Assistant resolves the bucket and credentials from the pipeline config, reads the columnar files, and returns rows in chat.
* **Search tab gains an Object Store option.** Pick a pipeline from the dropdown, see the resolved bucket/prefix/format, set a limit, hit Execute. Works for both MinIO and S3 destinations.
* **Assistant offers all three structured destinations.** When you ask for structured data and no pipeline yet covers it, the Assistant now mentions MongoDB, PostgreSQL, **and** Object Store as choices instead of silently defaulting to one.
* **Assistant can discover destination credentials it shouldn't create.** Platform-tab secrets are now visible to the Assistant for reading (names and field shape only — never values). When a pipeline destination needs a credentials reference, it lists what's available, verifies the field shape, and points you at the Secrets tab to create one when nothing fits.
* **Stop actually stops.** Clicking Stop in the Assistant now halts the in-flight chat within a fraction of a second instead of waiting for the upstream model to finish generating. Cancelled responses cost only what was already streamed.
* **Pipeline failures surface fast.** A failed pipeline now flips to Error in the Ops dashboard within seconds instead of staying stuck in Processing for up to ten minutes.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. Existing MinIO pipelines work unchanged. For AWS S3, create a Platform-tab secret with `accessKey`, `secretKey`, `region` (and optionally `sessionToken`), then reference it by name from your pipeline.
**Ask the Ops assistant about a failing pipeline without leaving the dashboard.**
* **New Ops chat side panel.** A collapsible chat lives on the right side of Ops → Activity. The assistant has the current failures, stale taps, and volume anomalies in mind — ask "why did `X` fail?" and it pulls the root cause; ask it to re-run a tap and it runs and reports the outcome. The panel stays mounted as you switch between Activity and Ingestion, so the conversation survives the tab change.
* **"Ask" buttons on failure and volume rows.** Click "Ask" next to a row to seed the chat with a row-specific question so you don't have to retype the tap or pipeline name.
* **Successes are expandable like Failures.** Click any row in the Successes pane to see the same event trail (begin → processing → end) you get from the Failures pane. Only one row across either pane is open at a time so the layout stays compact.
* **Claude Opus 4.8 is the new recommended CodeGen model.** New installs seed Opus 4.8 as the codegen default. Existing installs can pick it from the model dropdown in Configuration. The older Opus versions remain selectable for anyone who wants to pin a specific version.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**The Assistant stops second-guessing itself when the data is already there.**
* **No more apology loops.** When the Assistant verifies platform state and the pipeline / tap it created earlier is present in the list, it now treats that as evidence the work was done — instead of retracting a prior turn's "done" claim and rebuilding from scratch.
* **"Show me X" goes straight to the data.** When you ask to see / list / show data and a matching pipeline already exists, the Assistant now jumps to the destination's query tool (Mongo / Postgres / vector search) and returns the actual rows or documents. It no longer asks you which sources, providers, or schedules to use for a pipeline you already have.
* **Long catalogs no longer hide existing resources.** The tools the Assistant uses to inventory pipelines and taps now return a compact summary with names at the top. Previously, on environments with many pipelines, an entry near the end of the list could slip past the Assistant's scan — the new shape makes every name impossible to miss.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Ops Activity gains a Successes pane, long lists scroll in place, and the dashboard's auto-refresh no longer yanks you back to the top.**
* **Successes pane on Ops Activity.** A new pane below Failures lists every pipeline that ran successfully in the selected window, with the run count, items processed, and last-run time. Click a row to jump to that pipeline.
* **Long lists scroll inline.** Failures, Successes, and the Per-pipeline volume table all cap at \~10 rows of height and scroll internally instead of stretching the page. Per-pipeline volume column headers stick to the top so you don't lose context as you scroll.
* **Auto-refresh preserves scroll position and expansion state.** The 30-second refresh no longer scrolls a long pipeline-volume list back to the top or collapses an open failure detail. Expand a failure, scroll where you want, leave the tab open — it stays put.
* **Numeric column headers aligned with their data** on the Per-pipeline volume table — Today, 7d avg, vs avg now line up with the numbers underneath.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**A new Ops Activity dashboard, Postgres pipelines learn upsert, per-run tap parameters, and a safer agent workflow.**
* **Ops Activity dashboard — at-a-glance ingestion health.** A new tab under **Ops** pulls every tap and pipeline run in a rolling window (24h / 7d / 30d) into KPI tiles, time-series charts of runs and items ingested, a per-pipeline 7-day volume table, and a Failures pane that dedupes by item with attempt counts. Each unrecovered row has a **Re-run** button — for pipeline failures with an upstream tap, the button re-runs that tap to retry the load so you don't have to hunt for it in the Catalog.
* **Postgres pipelines upsert on conflict when `keyFields` is set.** Matches the semantics Mongo has always had. Backfills over already-loaded dates, incremental taps with overlap, and "load again with the same key" flows now upsert instead of failing with a duplicate-key error. If you retrofit `keyFields` onto an existing table, the platform adds the matching unique index for you on the next load — or surfaces a clear remediation message if existing data violates the proposed key.
* **Per-run tap parameters.** Run a tap with caller-supplied values — date ranges, ticker lists, page cursors, batch sizes — without rewriting the script or the secret. Pass `params` to `run_tap` and the script reads them as env vars for that one run only. Scheduled cron runs see an empty params bag and fall back to script defaults, so the same script handles both manual ad-hoc calls and unattended schedules without branching.
* **"No records" is no longer a failure.** A tap that runs cleanly and returns zero rows — a polling tap on a quiet day, an incremental tap that's caught up, a market tap on a weekend — now records a distinct `no_records` status with a neutral badge. Doesn't count in the Failures tile, doesn't fire bogus "recovered" badges, doesn't train agents to interpret "no new data" as "platform broken."
* **Large tap outputs fail fast with an actionable error.** A backfill that exceeds the size cap (default 100 MB) now stops with a clear message telling you and the Assistant to chunk the source range smaller — instead of OOM-killing the server. Multiple smaller runs all land in the same destination pipeline; with `keyFields` set, overlapping ranges upsert safely.
* **Concurrent tap runs no longer race.** Two pipelines that loaded data in the same millisecond previously risked landing each other's records in the wrong destination under specific timing. Fixed at the source; no action required.
* **Secret values stay masked in Configuration.** The Secrets tab now masks any field whose name suggests a credential — passwords, tokens, API keys, signing keys, certificates, and named variants of those (e.g. `*_API_KEY`, `*_SECRET_*`). No action required.
* **Tighter Assistant workflow.** Three new disciplines:
* **Scheduling lives on the tap.** Say "every morning" or "at market open" and the Assistant sets the CRON expression on the tap itself — instead of handing back a `cron` line for you to wire up yourself.
* **Test before first run.** A newly-created or just-edited script is validated before any real run, and before being put on a schedule — no more "guaranteed-bad nightly run" the next time the cron fires.
* **No confabulated progress.** If the Assistant intended to do N things and only did M of them, it tells you which M happened rather than narrating all N as complete.
The credential form the Assistant pops up now asks only for true secrets — not for configuration values you already typed into chat.
* **Agent Monitor stays responsive during long tool calls.** Running a slow tool no longer freezes the Connections viz or the Activity Log — they keep streaming. Returning to the Agent Monitor tab refreshes the log to current server state instead of showing a stale empty view.
* **Configurable JVM heap.** The bundled `datris` service has a sensible default heap size for small hosts (8 GB) and a clean way to bump it on larger machines via `.env`. See [Installation → JVM Heap Sizing](/installation#jvm-heap-sizing) for the suggested sizings per host RAM.
* **Ops tab moved after Search** in the top navigation. Mostly cosmetic; route URLs are unchanged so bookmarks and deep links still work.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. Existing Postgres pipelines with `keyFields` automatically pick up the upsert path on the next load. The CLI: `brew upgrade datris`.
**Embedding pipelines stop failing on oversized chunks, the Assistant carries long runs to completion, and the Catalog gets the missing delete buttons and run-history detail.**
* **Embedding pipelines no longer fail when a single chunk is too big.** Every embedding call is now guarded by a token-aware safety net that splits any chunk over the model's input cap before sending — so a 10-Q ingest with one dense table can't take down the whole batch. Works the same for OpenAI, Cohere, Voyage, BGE-M3, Nomic, Mistral, or anything else, with built-in caps for the common models and a conservative default for the rest. OpenAI families get exact token counts; everyone else gets a heuristic.
* **Token-aware chunking.** A new `maxChunkTokens` option on the chunking config tells the chunker to stop merging segments before they cross a token estimate — the safety net above becomes a true last resort. Recommended for any new vector pipeline.
* **The Assistant carries pipeline runs to completion.** When the agent kicks off a tap or upload, it now polls the run to a terminal outcome with exponential backoff instead of summarizing "still running — check back later." You'll see a one-line progress update each cycle and the final outcome reported in chat, including any per-document failures.
* **Run History now shows per-document outcomes.** Expanding a tap run that fed a vector pipeline lists every document the pipeline processed with its own status, elapsed time, and — for failures — the specific stage and error. A run that fetched 28 documents but failed on 1 now shows the failure inline instead of a misleading green "success."
* **Catalog: delete buttons work on individual taps and pipelines.** The trash icon now shows an inline confirm right in the row — Delete / × for taps, Config & Data / Data Only / × for pipelines (Data Only wipes rows but keeps the config so the next ingest fills it again).
* **Catalog: last-run timestamp is back above the status badge.**
* **Agent Monitor fits the viewport.** Both Connections and Activity Log panes now size to the visible area on first load and reflow when you resize the window, not just in the pop-out.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui mcp-server`. No data migration needed. The CLI: `brew upgrade datris`.
**Streamlined navigation and a Catalog-centric workflow — fewer tabs, richer Catalog, and the Assistant front-and-center.**
* **Five top-level tabs instead of ten.** Datris now opens to Assistant, MCP, Catalog, Data, Configuration — plus the Help dropdown. Same capabilities, organized around how you actually work.
* **Catalog is the home for taps and pipelines.** Each catalog card embeds the full tap and pipeline tables with inline rename and a move-to-catalog dropdown on every row. Uncataloged is always shown so day-1 users have a place to start.
* **Bulk move at the catalog level.** Move every tap and pipeline from one catalog into another in one click. The destination auto-expands so you see the items land.
* **Describe to Assistant from any catalog.** A button on each catalog card opens the Assistant with a fresh chat and the catalog pre-filled — the Assistant assigns the right catalog to whatever it creates.
* **Wizards link back to where you came from.** Tap and pipeline edit screens now show the item's name in the title, a "Back to Catalog" link at the top, and primary action buttons at both top and bottom of the form. Pipeline wizard's JSON-review step is gone — Save fires from the Destination step.
* **Pop out the Agent Monitor.** A new icon opens both Connections and Activity Log in a separate browser window — park it on a second monitor and watch tool calls stream while you work in the Assistant.
* **Catalog state persists across navigation.** Open catalogs and expanded sub-sections survive refreshes and tab switches.
* **Fixed: tap rename no longer breaks scripts.** Renaming a tap inline used to leave the new tap pointing at a deleted script file.
* **Fixed: catalogs containing only pipelines could lose their assignment** when the pipeline edit wizard was opened.
* **Discovery tab removed.** The Assistant covers its workflow.
* **Getting Started tab removed.** First-run guidance lives in the Assistant's starter prompts and inline empty states.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui`. No data migration needed. The CLI: `brew upgrade datris`.
**Scoped API keys with per-agent permissions, plus Assistant resilience and smarter onboarding.**
* **New API-Keys tab in Configuration.** Issue a dedicated key per agent, CLI, or integration with an explicit list of what it's allowed to do — read pipelines, run taps, upload documents, query data, and so on. Each key is its own identity in the request log and can be rotated or revoked independently. Five starting templates: read-only, rag-builder, reporting, ops, and full-access. The tab only appears when you've set `USE_API_KEYS=true` in `.env` — issuing keys is pointless until the validation layer is on.
* **Keys actually constrain.** When an external agent (Claude Desktop, Cursor, the CLI) connects with a scoped key and tries something outside its bundle, the platform refuses the call and tells the agent why in plain JSON — so the agent doesn't keep retrying alternate paths. Agents stay productive within their lane; you don't have to trust them not to wander.
* **UI no longer asks for a key when user authentication is on.** With login enabled, your session cookie is the only thing the browser needs — paste-the-key flow goes away. The Assistant runs under your identity, audit logs show you as the actor, and your role determines what it can do (admin = full access, editor = data writes, viewer = read-only).
* **Assistant rides through Anthropic overload.** When Claude Opus is rate-limit-shedded, the Assistant retries with backoff and, if needed, transparently switches to Sonnet for the rest of the turn — with a small inline note so you know it happened. Conversations that previously errored out now keep moving.
* **Assistant checks the platform before suggesting external sources.** On any data-related ask ("I'm looking for X"), the agent now lists your existing pipelines and taps first, then either points you to what already exists or asks before adding more. Avoids the "let me enumerate seven public APIs" detour.
* **Assistant auto-runs newly created taps that have no schedule.** When you build a one-shot tap, the Assistant kicks off the first run so you see real data instead of an empty pipeline. For scheduled taps it asks first, since the cron will fire on its own.
* **Health and version endpoints are public.** Container orchestrators, status pages, and the UI's connection check no longer trip 500s when API-key auth is required.
* **Configuration → Taps sub-tab removed.** Prompt Fragments are unchanged and still apply to tap creation, brainstorm, auto-fix, and Discovery — they're now managed via the API instead of a dedicated UI page.
* **Tap wizard pipeline link is clickable.** The "Linked to: \" pill in step 4 now navigates straight to the pipeline editor.
**Upgrading:** `docker compose pull && docker compose up -d --force-recreate datris ui`. Existing API keys keep working — they're treated as full-access until you replace them with scoped keys from the new tab. The CLI: `brew upgrade datris`.
**Polish for the Assistant, AI Configuration, and MCP tabs.**
* **Assistant: keep typing without clicking.** After you send a question, the cursor stays in the composer — so as soon as the agent finishes (or even while it's still working), you can type your next prompt without reaching for the mouse.
* **AI Configuration: provider switches no longer wipe your overrides.** Switching the primary AI, codegen, or embedding provider used to clear the saved provider/model/endpoint on the next page load, so you had to re-pick them every time. Your selections now persist correctly through a provider switch (you still re-enter the API key when changing providers — that's intentional).
* **Connect Your Agent: paste-and-go for local.** The MCP tab's generated config no longer asks for an API key for the default local setup — paste the snippet into your agent and it just works. A key is still required when you point the snippet at a remote instance.
* **Hosted trial provisioning tools removed from the agent toolset** (hosted platform since retired). The Getting Started tab has also been refreshed to point at the Assistant for the common onboarding path.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed. The CLI: `brew upgrade datris`.
**A new in-product Assistant — chat your way from "I need data" to a working pipeline.**
* **The Assistant tab.** A new top-level tab opens an in-product agent that finds an external data source, builds the tap, creates the pipeline, runs it, and shows you the result — all in one chat. You watch the model's reasoning, every tool it calls, and the live status, then click straight into the tap or pipeline it created. See [Assistant](/assistant).
* **Real-time visibility while the agent works.** Streaming reasoning, inline tool cards with friendly labels (*"Searching the web for …"*, *"Creating tap …"*), live success/error status, and a Stop button that aborts the loop at the next checkpoint. Conversations survive navigating to other tabs and back.
* **Credentials never enter the chat.** When the agent needs an API key or other credentials, it opens an inline credentials form right in the chat. Values go straight to the vault — they don't appear in the conversation log, your screenshots, or the model's context. The form also lets you reuse an existing tap secret instead of creating a new one.
* **Pipelines can be edited in place.** Calling pipeline-create again with the same name now upserts the configuration without dropping the destination data. Two new knobs in the same call: a natural-key list for dedupe/upsert on every run, and a flag to wipe the destination before each run for full-snapshot workflows. Both work on PostgreSQL and MongoDB destinations.
* **External agents get the same new tools.** Claude Desktop, Cursor, and any other MCP client connected to your Datris server now see two new tools for discovering existing tap secrets (names and field shapes, never values) plus the new dedupe and reset knobs on pipeline create.
* **Pipeline delete now describes what it actually does.** The tool description used to claim it kept your destination data; it never has. It now correctly says it removes both the configuration and the destination data, with an explicit opt-in flag for the "keep the schema, clear the data" reset case.
* **MCP transport upgraded — both protocols at once.** The bundled MCP server now serves the streamable-HTTP transport alongside the existing SSE transport on the same port, so the in-product Assistant and external agents connect to one running process. No configuration change needed.
* **Discovery tab is hidden in this release.** The Assistant supersedes the Discovery tab for the common path.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed. The CLI: `brew upgrade datris`. The Assistant uses your existing codegen AI configuration — with Anthropic you'll see full chain-of-thought reasoning streamed inline; on OpenAI you'll see reasoning summaries instead.
**Web search for AI tap workflows, plus simpler MCP authentication.**
* **AI tap workflows can consult the live web.** When enabled in Configuration → AI Providers, tap brainstorm, dataset discovery, tap diagnosis, and tap auto-fix look up current API documentation, free-tier limits, current package names, and recent deprecation notices before recommending sources or generating fixes. Pick your web-search provider independently of AI Primary — the platform uses each provider's native search tool and routes accordingly. First-pass tap script generation stays fast and uses the model's training data only. See [AI Configuration](/ai-configuration) for setup.
* **AI Configuration changes survive Docker restarts.** Saving from the Configuration UI now mirrors the relevant keys back to your `.env` file so changes aren't lost when the local Vault container restarts. Provider switches also clear any stale credential preserved from the prior provider, so a wrong-provider key can't silently 401 the next call.
* **Cleaner MCP authentication.** The bundled MCP server is now a transparent forwarder — each connecting agent provides its own API key per session and the MCP server passes it through to the Datris REST API on every tool call. The Configuration → Connect Your Agent panel generates the new configuration snippet automatically; paste your key from the Configuration UI into your agent's MCP config. Existing remote-instance setups see no change in behavior.
* **Tap brainstorm asks about sources first.** When you describe data without naming where to fetch it from, the AI now lists 3-5 candidate sources (with free vs paid and key-required info) before drilling into parameters — instead of asking for filtering details up front.
* **Tap script generation is more resilient.** The platform now validates that a generated script actually defines a `fetch()` function before storing it, and the JSON extractor handles model responses that contain narrative braces without falling back to the raw-text path.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed. The CLI: `brew upgrade datris`. If you run the MCP server standalone outside Docker, the connection-target environment variable was renamed for consistency — see the [MCP server docs](/mcp-server) for the new variable name.
**Reliable job-status polling, re-ingest that doesn't overwrite your config, and first-class catalog tooling.**
* **Polling an upload's job status now gives you a clear answer.** Previously, when a file finished processing successfully, the response was a raw stream of progress events with no terminal status — agents and scripts couldn't tell "still running" from "done." Job status now returns a rollup with a single `allDone` flag and an aggregate outcome (`success`, `warning`, `error`), plus per-job error detail when something fails. Poll the rollup; act on the outcome.
* **Re-ingesting a file preserves your pipeline's config.** `datris ingest` against an existing pipeline used to silently rewrite the config from CLI flags only — wiping out the catalog, custom validation rules, and any other fields you'd set through the UI or via an agent. Re-ingest now uploads into the existing pipeline as-is. To start over with a different config, delete the pipeline first.
* **Catalogs without the read-modify-write dance.** New `--catalog` flag on `datris ingest` for new pipelines, new optional `catalog` argument on the `create_pipeline` MCP tool, and a new `set_catalog` MCP tool that retags an existing pipeline or tap in one call. Empty catalog clears the label back to Uncataloged. See [Data Catalog](/data-catalog).
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed. The CLI: `brew upgrade datris`.
**Choose your embedding provider independently of your chat provider.**
* **Mix-and-match AI providers.** The embedding slot is now configured separately from the chat and code-generation slots, so you can keep Claude for chat and code generation while pointing embeddings at OpenAI (or vice-versa). Useful when the bundled embedder is too heavy for your host, or when you want a different model family for vector quality vs chat quality. See [AI Configuration](/ai-configuration) for the full list of options.
* **Existing installs keep their current behavior.** If you don't set an embedding override, the embedding slot continues to follow your chat provider exactly as before — Claude installs keep using the bundled embedder, OpenAI installs keep using OpenAI embeddings. The override is purely opt-in.
**Upgrading:** `docker compose pull && docker compose up -d --remove-orphans`. No data migration needed. If you switch embedding providers on an existing deployment, vector destinations built on the previous embedder will fail-fast with a dimension-mismatch message on the next run — drop the affected destination tables or collections and re-ingest.
**User authentication, roles, and an admin-only Configuration tab.**
* **Optional username/password login.** Datris can now require a login before any tab is reachable. Three roles ship out of the box — **admin** (full access), **editor** (read + edit pipelines, taps, secrets), and **viewer** (read-only). Off by default, so existing installs are unaffected. See the new [User Authentication](/user-auth) doc to enable it.
* **Configuration is admin-only.** When auth is on, only admins see the Configuration tab (Secrets, AI Providers, Taps, Users, Environment). Editors and viewers continue to use everything else.
* **New Users sub-tab.** Admins can add, remove, and reassign roles. A built-in 16-character password generator with a reveal toggle makes handing out credentials painless. The last admin can't be deleted.
* **Self-service password change.** Users can change their own password from the top-right user menu.
* **Reveal toggle on the login screen.** Easier to see what you're typing on a new device.
* **Clear the Agents activity log.** The trash icon now wipes the server-side activity buffer (with an inline confirm) so the cleared state survives a refresh.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed; auth defaults to off.
**AI-agent RAG ingestion no longer burns through the conversation context.**
* **Creating a vector-store pipeline no longer requires sample content.** Asking an agent to ingest a PDF into pgvector previously forced it to base64-encode the entire document just to register the pipeline — wasting tens of thousands of tokens before any work began. Vector pipelines now register from a name plus destination alone, freeing budget for the actual upload.
* **Agents are guided to send each document in a single upload.** The MCP server now makes explicit that vector destinations chunk server-side, preventing agents from needlessly splitting documents into many small uploads.
* **Clearer Claude setup docs.** The "Configuring Claude" guide shows the recommended SSE / mcp-remote setup first, and steers large-file ingestion to the CLI rather than dragging files directly into the chat — which can overflow the conversation context on sizable PDFs.
* **README accuracy pass.** Corrected tool counts, AI-provider model defaults, and license badge.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed.
**UI cleanup, friendlier Help menu, and a more reliable bundled embedding service.**
* **Bundled embedding handles large ingest batches without errors.** The bundled embedding service no longer rejects large batches submitted by the platform, which previously surfaced as ingestion failures on long documents.
* **Secrets is now a tab inside Configuration.** Instead of a separate top-level tab, Secrets lives under Configuration alongside AI Providers, Taps, and Environment. Existing `/secrets` links continue to work — they redirect to the new location.
* **Help menu in the top bar.** The Docs link is replaced with a Help dropdown that exposes both the docs and a direct link to file an issue on GitHub.
* **Easier setup with Claude.** A new "Configuring Claude" page in the docs walks through Claude Desktop and Claude Code setup end-to-end, including a first-prompts walkthrough for an empty install.
* **Structured issue reporting.** GitHub issues now use forms that capture version, component, deployment mode, and reproduction steps, making bug reports easier to triage and faster to fix.
**Upgrading:** `docker compose pull && docker compose up -d`. No data migration needed.
**Smaller, faster default install. Lighter download, opt-in Kafka, vector ingestion fixes.**
* **\~58% smaller `docker compose pull`.** The bundled platform now downloads roughly 11 GB less out of the box. Fresh installs come up dramatically faster.
* **Bundled embeddings are faster on the same `bge-m3` model.** No configuration changes needed; existing vector collections built with the previous bundled embedder continue to work without re-embedding.
* **Kafka is now opt-in.** Most local installs don't need it, so Kafka, Zookeeper, and the Kafka UI ship commented out in `docker-compose.yml`. Uncomment the bundled blocks (and the related volumes at the top of the file) to enable them. Pipelines that point at external Kafka brokers are unaffected.
* **Vector ingestion no longer fails with a "duplicate key" error when many documents land at once.** Concurrent document loaders previously raced on creating the `pgvector` extension; the race is now serialized.
* **Vector ingestion no longer fails on embedding providers that limit batch size.** The chunk batch size is now configurable per embedding secret (`batchSize`), with a cross-provider-safe default. OpenAI users who want to maximize throughput can set this higher.
* **Configuration tab clarifies optional providers.** The bundled embedding option is labeled `bge-m3 (bundled)`. The AI Provider, CodeGen Provider, and Embedding Provider dropdowns each indicate that local Ollama is opt-in.
* **Service Health no longer shows "Down" for optional services that were never enabled.** Kafka and the optional vector databases (Qdrant, Weaviate, Milvus, Chroma) now correctly report "Not Configured" until you turn them on. Existing installs may still show "Down" until their stale Vault secrets are removed.
**Upgrading from earlier versions:** the bundled embedding service moved off Ollama on the same host port `11434`, so existing installs must pass `--remove-orphans` to release the port: `docker compose pull && docker compose up -d --remove-orphans`. Without it the upgrade fails with `Bind for 0.0.0.0:11434 failed: port is already allocated`. Your data and the cached Ollama model are preserved.
**Reliable run-completion signals, smarter agent calls, cleaner run history.**
* **Agents get a single "are we done?" signal when watching a pipeline load.** Polling a publisher token returns a rollup with a clear `allDone` boolean and a per-job outcome (success / warning / error / processing / timed out), so agents no longer have to interpret the raw event stream to figure out whether a run is complete.
* **Pipeline status by publisher token works reliably for completed runs.** A storage path that occasionally hid completed runs from the publisher-token query is fixed; the query is now backed by an indexed top-level field, with a fallback for older rows so existing data resolves without a migration.
* **`run_tap` no longer ships the records array back to the agent.** A push run returns `recordCount`, `publisherToken`, and the `persisted` / `persistedReason` flags — enough to verify ingestion via `get_pipeline_status` without bloating the agent's context. Use `test_tap` to preview a script's output (capped at 20 sample rows with a `recordsTruncated` flag).
* **Duplicate `run_tap` calls are suppressed.** The agent skips a `run_tap` for a tap that's already in flight in the same session, and the platform debounces push runs to one per tap per 5 seconds. Prevents accidental duplicate ingestion from parallel tool calls, double-clicks, and transport retries (`persistedReason: already_running` or `debounced`).
* **Every tap run now produces visible logs.** The script wrapper emits start / fetch / record-count lifecycle lines on every run, so run history shows useful output even when the user's script never calls `print()`.
* **Secret values are masked in stored tap logs and exception messages.** A tap script that incidentally printed an API key or Vault-loaded credential would previously have surfaced the raw value in run history; those values are now redacted in the persisted log and the error string.
* **Deleting a tap now cleans up its run history.** Previously, run-history rows accumulated indefinitely and could resurface under a recreated tap with the same name.
* **Agent activity log shows full requests and responses.** The expanded view in the agent monitor no longer truncates request arguments or response bodies.
**MCP tab and documentation sync.**
* **MCP tab now lists every agent tool.** The in-app MCP reference was missing `get_pipeline_status`, `create_tap_secret`, `delete_tap_secret`, `get_tap_ledger`, and `query_natural`. All five are now in the catalog and the Try-It playground, and `create_tap` exposes the `tap_type` parameter.
* **Recommended Agent Workflow rewritten to match the platform's actual flow.** The in-app workflow and the MCP docs had drifted — they started with `profile_data` (which the platform explicitly says not to use for pipeline generation), skipped the `persisted` / `persistedReason` check after `run_tap`, and didn't show the `publisherToken` poll that confirms records actually landed. All three are now canonical: check-before-create, verify-via-publisher-token, and tap credentials managed via `create_tap_secret`.
* **Documentation: updated agent workflow examples.** RAG over external documents is now shown as a document tap (`tap_type="document"` + `get_tap_ledger`), onboarding an external source uses `create_tap_secret` for credentials, and quality monitoring of scheduled taps shows the `get_tap_logs` → `get_pipeline_status(publisher_token=...)` pivot.
**Tap wizard reliability, iteration history, and cleaner vector-search errors.**
* **Tap wizards learn from their own retries.** When the AI fixes, optimizes, or reviews a tap script, it now carries forward up to the last three attempts — what was tried, what went wrong, and what changed — into the next call. The wizard stops cycling through the same failed approaches.
* **Saved tap scripts always match the tap.** Saving a tap now pushes the in-memory script to object storage before writing the tap config, and the create/update call verifies the stored script is actually there. No more "missing script" banners from an interrupted save, and auto-revert no longer strands a tap with a deleted script.
* **Run Tap stays on the page when nothing was ingested.** If a manual run finishes without persisting records, the wizard keeps you on the run step and shows an inline reason (test mode, no records, run error) instead of navigating away and hiding the diagnostic.
* **Tap logs now carry the publisher token.** Every tap run that submitted records records its publisher token in the log. Agents reading `get_tap_logs` can pivot directly to `get_pipeline_status` to confirm a scheduled run actually landed in the destination — not just that the script ran.
* **Vector search fails cleanly when the embedding dimension doesn't match the collection.** If you change embedding providers on a pipeline whose vector collection already has vectors of a different dimension, search queries now return a clear 400 with a user-actionable message instead of leaking a JVM stack trace.
* **Sturdier local-dev startup.** Kafka and Zookeeper now use named volumes (no more corruption races on rebuild), Kafka waits for Zookeeper's request processor to actually be ready (not just its listener bound), and Vault init picks up an explicit AI-provider override so a stray shell env var can't silently flip providers.
**Agent-native tap observability, scheduler fix, and agent-owned tap secrets.**
* **Agents can watch a tap load to completion.** Running a tap now reports back whether the data was actually persisted — and names the reason when it wasn't (test mode, no target pipeline, no records, run error). Every persisted run returns a single *publisher token* covering the whole run, even for document taps that spawn many ingestion jobs. A new `get_pipeline_status` MCP tool lets an agent poll that one token until the entire load reaches its final state, so it can report "done" with real numbers instead of guessing from a response body.
* **Scheduled taps no longer need a manual kickoff.** Taps saved with a cron schedule now fire on their next scheduled time automatically. Previously, a newly saved scheduled tap would wait indefinitely until you ran it once by hand.
* **Self-diagnosing tap scripts.** If a tap's generated script goes missing from object storage, the Edit Tap page now shows an amber banner explaining the state and pointing you to Regenerate, instead of a cryptic mid-run "key does not exist" error. Test Tap surfaces the same state with actionable wording.
* **Agents can manage their own tap secrets.** Via MCP, agents can now create and delete the secrets their taps need (API keys, tokens). Scope is strictly tap-owned — agents cannot create, overwrite, or delete human-owned Platform secrets (DB creds, AI keys, vector-store creds).
* **Secrets page split into Platform and Taps sub-tabs.** Platform lists the built-in Datris secret slots; Taps lists agent-authored tap secrets. Creating a secret from the Taps sub-tab auto-tags it so it stays agent-editable.
* **Honest Test Tap banner.** The run-result banner on Test Tap now reflects what actually happened on the server — "sent to pipeline" only when the run was truly persisted, otherwise "not persisted" with the reason — rather than whatever the pre-request checkbox said.
* **BYO-code taps can declare pip dependencies.** If you paste your own fetch script into Create Tap, you can now list the Python packages it needs. Previously only AI-generated taps could declare dependencies.
* **Example agent refactored onto taps.** The bundled market-macro-agent example now drives ingestion through taps instead of ad-hoc fetch scripts, demonstrating the full agent-native tap flow (provisioning, secrets, publisher-token watching).
**Tap prompt fragments, post-run script review, BYO code, and Configuration page reorg.**
* **Tap prompt fragments (new).** A new Configuration → Taps sub-tab lets you save reusable context snippets — things like API conventions, required headers, rate limits, and preferred libraries for a given source. When the key or any of its aliases appears in a Create Tap description, brainstorm, auto-fix, optimize, or Discovery chat, the fragment's content is automatically added to the system prompt. Includes an AI Suggest button, a Load Examples catalog (AWS, Polygon, Stripe, SEC EDGAR), JSON import/export, and an "Extra context applied" chip row in the tap wizard showing which fragments hit.
* **Post-run script review.** After a tap's first successful test, the AI now scans the captured stderr/stdout for signals that the *script* should change — rate-limit or burst warnings, deprecation hints, pagination cues, schema drift, auth warnings — and regenerates the script if needed. On a rewrite the wizard auto-retests; the performance optimizer runs only when the logs are clean. The optimizer's prompt was also tightened so rate-limit markers push it toward throttling instead of more concurrency.
* **I Have My Own Code (new).** A third Tap Type on the Create Tap wizard lets you paste a fetch() script directly instead of having AI generate one. Step 1 switches to a code textarea with a Use My Code button; after upload the button flips to Re-upload My Code and Step 2 gates on the text matching what's on disk, so edits force a fresh upload before running the test.
* **Configuration page reorganized into three sub-tabs** — Environment, AI Providers, and Taps — with a prominent "Highly recommended: Anthropic with the latest coding model" tip on the CodeGen Provider section.
* **Tap Name collision warning.** If the name you type in the Create Tap wizard matches an existing tap, an amber banner appears under the field warning that continuing will overwrite the existing tap's configuration and script.
* **Auto-fix retries bumped to 3.** When a tap script fails its first test, the AI now gets up to three repair attempts (was two) before giving up.
* **Cron Custom preset no longer blocked by AI formatting.** AI-generated cron expressions wrapped in code fences, brackets, or quotes are now cleaned automatically, so the Next button is enabled on valid output.
* **OpenAI Codex models now work for code generation.** Previously, selecting a codex-family model for CodeGen (e.g., the recommended GPT-5.3-Codex) caused tap-script generation and AI data quality / transformation to fail immediately with a 404. Datris now routes codex models to the right OpenAI endpoint automatically.
* **Vector-store dimension changes now fail fast with a clear message.** If you switch embedding providers (for example, Ollama bge-m3 → OpenAI text-embedding-3-small) on a pipeline whose destination table or collection already has vectors of the old dimension, the job stops up front and tells you exactly what to do instead of blowing up mid-ingest with a cryptic database error. Applies to pgvector, Qdrant, Weaviate, Milvus, and Chroma destinations.
* **Configuration save is honest about missing API keys.** Changing the AI Provider to Anthropic or OpenAI without entering that provider's API key no longer silently skips the save while reporting success — the Configuration page now flags the missing key and tells you which one to add.
* **Create Tap brainstorm wraps up sooner.** For document taps, the AI assistant no longer drills for optional date filters once you've supplied the source, auth, and a broad scope — the tap ledger already dedupes by content, so those extra questions were just noise.
* **OpenAI GPT-5 and reasoning-model support.** AI-backed features (Discovery chat, Create Tap brainstorm, AI data quality, AI transformation, script generation) now work with OpenAI's newer model families. Previously, selecting one of these models caused chat panels and wizards to hang without a response.
**Document Taps — a new tap type for feeding vector stores with unstructured files.**
* **Document Taps (new).** A purpose-built tap for ingesting PDFs, Word docs, HTML, and other unstructured files into a vector-store pipeline. Describe the source in plain English ("ingest all PDFs from our SharePoint legal folder", "pull every DOCX from `legal-contracts/2026/` in S3") and the generated tap discovers the files and hands their raw bytes to the pipeline. Text extraction, chunking, embedding, and loading are all handled downstream — you don't configure any of that on the tap.
* **Tap type toggle in Create Tap.** Choose *Document Ingestion* or *Structured/Semi-Structured* on the first step of the wizard; the prompts, placeholders, and example instructions adapt to the choice.
* **Ingestion ledger.** Every discovered file is tracked by URI and content hash. Re-running the tap skips files that are already up to date — no re-embedding, no duplicates. Changed files flow through normally.
* **Pre-flight validation.** Document taps linked to a pipeline are checked at save time to confirm the pipeline is shaped for document ingestion (unstructured source, vector-store destination). Misconfigurations surface as an actionable error instead of a cryptic mid-run failure.
* **Safe defaults for local paths.** Document taps refuse to silently walk arbitrary host directories if a requested path isn't mounted into the container — they fail loudly instead of ingesting unintended files.
* **Faster embeddings.** The bundled Ollama sidecar now handles concurrent embedding requests in parallel and keeps the embedding model warm between pipeline runs, eliminating cold-start delays when a pipeline resumes after an idle period.
* **More Python libraries pre-installed for taps.** AWS S3, Google Cloud Storage, Azure Blob, Excel, YAML, and date/timezone helpers are now baked into the image. Taps that fetch from these sources no longer need a per-run `pip install`.
**Live MCP agent monitor and pipeline status self-healing.**
* **Agents tab** — new live view of connected MCP agents. See every tool call as it happens, with agent name, arguments, record count, response size, status, and latency. Click any row to expand the full request and response.
* **Pipeline status** now self-heals when a job completes but the summary gets stuck showing "processing" — completed, warned, and errored jobs resolve to their correct final state.
* **Example agent** (market-macro-agent) automatically reconnects with backoff if the MCP connection drops, and degrades gracefully while offline instead of crashing.
* New AI models from OpenAI and Anthropic now appear in the Configuration dropdowns automatically, without a Datris upgrade.
* **Claude Opus 4.7** is the recommended Anthropic model for CodeGen (tap script generation, AI data quality rules, AI transformations, JSON Schema / XSD generation, and natural-language → SQL).
* Fresh instances are seeded with the latest recommended models by default.
**Two-pass AI tap optimization and Data Catalog UX.**
* **AI optimize pass.** After a tap script passes its initial test, the platform sends it back to the LLM for a performance rewrite and re-tests. Auto-reverts on regression (>=20% slower) or failure. Also available through the API; tap test results now report run duration.
* **Discovery wizard.** Auto-optimize per tap with before/after timing banner, per-row stop button, on-demand AI fix panel for failed items, chat auto-scroll, and re-shown Discover Datasets button after new messages.
* **Create Tap.** Configurable test sample size (defaults to 20 records).
* **Data Catalog.** Kebab menu on Uncataloged items with Edit, Delete, and Move to Catalog (with name-clash detection).
**Database lockdown, tap script hardening, Create Tap UX.**
* **Server-controlled database name.** The UI no longer edits the Postgres or MongoDB database name — the server reports the configured names and the UI submits them unchanged.
* **MongoDB internal vs user split.** `mongodb.database` (user-facing, default `datris`) holds pipeline data; `mongodb.internalDatabase` (default `oss`) holds platform state. Existing installs keep `oss` for platform state; new user pipelines land in `datris`.
* **Unlimited reads for tap scripts.** Tap scripts can now request every matching row from the MongoDB and Postgres query APIs instead of a capped preview. Preview defaults (20 Mongo / 100 Postgres) are unchanged for UI/MCP callers.
* **Tap test sampling.** New "Limit test sample to 20 records" checkbox in Create Tap step 2 caps how much a test run reads. Cron and manual runs read everything.
* **Codegen + diagnosis hardening.** Generated scripts treat platform response shapes as contractual (no shape-probing, no candidate-key iteration). Diagnosis quotes the actual traceback and respects in-script guards.
* **Create Tap UX.** Ask button, auto-apply diagnosis (capped at 2 attempts), Stop Test, copy-to-clipboard, scrollable-JSON test results, destination collision check on Generate Pipeline, full destination shown on step 5.
* **Data Catalog.** One-click delete of the Uncataloged group; per-item trash icon on taps and pipelines inside every catalog.
**Input sanitization hardening and minor UI polish.**
* Pipeline creation sanitizes destination identifiers before writing the config — Postgres `dbName`/`schema`/`table`, MongoDB `dbName`/`table`, Kafka `topic`, ActiveMQ `queueName`, vector `collectionName`/`tableName`/`schemaName`, and the DQ schema name.
* Name sanitization is now applied consistently across the UI, so labels and identifiers are cleaned the same way everywhere.
**Discovery wizard, Data Catalog, and bring-your-own AI keys.**
* **Discovery wizard.** Six-step AI-guided onboarding that turns "yfinance daily prices for the S\&P 500" into running taps and pipelines. Available from the UI, the API, and the new `discover_source` MCP tool.
* **Data Catalog.** Group related taps and pipelines into named catalogs. New Data Catalog tab with expandable contents and an Uncataloged group.
* **Per-pipeline DQ + transformation editor.** Inline editor inside the pipeline view.
* **Bring-your-own AI keys.** Hosted trial signups could supply their own Anthropic or OpenAI key, which took effect on the next AI call (hosted platform since retired).
**Hosted platform release.**
* Hosted dedicated-instance provisioning and hosted-aware Configuration UI (since retired).
* Improved multi-user session handling.
**Full tap MCP tool suite and user-supplied tap scripts.**
* Four new MCP tools: `get_tap`, `test_tap`, `update_tap`, `get_tap_logs`.
* `create_tap` accepts an optional `script` parameter for user-supplied Python `fetch()`, plus `secret_name` for Vault-injected credentials.
* CLI: `datris tap create --script path/to/script.py` and new `datris tap show`.
**Ollama for all AI slots and hot-reload on save.**
* Configuration UI offers Ollama (local) as a provider for AI Primary, CodeGen, and Embedding. Bundled Ollama sidecar option pre-fills `bge-m3` for embedding.
* Saving AI configuration takes effect immediately — no container restart required.
**Hosted trial hardening.**
* Hosted trial cost tuning and Configuration tab polish (since retired). Self-hosted installs continue to recommend Opus for codegen on their own keys.
**Breaking: AI configuration restructured into three independent Vault secrets.**
* `ai.aiSecretName` and `ai.provider` are replaced by three top-level slots — `ai.aiPrimary.secretName`, `ai.codegen.secretName`, `ai.embedding.secretName`.
* Each Vault secret is self-describing (`provider`, `endpoint`, `model`, `apiKey`, optionally `version`). No path derivation from YAML.
* v1.5.x deployments must update their server configuration and re-seed Vault on upgrade.
**Onboarding and tap scheduling UX.**
* **Getting Started tab** as the new first-run landing page; Docs ↗ top-nav link.
* Inline cron editor on the Taps page: preset buttons, AI prompt field ("Every weekday at 4pm"), Quartz field validation, human-readable description.
* Truncate-before-run toggle in the tap wizard (sets `truncateBeforeWrite: true` on the generated destination).
* Hosted trial bring-your-own AI key banner on the Configuration tab (since retired).
* **Fix:** scheduled taps now honor the configured date timezone.
**Taps (Beta).** AI-generated Python scripts that fetch data on a schedule and stream it into a pipeline.
* 4-step tap creation wizard (Describe → Edit & Test → Schedule → Review) with brainstorm chat and proactive env-var suggestions.
* AI script generation with JSON-parse retry and raw-script fallback; AI diagnosis with explicit (a)/(b)/(c) options; "Apply Diagnosis" rewrites the script in place.
* Tap secrets stored in Vault and tagged as tap-owned; per-run history is retained and viewable.
* CRON scheduling (Quartz format), AI-generated from natural language.
* JSON-to-CSV pipeline feed (union of keys) with column-name normalization via spell-out table (`%` → `percent`, `#` → `num`, `&` → `and`, …).
* Pipelines page shows the feeding tap; pipeline wizard has "Create from Tap".
**Schema Evolution.**
* Additive evolution: new CSV columns are auto-added as `string`, `schemaVersion` increments, and `ALTER TABLE` runs on Postgres.
* Dropped columns are left out of the Postgres load so they default to `NULL` (previously failed on typed columns).
* Missing key fields raise a clear error.
* Schema evolution now behaves identically for streamed and file-based ingestion.
* Query results now include `NULL` columns.
**Remote MCP endpoint.**
* Per-session API key forwarding on the MCP server, with an option to reject unauthenticated remote connections.
* Hosted trial signup and dedicated-upgrade agent tools and remote registry entry (since retired).
* Hosted trial instances: pipeline wizard auto-populated database names (since retired).
**Shared-instance hosting.**
* Hosted multi-tenant isolation on a shared instance (since retired).
* Vector DB secret isolation across Qdrant, Weaviate, Milvus, Chroma, pgvector.
* Batch upload for compressed files (`.zip`, `.gz`, `.tar`, `.jar`) processes contents inline — no MinIO webhook dependency.
* Default AI provider changed to Anthropic (Claude Sonnet 4.6). Customers can still switch to OpenAI or Ollama in configuration.
* Generated Python scripts for DQ and transformation are logged in full to the server logs for debugging.
* New unified `datris analyze` command replaces `ask-sql` and `ask`. Auto-picks the right approach based on `--dest` (Postgres → SQL generation, MongoDB → document fetch, vector stores → RAG).
* `--ai-analyze` flag on ingest.
**Mintlify documentation and accuracy pass.**
* Migrated docs from `.md` to `.mdx` with a two-tab Mintlify layout.
* Removed deprecated docs (row rules, column rules, JavaScript row functions, REST transformations, deduplication, column trimming).
* Page-by-page accuracy review against the codebase.
* Server fails fast at startup with a clear error if an AI provider is not configured (CodeGen DQ and transformation require it).
* Removed stale NEVER rules and deprecated-feature references from the MCP server instructions.
**CodeGen DQ and transformation.**
* `aiRule` replaces the prior AI DQ approach: the LLM generates a self-contained Python validation script from a plain-English instruction, which runs locally. Cost drops from $25–40/file to ~$0.003/rule.
* `aiTransformation` uses the same CodeGen approach.
* Works for CSV, JSON, and XML.
* **Removed:** `columnRules` (regex), JavaScript row rules, REST endpoint row rules, and the legacy AI data-quality path. JavaScript row functions and REST transformations removed from UI and CLI.
* **Atomic `create_pipeline`.** `generate_schema` is removed; `create_pipeline` accepts base64-encoded sample data, auto-detects schema, and creates the pipeline in one call.
* Content-based uploads across `upload_data` (renamed from `upload_file`), `profile_data`, `upload_config`.
* `update_secret` MCP tool for AI provider keys only.
* All-string schema on MCP pipeline creation; pipeline registration verified by read-back.
* `update_secret` MCP tool added (scoped to AI secrets: anthropic, openai, ollama, embedding).
* Published to the MCP Registry (`io.github.datris/datris`) and PyPI (`datris-mcp-server`).
**New UI tabs.**
* **MCP Tab** — agent view, service health for 10 backend services, browsable tool grid, config generator for Claude Desktop/Code and Cursor, and a Tool Playground that executes against the live API.
* **Secrets Tab** — full CRUD for Vault secrets with sensitive-field masking.
* New REST endpoints for vector store metadata discovery (Qdrant collections and friends).
# Datris CLI
Source: https://docs.datris.ai/cli
Command-line interface reference
The Datris CLI (`datris`) is a command-line interface for the Datris Data Platform. It communicates with the platform via the MCP server, providing a simple way to ingest data, run queries, search vector stores, and manage pipelines — all from the terminal.
## Installation
```bash theme={null}
brew tap datris/tap
brew install datris
```
## Configuration
The CLI connects to the MCP server via SSE. Set the server URL with an environment variable:
```bash theme={null}
export MCP_SERVER_URL=http://localhost:3000/sse # default
```
### Authentication
When `USE_API_KEYS=false` (the OSS default) no key is needed. When [`USE_API_KEYS=true`](/api-keys), export the key and the CLI sends it as the `x-api-key` header on every request to the MCP server:
```bash theme={null}
export DATRIS_API_KEY=
```
If the key is missing or wrong, commands stop with a one-line message telling you to set `DATRIS_API_KEY`.
Older CLI versions do not read `DATRIS_API_KEY`. For those, put the key on the MCP URL's query string instead, which the MCP server still accepts: `export MCP_SERVER_URL="http://localhost:3000/sse?api_key="`.
Issue a key dedicated to the CLI from the UI: **Configuration → API-Keys → Issue new key**. See [API Keys](/api-keys) for the full pattern.
## JSON Output
Every command supports `--json` to return raw JSON instead of human-readable output. This is useful for scripting and programmatic use.
```bash theme={null}
datris pipelines --json
datris query "SELECT * FROM trades" --json
datris analyze "top 5 stocks" --table trades --json
datris health --json
```
## Commands
### `datris help`
Show all available commands and options.
```bash theme={null}
datris --help
```
***
### `datris pipelines`
List all registered pipelines.
```bash theme={null}
datris pipelines
```
***
### `datris ingest`
Create a pipeline and ingest a data file in one step. The schema is auto-detected from the file.
If the named pipeline already exists, the existing config is preserved and the file is uploaded into it — `--dest`, `--table`, `--ai-validate`, `--ai-transform`, and `--catalog` are ignored on re-ingest. Delete the pipeline first (`datris delete `) if you need a fresh config.
```bash theme={null}
datris ingest [OPTIONS]
```
| Option | Description |
| - | - |
| `--pipeline, -p` | Pipeline name (default: derived from filename) |
| `--dest, -d` | Destination: `postgres`, `mongodb`, `snowflake`, `databricks`, `qdrant`, `weaviate`, `milvus`, `chroma`, `pgvector` (default: `postgres`) |
| `--table, -t` | Table/collection name (default: pipeline name) |
| `--database` | Database name (default: `datris`). For `snowflake`: the Snowflake database; for `databricks`: the Unity Catalog name — required for both |
| `--schema` | Destination schema (`snowflake` default: `PUBLIC`; `databricks` default: `default`) |
| `--warehouse` | Snowflake warehouse name, or Databricks SQL warehouse ID (required for Snowflake; for Databricks, optional when the secret has a `warehouse` field) |
| `--credentials-secret` | Platform secret holding destination credentials (required for `snowflake` and `databricks`) |
| `--ai-validate` | AI data quality rule — plain-English instruction |
| `--ai-transform` | AI transformation instruction — plain-English instruction |
| `--ai-analyze` | Ask a question about the data after ingestion completes |
| `--catalog` | Catalog label to group this pipeline with related pipelines (e.g. `openclaw`). Free-form — no need to pre-create. Only applied when creating a new pipeline. |
| `--json` | Return raw JSON |
**Examples:**
```bash theme={null}
# Basic ingestion — auto-derives pipeline name from filename
datris ingest sales-data.csv --dest postgres
# With explicit pipeline name
datris ingest sales-data.csv --pipeline sales --dest postgres
# With AI validation
datris ingest trades.csv --dest postgres --ai-validate "all prices must be positive and dates must be YYYY-MM-DD"
# With AI transformation
datris ingest trades.csv --dest postgres --ai-transform "convert all date columns to YYYY/MM/DD format"
# Both validation and transformation
datris ingest trades.csv --dest postgres \
--ai-validate "all prices must be positive" \
--ai-transform "convert dates to YYYY/MM/DD and uppercase all ticker symbols"
# Ingest into MongoDB
datris ingest events.json --dest mongodb --database analytics
# Ingest into Snowflake (credentials come from a Platform secret)
datris ingest trades.csv --dest snowflake --database ANALYTICS \
--warehouse COMPUTE_WH --credentials-secret snowflake-creds
# Ingest into a vector store for RAG
datris ingest manual.pdf --dest pgvector
# Group related pipelines under a catalog
datris ingest legal-2026.pdf --pipeline legal_2026 --dest pgvector --catalog legal
# Ingest + analyze in one command
datris ingest trades.csv --dest postgres --ai-analyze "What are the top 5 stocks by volume?"
# Ingest a document and ask a question about it
datris ingest annual-report.pdf --dest pgvector --ai-analyze "What was the company's revenue?"
# Ingest + analyze, raw JSON output for scripts
datris ingest trades.csv --dest postgres --ai-analyze "top 5 by volume" --json
```
***
### `datris query`
Execute a read-only SQL SELECT query against PostgreSQL.
```bash theme={null}
datris query "SELECT * FROM public.sales LIMIT 10"
datris query "SELECT symbol, close FROM public.trades WHERE close > 100" --limit 50
```
| Option | Description |
| - | - |
| `--limit` | Max rows returned (default: 100; pass `-1` for no limit) |
| `--json` | Return raw JSON |
***
### `datris search`
Semantic search across a vector database.
```bash theme={null}
datris search "What is the return policy?" --store pgvector --collection support_docs
```
| Option | Description |
| - | - |
| `--store`, `-s` | Vector store: `qdrant`, `weaviate`, `milvus`, `chroma`, `pgvector` (default: `pgvector`) |
| `--collection`, `-c` | Collection/table name (required) |
| `--top-k`, `-k` | Number of results (default: 5) |
| `--json` | Return raw JSON |
***
### `datris analyze`
Ask a question about your data using AI. Works with any destination type — auto-picks the right approach based on `--dest`.
```bash theme={null}
datris analyze --table
[OPTIONS]
```
| Option | Description |
| - | - |
| `--table, -t` | Table/collection name (required) |
| `--dest, -d` | Data source type: `postgres`, `mongodb`, `snowflake`, `databricks`, `qdrant`, `weaviate`, `milvus`, `chroma`, `pgvector` (default: `postgres`) |
| `--pipeline, -p` | Pipeline name (required for `snowflake`/`databricks` — the pipeline selects the connection) |
| `--top-k, -k` | Number of search results for vector stores (default: 5) |
| `--json` | Return raw JSON instead of AI narrative |
**Examples:**
```bash theme={null}
# Analyze PostgreSQL data — AI generates SQL, executes it, returns AI answer
datris analyze "What are the top 5 stocks by volume?" --table trades
# Analyze MongoDB data
datris analyze "How many events occurred in March?" --table events --dest mongodb
# Analyze a Snowflake or Databricks destination — pipeline selects the connection
datris analyze "Which region had the most sales?" --table sales --dest snowflake --pipeline sales
# Analyze vector store data (RAG) — semantic search + AI answer
datris analyze "What is the return policy?" --table support_docs --dest pgvector
datris analyze "What was quarterly revenue?" --table financial_docs --dest qdrant
# Raw JSON output for scripts
datris analyze "top 5 stocks" --table trades --json
```
**How it works by destination:**
* **PostgreSQL** — AI generates a SQL query from your question, executes it, then summarizes the results in a natural language answer
* **MongoDB** — fetches documents from the collection, then AI answers the question based on the data
* **Snowflake / Databricks** — previews rows from the pipeline's destination table (connection and credentials resolved server-side from the pipeline config), then AI answers the question based on the data
* **Vector stores** — performs semantic search to find relevant chunks, then AI generates an answer from the retrieved context
***
### `datris query-mongo`
Query a MongoDB collection with optional filter and projection.
```bash theme={null}
datris query-mongo events
datris query-mongo events --filter '{"status": "active"}' --limit 20
datris query-mongo events --projection '{"name": 1, "status": 1}'
```
| Option | Description |
| - | - |
| `--filter`, `-f` | MongoDB filter JSON (default: `{}`) |
| `--projection` | Fields to include/exclude |
| `--limit` | Max documents (default: 20) |
| `--json` | Return raw JSON |
***
### `datris status`
Get the latest job status for a pipeline.
```bash theme={null}
datris status my_pipeline
```
***
### `datris delete`
Delete a pipeline configuration together with its destination data.
```bash theme={null}
datris delete my_pipeline
```
| Option | Description |
| - | - |
| `--json` | Return raw JSON |
Deleting a pipeline always deletes its destination data. `--keep-data` is not supported: it exits with an error and deletes nothing. To keep the data, copy it or point a new pipeline at the same prefix before deleting. Any warnings the server returns (for example, a Unity Catalog table an admin still has to drop) are printed after the success line, one per line, prefixed with `⚠`.
***
### `datris health`
Check the health of all backend services.
```bash theme={null}
datris health
```
***
### `datris secrets`
List all configured secrets.
```bash theme={null}
datris secrets
```
***
### `datris taps`
List all taps.
```bash theme={null}
datris taps
```
***
### `datris tap create`
Create a tap. The `instruction` argument (a plain-English description) is optional — provide it to have AI generate the fetch script, or omit it and supply your own script with `--script` (or create a config-only tap with neither).
```bash theme={null}
datris tap create [""] --pipeline [--name ] [--script ] [--cron ""] [--type structured|document] [--kind python|http --endpoint-url ]
```
| Argument | Description |
| - | - |
| `instruction` | Optional plain-English description of the data to fetch. When provided, AI generates the script. Omit when using `--script` or creating a config-only tap |
| Option | Description |
| - | - |
| `--pipeline`, `-p` | Target pipeline name |
| `--name`, `-n` | Tap name (default: derived from pipeline or instruction) |
| `--cron` | CRON expression for scheduling (Quartz format) |
| `--secret` | Vault secret name for credentials injected as env vars |
| `--script` | Path to a Python script file with a `fetch()` function (skips AI generation) |
| `--type` | `structured` (default) returns rows of records; `document` returns `{uri, filename, content}` dicts destined for a vector-store pipeline. See [Document Taps](/taps#document-taps) |
| `--kind` | Tap implementation kind: `python` (default) runs a script on the platform; `http` calls a user-hosted endpoint speaking the tap HTTP contract (no instruction or `--script`) |
| `--endpoint-url` | For `--kind http`: absolute http(s) URL Datris POSTs the run context to on each run (required with `--kind http`) |
| `--json` | Return raw JSON |
**Examples:**
```bash theme={null}
# Structured: rows into a Postgres/Mongo pipeline
datris tap create "Fetch daily stock prices for S&P 500 from yfinance" --pipeline stocks --cron "0 0 0 * * ?"
datris tap create "Get weather data from Open-Meteo API" --pipeline weather --name weather-tap
# Document: raw file bytes into a vector-store pipeline
datris tap create "Discover all PDFs in the S3 bucket 'contracts' under prefix 2026/" \
--pipeline contracts-vec --type document --secret aws-creds
```
Document taps require a target pipeline whose source is `unstructuredAttributes` and whose destination is a vector store (qdrant, pgvector, weaviate, milvus, or chroma). The server rejects `tap create --type document` against a structured pipeline with HTTP 400.
***
### `datris tap run`
Run a tap manually. Output reflects whether the fetched records actually landed in the target pipeline:
```bash theme={null}
$ datris tap run stock-prices
Running tap 'stock-prices'...
✓ success — 33 records fetched
→ persisted to stock-prices
→ watch: datris pipeline status --publisher 5b2f4a1d-8c7e-4f0a-9b3d-6e1c2a4f8b9e
```
Or if the run succeeded but records weren't persisted (missing target pipeline, test mode, script error, zero records), the CLI tells you exactly why:
```bash theme={null}
✓ success — 33 records fetched
→ not persisted (no_target_pipeline)
```
Pass `--json` to get the full response including `mode`, `persisted`, `persistedReason`, `publisherToken`, and `pipelineTokens`.
***
### `datris tap show`
Show full details of a tap, including its script. Prints the name, description, target pipeline, schedule, enabled state, secret, and last run/test results, followed by the stored script.
```bash theme={null}
datris tap show
```
| Option | Description |
| - | - |
| `--json` | Return raw JSON |
***
### `datris tap test`
Test-run a tap without pushing the fetched records to the target pipeline. Reports the run status, record count, and data type.
```bash theme={null}
datris tap test
```
| Option | Description |
| - | - |
| `--json` | Return raw JSON |
***
### `datris tap logs`
Show the run history for a tap — each entry's timestamp, status, mode (run or test), record count, duration, and any error.
```bash theme={null}
datris tap logs
```
| Option | Description |
| - | - |
| `--json` | Return raw JSON |
***
### `datris tap update`
Update a tap's configuration without regenerating its script. Specify at least one option.
```bash theme={null}
datris tap update [--enabled | --disabled] [--cron ""] [--pipeline ] [--description ""] [--endpoint-url ]
```
| Option | Description |
| - | - |
| `--enabled` / `--disabled` | Enable or disable the tap |
| `--cron` | CRON expression for scheduling (Quartz format) |
| `--pipeline`, `-p` | Target pipeline name |
| `--description`, `-d` | New description |
| `--endpoint-url` | New endpoint URL (HTTP taps only) |
| `--json` | Return raw JSON |
***
### `datris tap delete`
Delete a tap.
```bash theme={null}
datris tap delete
```
***
### `datris catalog export`
Download a catalog as a zip archive: every tap and pipeline in it, each tap's script and incremental state, and the validation schemas its pipelines reference. Secret values, destination data, run history and version history are never included. The command prints how many taps and pipelines the archive holds and which secrets the catalog needs.
```bash theme={null}
datris catalog export [OPTIONS]
```
| Option | Description |
| - | - |
| `--out` | Archive file to write (default: `.zip`) |
| `--json` | Print the raw tool result (includes the archive as base64) |
**Example:**
```bash theme={null}
datris catalog export sales --out sales-backup.zip
```
Uncataloged is not a catalog and cannot be exported. On an error nothing is written and the command exits 1.
***
### `datris catalog restore`
Restore an archive made by `datris catalog export`, under the same name or a new one. A restore never merges into an existing catalog: it is refused when the catalog already exists or when any archived tap or pipeline name already exists anywhere on this install, and each clash is printed. Restored taps start disabled; test each one and then enable it. Secrets are never written: the command names any secret the restored items need that does not exist yet.
```bash theme={null}
datris catalog restore [OPTIONS]
```
| Option | Description |
| - | - |
| `--as` | Catalog name to restore into (default: the name stored in the archive). Letters, digits, `_` and `-` only |
| `--overwrite` | Replace the items already in the target catalog instead of creating a new one. Asks you to type the catalog name to confirm |
| `--yes` | Skip the `--overwrite` confirmation prompt (for scripts) |
| `--no-state` | Skip each tap's incremental state, so a clone starts from scratch instead of from the source's cursor |
| `--json` | Print the raw tool result |
**Examples:**
```bash theme={null}
# Restore under a new name
datris catalog restore sales-backup.zip --as sales_restored
# Clone without the source taps' incremental state
datris catalog restore sales-backup.zip --as sales_copy --no-state
# Replace the contents of an existing catalog
datris catalog restore sales-backup.zip --as sales --overwrite
```
Tap and pipeline names are unique across the install, so restoring onto the same install under a new name is refused while the source catalog still exists: delete or rename the source first. The command exits 1 on an error, on a refusal, and when some items could not be restored (each is printed with its reason).
***
### `datris version`
Get the server version.
```bash theme={null}
datris version
```
***
### `datris doctor`
Operational self-check: the server's own checks (Vault token expiry, AI slot secrets, embedding model loaded, disk, version skew) plus host checks that need Docker (anonymous volumes, container env drift, orphaned containers, stale source build). Every finding prints the command that fixes it; nothing is changed. See [Doctor](/doctor) for the full check list.
```bash theme={null}
datris doctor # human-readable report
datris doctor --json # the same report as JSON
datris doctor --probes ai # also confirm each AI model answers (spends a few tokens)
datris doctor --pre-upgrade # host checks only — run before every upgrade
```
| Option | Description |
| - | - |
| `--pre-upgrade` | Host checks plus the AI slot secrets via the vault container; works with the server stopped. Prints the upgrade command when clean. |
| `--probes ai` | Opt in to the AI reachability probe |
| `--json` | Print the merged report as JSON |
| `--project-dir DIR` | Directory holding `docker-compose.yml` and `.env` (default: current directory) |
| `--compose-file FILE` | Compose file to inspect (e.g. `docker-compose.standalone.yml`) |
Unlike the other commands, `doctor` calls the server's REST API directly rather than going through the MCP server (`DATRIS_URL`, default `http://localhost:8080`). It reads the same `DATRIS_API_KEY` as every other command when API keys are on — a dead MCP server is itself a finding, not a reason to fail.
Exit codes: `0` all ok, `1` any warning, `2` any error, `3` server unreachable (host checks still ran).
## Pipeline Name Auto-Detection
When `--pipeline` is not specified, the CLI derives the pipeline name from the filename:
* `sales-data.csv` → `sales_data`
* `Q1 Revenue Report.csv` → `q1_revenue_report`
* `trades.json` → `trades`
The extension is stripped, hyphens and spaces are replaced with underscores, and the name is lowercased.
# Configuration Assistant
Source: https://docs.datris.ai/configuration-assistant
Read and change AI providers, secrets, users, API keys and agent policy from a chat panel on the Configuration tab. Every change waits for your confirmation; secrets go in a form, never in chat.
The Configuration assistant is a chat panel on the **Configuration** tab. Ask it what a setting is, or tell it what to change, and it reads the same settings the sub-tabs show and proposes the change for you to confirm.
## What it is
A narrow **Ask** rail sits at the right edge of the Configuration tab. Click it, or press `Cmd+\`, to open a panel titled **Configuration assistant**. Press `Cmd+\` again, or use the collapse arrow, to close it.
The panel stays open while you move between sub-tabs, and it knows which sub-tab you are looking at, so "what is set here?" means the sub-tab in front of you. The conversation is kept when you leave Configuration and come back; the **+** button in the header starts a new one.
An empty panel offers four starting points:
| Button | What it asks |
| - | - |
| What is each AI slot using? | List each AI slot with the provider and model it is using right now. |
| Add a secret for a new tap | Help me add a secret for a new tap and tell me which fields it needs. |
| Who has admin? | Which users have the admin role? |
| Run doctor | Run doctor and summarize anything that needs attention. |
## What it can read and change
It covers the same ground as the sub-tabs, one row per sub-tab:
| Sub-tab | Reads | Changes |
| - | - | - |
| AI Providers | Each AI slot (primary, code generation, embedding, web search) with its provider and model, and which provider credentials are set | Point a slot at a provider and model; enter a provider's credentials in a masked form. Not offered on trial or hosted installs, matching the sub-tab |
| Secrets | Secret names and types, and the field names of one secret. Values are never returned | Create or replace a secret; delete a secret |
| Data Sources | The data-sources prompt fragment and whether it is on | Replace the fragment and turn it on or off |
| Code Repository | The repository settings used to store generated scripts; a connection test | Update the settings; store an access token through a masked form; delete a stored token |
| Users | Users and their roles. Only when the Users sub-tab is shown (`USE_USER_AUTH=true`) | Create a user, change a role, reset a password, delete a user. The `admin` user cannot be deleted, matching the sub-tab |
| API-Keys | Keys by label with their capabilities, the capability templates and the full capability list. Only when the API-Keys sub-tab is shown (`USE_API_KEYS=true`). Key values are never returned | Issue a key from a template or a capability list; rotate a key; revoke a key |
| Agent Policy | Whether the policy is on, the current policy, the recommended policy and the known actions | Change actions, overrides or limits; switch to the recommended policy |
| Audit Log | Read-only: search entries by time, category, action, actor, outcome or resource, and list the values present for each filter | Nothing |
| Doctor | Read-only: run the platform health checks, optionally with a small test request to each configured AI provider | Nothing |
So you can ask the audit log a question in plain language ("what changed yesterday?", "who rotated a key this week?") and get an answer from the entries themselves, and you can run [Doctor](/doctor) from the chat and get a summary of what needs attention.
## Every change is confirmed
The assistant proposes one change at a time. Each proposal appears in the panel as a card with a short summary of what will happen and two buttons, **Confirm** and **Cancel**.
* Nothing is written until you click **Confirm**.
* **Cancel** does nothing, and the assistant says the change was cancelled.
* A confirmation works once. It also expires after ten minutes, so an old card left in the conversation cannot be confirmed later; ask again for a fresh one.
Before it proposes removing a user, an API key, a secret or a repository token, the assistant reads the current list and names the exact one it will remove. If your request could mean more than one, it asks which.
Changes you confirm here are carried out as your own actions. The [Agent Policy](/agent-policy) gates agents and API-key clients, not the Configuration assistant, so a confirmed change is not queued for approval: your Confirm is the approval.
## Secrets never go in the chat
For AI provider credentials, code repository access tokens and secret values, the panel opens a masked form on the card. The value goes from your browser straight to the platform; the assistant only learns that it was provided.
The assistant will not ask you to paste a key, token or password into the conversation. If an answer shows a value as `••••••••`, the value is set, not missing: masked values are never revealed.
## Shown once
Two kinds of value are shown once, on the card, with a **Copy** button:
* the temporary password for a new user;
* the value of a newly issued or rotated API key.
They appear on that card and nowhere else, and the assistant does not repeat them in later answers. Copy the value before you move on. Resetting a password does not produce a value; the user sets a new one at next login, as described in [User Authentication](/user-auth).
## Who can use it
* With user authentication on, the panel is shown to admins only.
* With user authentication off, it is shown to everyone who can open Configuration.
* On a trial install it is not shown.
With user authentication on, the assistant needs a signed-in admin session. A client that authenticates with an API key, including the CLI and MCP agents, cannot use it, and the MCP tool catalog does not gain any of these abilities: agents still see Platform-tab secrets as read-only (see [the Assistant](/assistant#platform-tab-secrets-are-visible-but-read-only)) and still need a scoped key for everything else (see [API Keys](/api-keys)).
## Audited as you
With user authentication on and the audit log on, every change the assistant makes lands in the [audit log](/audit-log#who-is-recorded) (best effort by default; see [strict mode](/audit-log#strict-mode)) attributed to the signed-in admin, exactly as if you had made it in the sub-tab, with `via: config-chat` in the entry detail so you can tell which changes came through the panel. With user authentication off there is no signed-in user, and the entry is recorded like any other UI action.
## Refresh and Show me
When a change succeeds, the sub-tab that owns the setting reloads on its own, so what you see matches what was saved. A **Show me** link on the card switches to that sub-tab.
Links that open Configuration on a given sub-tab now work for Code Repository too (`?tab=code-repo`).
## What it leaves to other panels
The Configuration assistant handles settings. Other questions go elsewhere, and it will say which panel to use:
* building taps and pipelines, including Live Read, belongs to the [Assistant](/assistant);
* runs, failures, retries and recovery belong to the Ops panel (see [Incidents](/incidents));
* questions about the data itself belong to the Search panel.
The settings those rely on (AI providers, secrets, the code repository, users, API keys and agent policy) are the Configuration assistant's.
# Configuration Reference
Source: https://docs.datris.ai/configuration-reference
Complete configuration reference
The pipeline server is configured via `application.yaml` (or `application.properties`). This page documents all available properties.
## Changing Settings After Install
Datris settings fall into **two buckets**, and which bucket a setting is in determines how you change it after the first install. Editing the wrong place — or restarting the wrong way — is the most common source of "I changed it and nothing happened."
### Bucket 1 — Configuration tab (Vault-backed)
AI provider / model / API keys, the embedding slot, and all connection secrets (Postgres, MongoDB, MinIO, vector stores, API keys) are stored in Vault and **edited in the Configuration tab**.
`.env` seeds these **only on the very first boot**. After that, Vault persists on disk and the **Configuration tab is the source of truth** — editing the corresponding `.env` value later has **no effect**. Saves in the tab go straight to Vault and take effect right away (AI config hot-reloads in-process; other secrets are picked up on next use), with no restart needed.
To deliberately throw away the persisted config and re-seed from `.env`, wipe the Vault volume — see [How Configuration Persists](/ai-configuration#how-configuration-persists).
### Bucket 2 — Container environment variables (`.env`, read every boot)
The deployment-level settings in the [Container Environment Variables](#container-environment-variables) table — `JAVA_OPTS`, `PIPELINE_MAX_PAYLOAD_MB`, `USE_USER_AUTH`, `USE_API_KEYS`, `USE_AUDIT_LOG`, `AUDIT_LOG_STRICT`, `PROVENANCE_STRICT`, `CAPABILITY_ENFORCEMENT`, `TAP_SCRIPT_TIMEOUT_SECONDS`, `TAP_RUN_TIMEOUT_SECONDS`, the tap-runner vars, etc. — are **not** Vault-seeded. They're plain container env vars read **fresh on every boot**, so changing them in `.env` after install works normally:
```bash theme={null}
# 1. Edit the value in .env, then:
docker compose up -d datris
```
`up -d` recreates the `datris` container with the new environment. (Use `--force-recreate datris` if Compose doesn't detect the change.)
Do **not** use `docker compose restart` to apply an `.env` change. `restart` reuses the existing container with its **old** environment, so your edit is silently ignored. Always use `up -d` (which recreates the container) or a `down` / `up`.
**Enabling `USE_USER_AUTH` is a clean toggle.** When the user table is empty the server seeds an `admin` user with a random bootstrap password and prints it once, in the log of that container only (`docker compose logs datris | grep "Bootstrap login"`). Read and save it **before** you recreate the container to turn the flag on, because recreating discards that log; your first login is then `admin` with that password, and there's no separate provisioning step. If the line is already gone, follow [Recovering admin access](/user-auth#recovering-admin-access). Change the password after you log in. See [User Authentication](/user-auth) for the full walkthrough.
## Full Reference
### Spring Boot
| Property | Default | Description |
| - | - | - |
| `spring.servlet.multipart.max-file-size` | `${DATRIS_MAX_UPLOAD_MB:1024}MB` | Maximum upload file size (1 GB unless `DATRIS_MAX_UPLOAD_MB` is set) |
| `spring.servlet.multipart.max-request-size` | `${DATRIS_MAX_UPLOAD_MB:1024}MB` | Maximum request size (same variable) |
| `spring.servlet.multipart.location` | `${DATRIS_TEMP_DIR:/tmp/datris-staging}/_multipart` | Where an upload is spooled while it is received — on the staging volume, not the container's writable layer |
| `spring.server.tomcat.connection-timeout` | `600000` | Tomcat connection timeout (ms) |
### Logging
| Property | Default | Description |
| - | - | - |
| `logging.level.root` | `INFO` | Root log level |
| `logging.level.org.springframework.web` | `INFO` | Spring web log level |
| `logging.level.ai.datris` | `INFO` | Pipeline log level |
### Scheduling
| Property | Default | Description |
| - | - | - |
| `schedule.checkFileNotifierQueue` | `5000` | Polling interval for file notification queue (ms) |
| `schedule.findJobsToStart` | `5000` | Interval to check for queued jobs (ms) |
| `schedule.checkDatabaseSourceQueries` | `30000` | Interval to check for database pulls (ms) |
| `schedule.checkTapSchedules` | `30000` | Interval to check for taps with CRON schedules due to run (ms) |
| `schedule.incidentSweep` | `300000` | Recovery-agent sweep interval (ms): resumes approved actions and raises stale/volume signals |
| `schedule.doctorTick` | `60000` | How often (ms) the periodic [doctor](/doctor) checks whether `doctor.intervalMinutes` has elapsed |
### Pipeline
| Property | Default | Description |
| - | - | - |
| `environment` | `oss` | Environment name. Used as prefix for bucket names (`oss-raw`, `oss-data`, etc.) and table names |
| `useApiKeys` | `false` | Enable API key authentication. See [Authentication](#authentication) and the [API Keys guide](/api-keys) |
| `multiTenant` | `false` | Enable per-request tenant resolution. When true, the postgres database is overridden per-request with the tenant name |
| `sendPipelineNotifications` | `true` | Enable pipeline event notifications |
| `ttlFileNotifierQueueMessages` | `60` | Days to retain processed message IDs for deduplication |
| `tapScriptTimeoutSeconds` | `300` | Wall-clock ceiling for a tap **test** (`mode=test`), in seconds. Set it from `TAP_SCRIPT_TIMEOUT_SECONDS` in `.env` |
| `tapRunTimeoutSeconds` | `3600` | Wall-clock ceiling for a real or scheduled tap **run**, in seconds. When unset it resolves to `max(3600, tapScriptTimeoutSeconds)`, so an install that raised the old single ceiling never gets a shorter one. Set it from `TAP_RUN_TIMEOUT_SECONDS` in `.env` |
| `versionCap` | `50` | Definition versions retained per tap / pipeline before older snapshots (and their pinned scripts) are pruned |
| `cron.retry.enabled` | `true` | Automatically retry failed scheduled (CRON) tap runs that fed nothing downstream |
| `cron.retry.cap` | `3` | Maximum automatic retries per failed scheduled tap run |
| `cron.retry.backoffMinutes` | `5,15` | Comma-separated backoff ladder (minutes) between automatic retries; the last value repeats |
| `pipelineMaxPayloadMB` | `4096` | Per-run payload disk budget in megabytes: the most one run — a tap run, an upload or an object-store pickup — may stage on disk (`DATRIS_TEMP_DIR`) before it is stopped with an actionable error naming `PIPELINE_MAX_PAYLOAD_MB` (raise it or split the source; for a tap, chunk the source range via `run_tap` `params`). `0` = unlimited. Records stream to the staged file and never sit in JVM heap, so this bounds disk, not memory. Size it from the data: a staged record costs about 26 bytes × column count for numeric/timestamp/short-string data (JSON keys repeat on every row), so 20,000,000 rows × 25 columns stage roughly 13 GB. Override per deployment with the `PIPELINE_MAX_PAYLOAD_MB` env var. |
| `tapMaxOutputMB` | unset | **Deprecated** alias of `pipelineMaxPayloadMB`, honoured for one release only when `pipelineMaxPayloadMB` is unset; a warning is logged at boot when it is set. |
| `scratchInlineRows` | `200` | For Live Read pipelines (the `scratch` destination): how many result rows ride inline on the run status (`resultPreview`), and the hard maximum page size for `GET /api/v1/pipeline/result`. Override per deployment with the `SCRATCH_INLINE_ROWS` env var. |
| `scratchRetentionHours` | `24` | How long a Live Read result object is kept before the hourly sweep deletes it; reads after expiry return 410 and the caller runs the pipeline again. A Live Read result holds whatever the source returned, so retention is the mitigation — keep it short. `0` expires results immediately. Override per deployment with the `SCRATCH_RETENTION_HOURS` env var. |
| `datris.agent.toolResultMaxChars` | `32000` | In-app assistants (Assistant, Ops, Catalog, Configuration and Search chats, recovery agent): the most characters of one tool result passed back to the model. The chat always shows the full result; past the cap the model sees a note saying how much was cut. |
| `datris.agent.toolResultMaxCharsScript` | `131072` | The same cap for tools that return code (`get_tap`, `get_tap_version`, `diff_tap_versions`, and any tool whose name ends in `_script`), so an assistant can read a whole tap script. Never lower than `datris.agent.toolResultMaxChars`. |
### Authentication
All four governance controls (`USE_USER_AUTH`, `USE_API_KEYS`, `USE_AUDIT_LOG`, `USE_AGENT_POLICY`) default to `false` and are switched on for production together — see [Switch on the governance controls](/quick-start#2-switch-on-the-governance-controls) for the installer option and the `.env` block; the `governance.controls` [doctor](/doctor) check reports any that is off.
Two independent flags control access. Set them together — see [the supported matrix](/api-keys#supported-configurations) — and use [User Authentication](/user-auth) and [API Keys](/api-keys) for the full setup walkthroughs.
| Env var | Default | Description |
| - | - | - |
| `USE_USER_AUTH` | `false` | Require login for the UI; gate Configuration and Users tabs to `admin`. See [User Authentication](/user-auth) |
| `USE_API_KEYS` | `false` | Require `x-api-key` on every REST and MCP request. Maps to `useApiKeys` above |
| `MULTI_TENANT` | `false` | Advanced: per-tenant database isolation. Requires `USE_API_KEYS=true`; each request is routed to the tenant environment its API key belongs to |
| `OIDC_ENABLED` | `false` | `oidc.enabled`: single sign-on through your identity provider ("Sign in with SSO" on the login page). Only takes effect with `USE_USER_AUTH=true`; a wrong or incomplete setting switches SSO off (ERROR in the log) and never blocks startup or password login. See [Single sign-on (OIDC)](/user-auth#single-sign-on-oidc) |
| `OIDC_ISSUER` | empty | `oidc.issuer`: the provider's issuer URL; must equal the `issuer` in its discovery document exactly |
| `OIDC_CLIENT_ID` | empty | `oidc.clientId`: the client id registered at the provider. The client secret is not an env var: it lives in Vault under `oss/oidc` (`oidc.secretName`), key `clientSecret` |
| `OIDC_REDIRECT_URI` | empty | `oidc.redirectUri`: `https:///api/v1/auth/oidc/callback`, registered at the provider |
| `OIDC_SCOPES` | `openid email profile` | `oidc.scopes`: scopes requested at sign-in |
| `OIDC_USERNAME_CLAIM` | `email` | `oidc.usernameClaim`: the ID-token claim that becomes the Datris username |
| `OIDC_DEFAULT_ROLE` | empty | `oidc.defaultRole`: role for a person with no Datris user. Empty refuses them; `viewer` or `editor` creates them. `admin` is not accepted and counts as empty |
| `CAPABILITY_ENFORCEMENT` | `enforce` | Default: scoped keys missing the required capability get HTTP 403. Override to `log-only` to emit `would-deny` log lines without rejecting — useful only when iterating on a new scope policy and you want a trial run before flipping enforcement on. Legacy `*:*` keys are unaffected either way |
| `DATRIS_ALLOW_PRIVATE_EGRESS` | `false` | HTTP taps and REST endpoint destinations refuse loopback, link-local, private, and cloud-metadata addresses. Set `true` only if your endpoints legitimately live on an internal network. See [Tap Execution & Isolation](/tap-execution-isolation#always-on-protections). |
| `USE_CODEGEN_RUNNER` | `true` (compose) / unset=`false` (sbt) | Run the Python scripts generated for AI data-quality rules and AI transformations in the isolated `datris-codegen-runner` sidecar (no platform secrets, no server files, no internet). With it on, a stopped or unreachable runner fails the rule or transformation; there is no in-process fallback. Set `false` to run them in-process (warned at startup, per run, and by `datris doctor`). See [Tap Execution & Isolation](/tap-execution-isolation#generated-data-quality-and-transformation-scripts). |
| `CODEGEN_RUNNER_URL` | `http://datris-codegen-runner:8090` | Address of the CodeGen runner service. Shares `TAP_RUNNER_TOKEN` with the tap runner. |
| `CODEGEN_RUNNER_API_BLOCK` | `true` | With `USE_CODEGEN_RUNNER=true`, the server answers `403` to every request from the CodeGen runner (its `codegen-net` interface and the runner's address). Set `false` only if the guard refuses traffic it should not. See [Tap Execution & Isolation](/tap-execution-isolation#generated-data-quality-and-transformation-scripts). |
| `CODEGEN_SCRATCH_DIR` | unset (compose sets `/scratch` on `datris-codegen-runner`) | Set on the runner container, not on `datris`. Directory where the runner keeps each script's input and output (the `codegen-scratch` volume in compose). Setting it is what turns on the cleanup controlled by the next two variables. |
| `CODEGEN_SCRATCH_DEDICATED` | `1` (image default) | Runner container. When `1` and `CODEGEN_SCRATCH_DIR` is set, that directory is treated as used for nothing else: **everything in it** is removed after every run and when the runner starts. Never point `CODEGEN_SCRATCH_DIR` at a shared path with this on. Set any other value to remove only the runner's own `cg_*` run directories. |
| `CODEGEN_TMP_DIR` | `/tmp` (image default) | Runner container. When `CODEGEN_SCRATCH_DIR` is set, this directory (the runner's small `/tmp`) is emptied after every run and at start, so a script cannot leave files there for the next run. |
| `DATRIS_TAP_SECRET_SCOPE` | `tap` | Which secrets a tap may use. `tap` (default): only secrets tagged `_type=tap`; saving a tap on any other secret returns HTTP 400, and a tap saved earlier on a platform secret fails its run with the same message before any field is injected. `any`: taps may also use platform secrets (the behaviour before this setting existed), which lets anyone who can create and run a tap read those values. Any other value means `tap` and is logged once. The server-managed secrets (field protection keys, API-key stores) are refused either way. Affected taps are named in a startup warning and by the `tap.secret_scope` [doctor](/doctor) check. See [Taps](/taps#taps-can-only-use-tap-secrets). |
| `DATRIS_AI_SAMPLE_VALUES` | `true` | Whether the helpers that write configuration or code send row values to a model: schema generation from a file, data profiling, JSON Schema / XSD generation, Assistant attachments, the sample rows in CodeGen data-quality and transformation prompts, and the error text in AI fix suggestions and recovery diagnosis for failed pipeline runs. `false`: none of them sends a row value; each works from column names, types, counts and lengths (or a value-free skeleton for JSON and XML), error text is reduced to exception classes and fixed messages, and helper responses carry `valuesWithheld: true`. Any other value means `true` and is logged once. Not affected: Search chat, the answer endpoint, the Assistant reading query results or a Live Read preview, the Assistant or recovery agent reading run status, logs or job details through tools, tap script generation, review and diagnosis and fix suggestions for failed tap runs, embeddings, and the run-time header check (`validateFileHeader`); see [Data Flows](/production/data-flows) for the full list. Reported by the `ai.sample_values` [doctor](/doctor) check. See [Field Protection](/transformation/field-protection#what-still-sees-raw-data). |
| `DATRIS_ENV` | *(unset)* | Set to `production` to require TLS (`sslmode=require` or stricter) on pipeline loads to external Postgres hosts. The bundled and local Postgres services are exempt. |
| `DATRIS_ALLOW_PLAINTEXT_DB` | `false` | Opt-out for the `DATRIS_ENV=production` TLS check: the load proceeds and a warning is logged. |
| `DATRIS_UNITY_CATALOG_SYNC` | `true` | Install-wide switch for Unity Catalog sync on pipelines that set `unityCatalog.enabled` or that `DATRIS_UNITY_CATALOG_DEFAULT` turned on: the metadata push and lineage on Databricks pipelines, and Iceberg table registration on object store pipelines. Set `false` to skip it (each opted-in run logs one line saying it is off). An object store pipeline with `catalogMode: "rest"` writes by path while it is off, unless its table has already been committed through the catalog, in which case the run fails rather than split the table's history. A pipeline with `catalogMode: "managed"` fails every run while it is off, because it never writes by path. Beats `DATRIS_UNITY_CATALOG_DEFAULT`: while it is `false`, nothing is written even for pipelines the default turned on. Has no effect on pipelines where Unity Catalog is off. See [Databricks](/destinations/databricks#unity-catalog-metadata) and [Register Iceberg tables](/destinations/unity-catalog#register-iceberg-tables). |
| `DATRIS_UNITY_CATALOG_DEFAULT` | `disabled` | Install-wide default for Databricks pipelines that have no `unityCatalog` block. Set `enabled` and such a pipeline behaves as if it had `"unityCatalog": {"enabled": true}` (comments, tags, properties and lineage all on); its Unity Catalog state reports `enabledBy: "default"`. A pipeline's own block always wins, so `"unityCatalog": {"enabled": false}` opts it out. `DATRIS_UNITY_CATALOG_SYNC=false` beats it. Object store Iceberg pipelines are never defaulted (registration needs the pipeline's own `credentialsSecret` and `catalog`). Any value other than `enabled` means `disabled`. See [Databricks](/destinations/databricks#unity-catalog-metadata). |
| `DATRIS_UNITY_CATALOG_SEARCH_TIMEOUT_SECONDS` | `45` | Per-secret time limit for `find_data` with `includeUnityCatalog`. A Databricks secret whose warehouse does not answer in time (for example a classic warehouse still starting) is listed under `unityCatalog.skipped` as `timed out after Ns` instead of holding the call. Blank or non-positive values use the default. See [Unity Catalog](/destinations/unity-catalog). |
| `DATRIS_OBJECTSTORE_BUCKET_ALLOWLIST` | *(unset)* | Comma-separated list of buckets on the built-in MinIO that a pipeline's `destinationBucketOverride` may target. Unset means any bucket (today's behaviour, with a warning logged on save). When set, an override outside the list is rejected on save and refused at run, query and delete time; the environment default bucket `{environment}-data` is always allowed. `provider: s3` buckets are exempt. See [Object Store](/destinations/object-store#restricting-bucket-overrides). |
| `USE_AUDIT_LOG` | `false` | Record who created, changed, ran, or deleted what — readable by admins under Configuration → Audit Log and mirrored to the server log. Restart to change. See [Audit Log](/audit-log) |
| `AUDIT_LOG_RETENTION_DAYS` | `90` | Days to keep audit entries in the config store; `0` keeps them forever. Maps to `auditLog.retentionDays` |
| `AUDIT_LOG_READS` | `false` | Also record reads — queries, searches, listings, metadata. Off by default because they dwarf the write trail. Maps to `auditLog.logReads` |
| `AUDIT_LOG_STRICT` | `false` | Strict evidence mode for the audit log (needs `USE_AUDIT_LOG=true`). `false`: best effort, an entry the config store cannot take is dropped and counted in `datris_audit_dropped_total`. `true`: nothing is dropped; while the audit queue is backed up, requests that would be audited get HTTP 503 before they run. Restart to change. Maps to `auditLog.strict`. See [Audit Log](/audit-log#strict-mode) |
| `PROVENANCE_STRICT` | `false` | Strict evidence mode for provenance stamping. `false`: a run with `provenance.stamp` on whose data cannot be stamped loads unstamped. `true`: that run fails before anything is loaded, and unknown `provenance.fields` names are refused on save. XML sources are exempt: they are never stamped and still load unstamped. Does not turn stamping on. Restart to change. Maps to `provenance.strict`. See [Provenance](/provenance#strict-mode) |
| `USE_AGENT_POLICY` | `false` | Per-action auto / approve / deny for agent-initiated changes, with a human approval queue under Activity → Approvals. With it on and no rules set, nothing changes. Restart to change. See [Agent Policy](/agent-policy) |
| `RECOVERY_AGENT_ENABLED` | `false` | The platform opens, diagnoses and — within the agent policy's recovery mode — repairs incidents for failed, stale or anomalous data flows. Needs `USE_AGENT_POLICY=true`. See [Incidents](/incidents) |
| `INCIDENT_WEBHOOK_URL` | *(empty)* | Optional URL receiving a JSON POST on incident open / awaiting-approval / resolved / failed, and on doctor checks flipping to error when the periodic run is on |
| `DOCTOR_ONSTARTUP` | `true` | Log the cheap [doctor](/doctor) checks at boot as `DOCTOR` lines. Maps to `doctor.onStartup` |
| `DOCTOR_INTERVAL_MINUTES` | `0` | Re-run the server-side doctor checks every N minutes and POST checks that flip to error (or recover) to `INCIDENT_WEBHOOK_URL`. `0` = off. AI probes never run on the timer. Maps to `doctor.intervalMinutes` |
| `DATRIS_UPDATE_CHECK` | `1` | Host-side only, read by the `datris doctor` CLI from the project `.env` or its environment (not by any container). `0`, `false` or `off` stops the `version.update` check from asking datris.ai whether a newer version exists; with it on, a full `datris doctor` run sends the server version, OS and CPU architecture. See [Doctor](/doctor#update-check) |
One further audit setting lives only in `application.yaml`: `auditLog.emitLogLine` (default `true`; mirror each entry to the `ai.datris.audit` logger for SIEM pickup).
With `USE_USER_AUTH=true` + `USE_API_KEYS=true` (recommended), the UI authenticates via session cookie — no `x-api-key` value to seed or paste. Issue programmatic-client keys from **Configuration → API-Keys**.
For `USE_USER_AUTH=false` + `USE_API_KEYS=true` (legacy paste-and-go), the seeded fallback value `default-ui-key` is what the user pastes into the Connect prompt on first load. Rotate it any time from **Configuration → API-Keys**.
### CORS
Cross-Origin Resource Sharing controls which browser origins can call the Datris API directly. The default `*` allows any origin and is appropriate for local development. **In production, lock this down to your real frontend origin(s).**
| Property | Default | Description |
| - | - | - |
| `cors.allowedOrigins` | `*` | Comma-separated list of allowed origins. Use `*` for any origin (development only), or specific URLs like `https://app.example.com,https://admin.example.com`. Applied globally to all `/api/**` endpoints. |
In the deploy config, this reads from the `CORS_ALLOWED_ORIGINS` environment variable so you can change it without rebuilding the image:
```yaml theme={null}
cors:
allowedOrigins: ${CORS_ALLOWED_ORIGINS:*}
```
### Date / Timezone
All display timestamps across the platform (pipeline status, tap run history, etc.) are formatted using these settings.
| Property | Default | Description |
| - | - | - |
| `dateFormat` | `yyyy-MM-dd HH:mm:ss z` | Java `SimpleDateFormat` pattern. Use `z` to print the timezone abbreviation (e.g., `UTC`, `EDT`, `EST`) |
| `dateTimezone` | `America/New_York` | IANA timezone ID (e.g., `UTC`, `America/New_York`, `Europe/London`). When the format includes `z`, daylight saving is handled automatically |
**Example — Eastern time with auto-DST:**
```yaml theme={null}
dateFormat: "yyyy-MM-dd HH:mm:ss z"
dateTimezone: "America/New_York"
# Displays: 2026-04-05 14:30:00 EDT (summer) or 2026-11-05 14:30:00 EST (winter)
```
### MinIO (Object Store)
| Property | Description |
| - | - |
| `minio.server` | MinIO endpoint URL (e.g., `http://localhost:9000`) |
MinIO credentials are stored in Vault under the secret specified by `secrets.minIOSecretName`:
```json theme={null}
{
"accessKey": "minioadmin",
"secretKey": "minioadmin"
}
```
### AWS S3 (per-pipeline credentials)
S3 destinations don't use a global secret. Each pipeline that writes to S3 references a **Platform-tab secret** by name via the objectStore config's `credentialsSecret` field. Create the secret in the UI under Configuration → Secrets → Platform with the fields below:
```json theme={null}
{
"accessKey": "AKIA…",
"secretKey": "…",
"region": "us-east-1",
"sessionToken": "…"
}
```
Required: `accessKey`, `secretKey`, `region`. Optional: `sessionToken` (for temporary STS credentials). Field-name lookups are case-insensitive and accept the `AWS_ACCESS_KEY` / `AWS_SECRET_KEY` / `AWS_REGION` style as well.
Region lives in the credentials secret rather than on the pipeline config so the credential and its scope travel together — one place to rotate, no silent mismatches. See [S3 Destination](/destinations/s3) for the full pipeline-config shape.
### Secrets (HashiCorp Vault)
| Property | Description |
| - | - |
| `secrets.apiKeysSecretName` | Vault path for API keys |
| `secrets.postgresSecretName` | Vault path for PostgreSQL credentials |
| `secrets.minIOSecretName` | Vault path for MinIO credentials |
| `secrets.activeMQSecretName` | Vault path for ActiveMQ credentials |
| `secrets.mongoDbSecretName` | Vault path for MongoDB credentials |
| `secrets.kafkaProducerSecretName` | Vault path for Kafka producer credentials |
| `secrets.qdrantSecretName` | Vault path for Qdrant connection |
| `secrets.weaviateSecretName` | Vault path for Weaviate connection |
| `secrets.milvusSecretName` | Vault path for Milvus connection |
| `oidc.secretName` | Vault path for the OIDC single sign-on client secret (default `oss/oidc`, key `clientSecret`). A platform secret: no tap or pipeline can reference it |
| `secrets.chromaSecretName` | Vault path for Chroma connection |
| `secrets.pgvectorSecretName` | Vault path for pgvector PostgreSQL connection |
Vault connection is configured via environment variables:
* `VAULT_ADDR` - Vault server URL (e.g., `http://vault:8200`)
* `VAULT_TOKEN_FILE` - Path to a file holding the auth token (preferred; the bundled Compose writes a random per-install token here). Takes precedence over `VAULT_TOKEN`.
* `VAULT_TOKEN` - Authentication token, used when `VAULT_TOKEN_FILE` is unset (local dev, custom setups)
### ActiveMQ (Queue & Notifications)
| Property | Description |
| - | - |
| `activemq.server` | ActiveMQ broker URL (e.g., `tcp://localhost:61616`) |
ActiveMQ credentials are stored in Vault under `secrets.activeMQSecretName`:
```json theme={null}
{
"username": "admin",
"password": "admin"
}
```
### MongoDB (NoSQL Store)
| Property | Description |
| - | - |
| `mongodb.connectionString` | MongoDB connection URI (e.g., `mongodb://localhost:27017`) |
| `mongodb.database` | **User-facing** database name (default: `datris`). Pipelines write here, the UI Data tab reads here (semantic-search panel), and tap scripts get this as `DATRIS_MONGODB_DATABASE`. In multi-tenant mode the tenant environment name is used instead. |
| `mongodb.internalDatabase` | **Platform-internal** database name (default: `oss`). Holds pipeline/tap configs, run status, job queues — never surfaced in the UI. Keep this distinct from `mongodb.database` so user data and platform state don't mix. |
### PostgreSQL
| Property | Default | Description |
| - | - | - |
| `postgres.database` | `datris` | Default database name used by `/api/v1/query/postgres` and `/api/v1/metadata/postgres/*` when no `database` parameter is supplied. Also injected into tap scripts as the `DATRIS_POSTGRES_DATABASE` environment variable. In multi-tenant mode this value is automatically overridden per-request with the tenant name. |
PostgreSQL connection details (`username`, `password`, `jdbcUrl`) are stored in Vault under `secrets.postgresSecretName` — see the Vault Secret Formats section below.
### Kafka Consumer (Optional)
| Property | Default | Description |
| - | - | - |
| `kafkaConsumer.enabled` | `false` | Enable Kafka topic consumption |
| `kafkaConsumer.bootstrapServers` | `localhost:9092` (`kafka:9092` in Docker) | Kafka broker address |
| `kafkaConsumer.groupId` | `oss-group` | Consumer group ID |
| `kafkaConsumer.topicPollingInterval` | `500` | Topic polling interval (ms) |
| `kafkaConsumer.topicPrefix` | `oss` | Prefix for topic names |
### AI (Required)
AI configuration is split into four independent slots (three seeded at first boot, plus optional web search), each pointing at its own self-describing Vault secret. The resolver reads whatever it finds in the secret — `provider`, `endpoint`, `model`, `apiKey`, and (optionally) `version` — so the YAML side never needs a `provider` field.
| Property | Default | Description |
| - | - | - |
| `ai.enabled` | `true` | Enable AI features (required for the platform to start) |
| `ai.aiPrimary.secretName` | `oss/ai-primary` | Vault secret for the main AI model used for general reasoning (NL→SQL, search answers, etc.). Seeded with the recommended chat model — Anthropic gets `claude-opus-5-5`, OpenAI gets `gpt-5.6-sol`. |
| `ai.codegen.secretName` | `oss/codegen` | Vault secret for the code-generation model (tap scripts, AI DQ, AI transformations, schema generation). Seeded with the strongest code-generation model — Anthropic gets `claude-opus-5-5`, OpenAI gets `gpt-5.6-sol`. |
| `ai.embedding.secretName` | `oss/embedding` | Vault secret for the embedding model used by vector destinations (Chroma, Qdrant, Milvus, Weaviate, pgvector) and search. For Anthropic-only deployments this is seeded to point at the bundled TEI sidecar serving `BAAI/bge-m3` (1024-dim), so vector destinations work out of the box without an OpenAI key. |
| `ai.webSearch.secretName` | `oss/web-search` | Vault secret for the optional live web-search provider used during tap script generation and diagnosis. Not seeded at first boot — configure it from the Configuration tab. |
| `ai.extendedThinking` | `true` | Enable extended thinking on the Assistant tab's agent loop (Anthropic models only); the model's reasoning is shown above the visible response. Set to `false` to disable platform-wide. |
Each Vault secret is self-describing and looks like:
```bash theme={null}
vault kv put secret/oss/ai-primary \
provider="anthropic" \
endpoint="https://api.anthropic.com/v1/messages" \
model="claude-opus-5-5" \
apiKey="sk-ant-..." \
version="2023-06-01"
```
`docker/vault-init.sh` seeds all three secrets automatically based on which key is present in `.env` (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, or the `AZURE_OPENAI_*` trio) or the explicit `AI_PROVIDER=bedrock` selection. For multi-tenant deployments, per-tenant override secrets live at `{env}/ai-primary`, `{env}/codegen`, `{env}/embedding`.
These `.env` variables tune what gets seeded. They apply on the **first boot only** — after that, Vault persists and the Configuration tab is the source of truth:
| Variable | Default | Description |
| - | - | - |
| `AI_PROVIDER` | auto | Explicit provider pick (`anthropic`, `openai`, `azure`, `bedrock`, or `grok`) for the `ai-primary` and `codegen` slots. When unset, whichever API key is present wins (OpenAI wins ties; azure and grok are auto-picked only when they hold the sole key; bedrock is never auto-picked). |
| `ANTHROPIC_MODEL` | `claude-opus-5-5` | Model seeded into `oss/ai-primary` on the Anthropic path. |
| `OPENAI_MODEL` | `gpt-5.6-sol` | Model seeded into `oss/ai-primary` on the OpenAI path. Any OpenAI chat model id is accepted, including `gpt-6-astra`. |
| `AZURE_OPENAI_ENDPOINT` | — | Azure resource base URL (`https://YOUR-RESOURCE.openai.azure.com`). Required on the Azure path. |
| `AZURE_OPENAI_MODEL` | — | Chat deployment name seeded into `oss/ai-primary` on the Azure path. Required on the Azure path. |
| `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` | — | Entra ID service principal for keyless Azure OpenAI auth (no API key stored anywhere). Single-tenant only; the Configuration tab's Azure credentials section is the equivalent UI path. On Azure compute, leave all auth unset to use the server's managed identity. |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | — | AWS credentials for the Bedrock path (`AI_PROVIDER=bedrock`). Optional — leave unset to use the server's IAM role / default credential chain. Never selects Bedrock by itself. |
| `AWS_REGION` | — | AWS region for the Bedrock path (e.g. `us-east-1`). |
| `BEDROCK_MODEL` | `anthropic.claude-opus-5-5` | Invokable Bedrock model id seeded into `oss/ai-primary` on the Bedrock path. |
| `XAI_API_KEY` | — | Grok (xAI) API key from console.x.ai, for the Grok path (`AI_PROVIDER=grok`, or auto-picked when it's the sole key). |
| `GROK_MODEL` | `grok-4.7` | Grok model seeded into `oss/ai-primary` on the Grok path. |
| `CODEGEN_MODEL` | `claude-opus-5-5` or `gpt-5.6-sol` | Model seeded into `oss/codegen` (Anthropic and OpenAI defaults; the Azure path defaults to `AZURE_OPENAI_MODEL`; the Bedrock path defaults to `anthropic.claude-opus-5-5`; the Grok path defaults to `grok-4.7`). |
| `EMBEDDING_PROVIDER` | follows provider | `openai`, `azure`, `tei`, or `ollama`. When unset: `openai` when the AI provider is OpenAI, otherwise the bundled TEI sidecar (Azure embeddings are opt-in only). |
| `EMBEDDING_MODEL` | provider default | `text-embedding-3-small` (openai; also the azure default deployment name), `BAAI/bge-m3` (tei), `bge-m3` (ollama). |
| `EMBEDDING_ENDPOINT` | provider default | Endpoint override for the `tei`, `ollama`, and `azure` providers — useful when the service runs on a separate host or a non-standard path. |
All AI calls (`callAI`, `callAIWithSystem`, `callAIWithMessages`) share a unified retry helper that automatically retries on transient `429` (rate limited), `503` (service unavailable), and `529` (overloaded) responses with linear backoff (5s, 10s, 15s, 20s, 25s) for up to 5 attempts. This applies uniformly across all configured providers.
See [AI Configuration](/ai-configuration) for full setup details.
## Vault Secret Formats
### PostgreSQL
```json theme={null}
{
"username": "postgres",
"password": "password",
"jdbcUrl": "jdbc:postgresql://localhost:5432"
}
```
### MySQL
```json theme={null}
{
"username": "root",
"password": "password",
"jdbcUrl": "jdbc:mysql://localhost:3306"
}
```
### Kafka Producer
```json theme={null}
{
"bootstrapServers": "kafka:9092",
"username": null,
"password": null
}
```
### MongoDB
```json theme={null}
{
"connectionString": "mongodb://localhost:27017"
}
```
### Embedding
```json theme={null}
{
"endpoint": "https://api.openai.com/v1/embeddings",
"model": "text-embedding-3-small",
"apiKey": "sk-...",
"batchSize": "32",
"maxTokens": "8192",
"tokenizer": "openai",
"tokensPerCharRatio": "2.0",
"oversize": "split",
"retryIndividualOnFailure": "false"
}
```
`endpoint`, `model`, `apiKey` are required; `batchSize` defaults to 32. The remaining fields tune the server-side token-count guard that prevents oversized chunks from failing an embedding batch:
| Field | Default | Description |
| - | - | - |
| `maxTokens` | model-default | Per-model input cap. Built-in defaults cover common OpenAI / Cohere / Voyage / BAAI / Nomic / E5 / Mistral models; unknown models get 6000. A 90% safety margin is applied automatically. |
| `tokenizer` | auto | Token-count strategy. Auto picks `openai` (exact, via jtokkit) when the model name matches an OpenAI family, otherwise `heuristic`. Set explicitly to override. |
| `tokensPerCharRatio` | `2.0` | Chars-per-token for the heuristic counter. Lower is more conservative (more splits); raise to `~3.5` for predictable Latin prose. Ignored when `tokenizer=openai`. |
| `oversize` | `split` | What to do with chunks over the cap: `split` (lossless — fan one input into N sub-chunks, token-boundary with `openai`, char-boundary with heuristic), `truncate` (lossy — drop the tail), or `fail` (raise an error naming the chunk index). |
| `retryIndividualOnFailure` | `false` | When the embedding API rejects a batch with a 4xx, retry each chunk one-at-a-time so a single poison chunk doesn't lose the whole batch. Costs N HTTP calls in the failure path. |
The guard always runs. When `maxChunkTokens` is set on the destination's `chunking` config (see [Pipeline configuration](/pipeline-configuration#chunking-config)), the chunker stops merging segments before they cross the token cap, leaving the guard as a true safety net rather than the primary defender.
## MinIO Buckets
The following buckets are created automatically by the `minio-init` container:
| Bucket | Purpose |
| - | - |
| `{environment}-raw` | File upload staging |
| `{environment}-temp` | Temporary processing files |
| `{environment}-data` | Object store destination output |
| `{environment}-config` | Configuration files (validation schemas) |
## MongoDB Collections
| Collection | Purpose |
| - | - |
| `{environment}-pipeline` | Pipeline configurations |
| `{environment}-pipeline-status` | Job processing status |
| `{environment}-archived-metadata` | File ingestion metadata |
| `{environment}-file-notifier-message` | Processed message deduplication |
| `{environment}-data-pull` | Database pull scheduling state |
| `{environment}-pipeline-version` | Append-only pipeline definition versions (pruned per `versionCap`) |
| `{environment}-tap` | Tap configurations |
| `{environment}-tap-version` | Append-only tap definition versions (pruned per `versionCap`) |
| `{environment}-tap-state` | Per-tap incremental-sync state (bookmarks / cursors) |
| `{environment}-tap-log` | Tap run history |
| `{environment}-tap-ledger` | Document-tap ledger of processed documents |
| `{environment}-tap-prompt` | Tap prompt fragments |
| `{environment}-code-repo` | Git repository connections for repo-backed tap scripts |
| `{environment}-codegen-scripts` | Index of each pipeline's stored CodeGen scripts (AI rule, AI transformation): where the script is stored, fingerprint, model, generated-at, ready or pending. The script text is in the object store under `pipeline-scripts//`, or in the code repository under `pipelines//` with the recorded commit |
| `{environment}-run-lineage` | One document per pipeline run (what it read, wrote, and how it ended) |
| `{environment}-column-lineage` | AI-inferred column mappings per pipeline version |
| `{environment}-user` | User accounts (when `USE_USER_AUTH=true`) |
| `{environment}-user-session` | Login sessions (when `USE_USER_AUTH=true`) |
| `{environment}-audit-log` | Audit entries (when `USE_AUDIT_LOG=true`); TTL-expired per `AUDIT_LOG_RETENTION_DAYS` |
| `{environment}-agent-policy` | The [agent policy](/agent-policy) document (when `USE_AGENT_POLICY=true`) |
| `{environment}-pending-action` | Agent actions queued for approval; TTL-expired at their own expiry |
| `{environment}-incident` | Recovery-agent incidents (when `RECOVERY_AGENT_ENABLED=true`) — the operational history behind Activity → Incidents |
## Container Environment Variables
These environment variables are read by `docker-compose.yml` and passed into the `datris` container. They tune deployment-level concerns rather than business logic. Override in your `.env` file. The four governance controls below (`USE_USER_AUTH`, `USE_API_KEYS`, `USE_AUDIT_LOG`, `USE_AGENT_POLICY`) stay `false` by default; for production switch them on together as described in [Switch on the governance controls](/quick-start#2-switch-on-the-governance-controls).
| Variable | Default | Description |
| - | - | - |
| `JAVA_OPTS` | `-Xms512m -Xmx2g` | JVM heap sizing for the datris-server process. The default is a 2 GB max heap: the minimal install (`TEI_ENABLED=0`, `POSTGRES_ENABLED=0`) fits a 4 GB Docker memory allocation, and the full stack with the bundled TEI embedder needs about 8 GB. Bump on larger hosts (e.g. `-Xms1g -Xmx8g` for 24 GB). See [Installation → JVM Heap Sizing](/installation#jvm-heap-sizing) for the full sizing table and Docker Desktop ceiling notes. |
| `PIPELINE_MAX_PAYLOAD_MB` | `4096` | Override `pipelineMaxPayloadMB`: the per-run payload disk budget (MB) for tap runs, `0` = unlimited. Bounds what one run may write to the `datris-staging` volume, not JVM heap. Size it from the data: about 26 bytes × column count per staged record for numeric/timestamp/short-string data. Raise for large single-shot backfills on hosts with the disk to hold them; lower on disk-constrained deployments to fail earlier. Compose forwards it without a default, so leaving it out of `.env` lets the server apply 4096 (or the deprecated `TAP_MAX_OUTPUT_MB` when that is set) and report which one was in force as `pipelineMaxPayloadMBSource` on `GET /api/v1/version`. |
| `TAP_MAX_OUTPUT_MB` | unset | **Deprecated** alias of `PIPELINE_MAX_PAYLOAD_MB`, honoured for one release when `PIPELINE_MAX_PAYLOAD_MB` is unset; the server logs a deprecation warning at boot while the alias is set. |
| `TAP_SCRIPT_TIMEOUT_SECONDS` | `300` | Override `tapScriptTimeoutSeconds`: how long a tap **test** may run before it is killed, in seconds. Keep it short so a broken script fails in seconds. A timed-out test names this variable in its error. Compose forwards it without a default, so leaving it out of `.env` lets the server apply 300 and report `tapScriptTimeoutSecondsSource` as `default` on `GET /api/v1/version`. |
| `TAP_RUN_TIMEOUT_SECONDS` | `3600` (or `tapScriptTimeoutSeconds` when higher) | Override `tapRunTimeoutSeconds`: how long a real or scheduled tap **run** may take, in seconds. Raise it when a large streaming source needs more than an hour to land in one run; a timed-out run names this variable in its error. Compose forwards it without a default, so leaving it out of `.env` resolves to `max(3600, tapScriptTimeoutSeconds)` rather than a flat 3600. |
| `TAP_RUNNER_QUEUE_TIMEOUT_SECONDS` | `600` | How long a tap run or generated script waits for a free runner slot when the runner is busy, in seconds. A tap run that waited shows `queued N s` in its run history; one that still has no slot after this fails naming `TAP_RUNNER_MAX_CONCURRENT`. See [Overload protection](/tap-execution-isolation#overload-protection). |
| `TAP_RUNNER_MAX_CONCURRENT` / `CODEGEN_RUNNER_MAX_CONCURRENT` | `4` / `4` | Passed to the `datris-tap-runner` / `datris-codegen-runner` containers (not to `datris`): the most scripts each runs at once. |
| `TAP_RUNNER_SCRIPT_MEMORY_MB` / `CODEGEN_RUNNER_SCRIPT_MEMORY_MB` | `1024` / `1024` | Passed to the runner containers: address-space limit per script, in MB (`0` = no limit). |
| `TAP_RUNNER_CPUS`, `TAP_RUNNER_MEMORY`, `TAP_RUNNER_PIDS` | `1`, `2g`, `256` | CPU, memory and process ceilings of the `datris-tap-runner` container. Set `TAP_RUNNER_CPUS=2` or more on hosts with the cores; Docker refuses a value above the host's CPU count. |
| `CODEGEN_RUNNER_CPUS`, `CODEGEN_RUNNER_MEMORY`, `CODEGEN_RUNNER_PIDS` | `1`, `1g`, `128` | CPU, memory and process ceilings of the `datris-codegen-runner` container. |
| `DATRIS_MAX_UPLOAD_MB` | `1024` | Largest single file `POST /api/v1/pipeline/upload` accepts, in MB (`spring.servlet.multipart.max-file-size` / `max-request-size`). Deliberately separate from `PIPELINE_MAX_PAYLOAD_MB`: the upload is received before the API key is checked, so this ceiling bounds pre-auth disk exposure and stays small. Raise it to accept multi-GB uploads; the run itself is then bounded by `PIPELINE_MAX_PAYLOAD_MB`. |
| `DATRIS_TEMP_DIR` | `/tmp/datris-staging` | Local directory where a pipeline run's payload is staged on disk (one sub-directory per run, removed when the run ends; stale runs are swept at boot). Backed by the named `datris-staging` volume in the default compose file. |
| `PIPELINE_MATERIALIZE_MAX_MB` | `256` | Largest staged payload (MB) a pipeline feature that still reads the whole payload into memory may materialize: deduplication, JavaScript row functions, JSON/XML schema validation, and a REST preprocessor or `restEndpoint` row function in batch mode with no batch size (one call carrying every row). Above it the run fails with an error naming the feature and this variable — ` reads the whole payload into memory (N bytes) and PIPELINE_MATERIALIZE_MAX_MB is 256 MB. Raise it for this install, or use a CodeGen rule/transformation, which streams.` — instead of exhausting the JVM heap. Loaders, provenance stamping, CodeGen rules/transformations and batched REST calls stream the staged file and are never subject to it. Raise together with `JAVA_OPTS` on larger hosts. |
| `SCRATCH_INLINE_ROWS` | `200` | Override `scratchInlineRows`: rows inlined on a Live Read run's status and the maximum `limit` per `/pipeline/result` page. |
| `SCRATCH_RETENTION_HOURS` | `24` | Override `scratchRetentionHours`: hours a Live Read result object survives before the hourly sweep deletes it. Live Read results hold whatever the source returned, so shorten this rather than lengthen it on shared hosts. |
| `USE_USER_AUTH` | `false` | Enable username/password login + admin gating. See [User Authentication](/user-auth). |
| `OIDC_ENABLED` | `false` | Single sign-on through your identity provider; needs `USE_USER_AUTH=true`. With `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_REDIRECT_URI`, `OIDC_SCOPES` (`openid email profile`), `OIDC_USERNAME_CLAIM` (`email`) and `OIDC_DEFAULT_ROLE` (empty). Client secret in Vault, `oss/oidc` key `clientSecret`. See [Single sign-on (OIDC)](/user-auth#single-sign-on-oidc). |
| `USE_API_KEYS` | `false` | Enable per-tenant API-key gating on the REST/MCP API. See [API Keys](/api-keys). |
| `MULTI_TENANT` | `false` | Advanced: switch to per-tenant database isolation, with each request routed by its API key. Requires `USE_API_KEYS=true`. |
| `ANTHROPIC_OVERLOAD_FALLBACK_MODEL` | `claude-sonnet-5` | Model used for the current request when Anthropic returns a sustained `overloaded_error` on an Opus call (after the automatic retries). Only consulted when the configured model is an Anthropic Opus variant — other providers ignore it. Bump when a newer Sonnet ships to keep the fallback current. |
| `AI_HTTP_MAX_PER_ROUTE` | `96` | Maximum concurrent connections the server's shared AI-provider and MCP clients open to one host. Every chat panel and the Assistant stream to the same provider host, so this is the limit concurrent chats hit first. The default covers the five chat panels at full load (80 concurrent streams) with headroom; when it is exhausted, the AI clients wait up to 10 seconds for a free connection and then fail with `Timeout waiting for connection from pool`. A blank, non-numeric or non-positive value uses the default. |
| `AI_HTTP_MAX_TOTAL` | `192` | Maximum concurrent connections across all hosts for the same clients. Never lower than `AI_HTTP_MAX_PER_ROUTE`; a smaller value is raised to match. A blank, non-numeric or non-positive value uses the default. |
| `CAPABILITY_ENFORCEMENT` | `enforce` | `enforce` returns HTTP 403 on policy violations by scoped keys. Set to `log-only` to emit would-deny log lines without rejecting — useful when iterating on a new scope policy. |
| `USE_AUDIT_LOG` | `false` | Enable the [Audit Log](/audit-log) — who did what, by login or API key. |
| `AUDIT_LOG_RETENTION_DAYS` | `90` | Days to keep audit entries; `0` = forever. |
| `AUDIT_LOG_READS` | `false` | Also record reads (queries, searches, listings). |
| `AUDIT_LOG_STRICT` | `false` | Refuse audited requests (503) instead of dropping audit entries when the config store cannot keep up. Needs `USE_AUDIT_LOG=true`. See [Audit Log](/audit-log#strict-mode). |
| `PROVENANCE_STRICT` | `false` | Fail a stamping pipeline's run instead of loading unstamped rows. XML sources are exempt and load unstamped. See [Provenance](/provenance#strict-mode). |
| `USE_AGENT_POLICY` | `false` | Enable the [Agent Policy](/agent-policy) — auto / approve / deny per agent action, with a human approval queue. |
| `RECOVERY_AGENT_ENABLED` | `false` | Enable the [recovery agent](/incidents) — platform-opened incidents with a bounded repair loop. |
| `INCIDENT_WEBHOOK_URL` | *(empty)* | Optional JSON webhook for incident lifecycle events and periodic-doctor flips. |
| `DOCTOR_ONSTARTUP` | `true` | Boot-time [doctor](/doctor) lines. |
| `DOCTOR_INTERVAL_MINUTES` | `0` | Periodic doctor run; `0` = off. |
# Configuring Claude
Source: https://docs.datris.ai/configuring-claude
Connect Claude Desktop and Claude Code to the Datris MCP server
This page covers the client-side configuration for Anthropic's Claude products to talk to the Datris MCP server. For server-side details (architecture, transports, available tools), see [MCP Server (AI Agent Integration)](/mcp-server).
## Authentication
The MCP server is a transparent forwarder — it has no API key of its own. When auth is enabled, the client sends an `x-api-key` header on every session and the MCP server forwards that key to the Datris REST API on every tool call.
* **Single-tenant OSS with `USE_API_KEYS=false`** (default): no key needed. The snippets below work as-is.
* **Single-tenant OSS with `USE_API_KEYS=true`**: append `--header x-api-key:` to the `args` array. Issue and rotate keys from **Configuration → API-Keys** in the UI — the value shown when you create a key is what your client sends.
The Configuration UI's **Connect Your Agent** panel pre-generates these snippets — paste your API key into the field there and the `--header` flag gets added automatically.
## Configuring Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows), then quit and relaunch Claude Desktop.
### Recommended: mcp-remote bridge (Docker stack)
Run the bundled MCP server through the [mcp-remote](https://www.npmjs.com/package/mcp-remote) stdio bridge. It transparently reconnects when the server restarts and lets every desktop agent (Claude Desktop, Cursor, Claude Code) share one snippet shape:
```json theme={null}
{
"mcpServers": {
"datris": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:3000/sse",
"--transport",
"sse-only"
]
}
}
}
```
The underlying connection is SSE to port 3000, so sessions register with the Datris server and the **Agent Monitor** tab in the Datris UI shows live tool-call streaming. `--transport sse-only` pins the protocol so the bridge doesn't try its streamable-HTTP fallback.
Requires Node.js on your `PATH` (`brew install node`). On macOS, Claude Desktop is a GUI app and doesn't inherit your shell `PATH`, so use the full path to `npx` (e.g. `/opt/homebrew/bin/npx`) if `npx` alone doesn't resolve.
If your instance requires an API key (`USE_API_KEYS=true`), append two more args:
```json theme={null}
"args": [
"-y", "mcp-remote", "http://localhost:3000/sse",
"--transport", "sse-only",
"--header", "x-api-key:"
]
```
### Alternative: stdio via uvx (no Docker)
If you're not running the Docker stack — for example, you only want the MCP server and run the Datris API separately — use the PyPI package directly:
```json theme={null}
{
"mcpServers": {
"datris": {
"command": "uvx",
"args": ["datris-mcp-server"],
"env": {
"DATRIS_API_URL": "http://localhost:8080"
}
}
}
}
```
Requires `uv` / `uvx` (`brew install uv`). On macOS, Claude Desktop is a GUI app and doesn't inherit your shell PATH, so use the full path to `uvx` (e.g. `/Users/you/.local/bin/uvx`) if `uvx` alone doesn't resolve.
The stdio transport has no header layer, so the API key comes from the environment instead: if your Datris instance has `USE_API_KEYS=true`, add `"DATRIS_API_KEY": ""` to the `env` block (or use the mcp-remote option above). With `USE_API_KEYS=false` (single-tenant OSS default), stdio works fine without a key, but tool calls won't appear in the Agent Monitor tab.
## Configuring Claude Code
Add to `.mcp.json` in your project root. The same two options apply.
### Recommended: mcp-remote bridge (Docker stack)
```json theme={null}
{
"mcpServers": {
"datris": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:3000/sse",
"--transport",
"sse-only"
]
}
}
}
```
To add an API key (`USE_API_KEYS=true`), append `"--header", "x-api-key:"` to the args array.
### Alternative: stdio via uvx (no Docker)
```json theme={null}
{
"mcpServers": {
"datris": {
"command": "uvx",
"args": ["datris-mcp-server"],
"env": {
"DATRIS_API_URL": "http://localhost:8080"
}
}
}
}
```
## Give Claude the Datris skill
Connecting the MCP server gives Claude the tools. The [datris-platform skill](/agent-skill) tells Claude when to use them during ordinary project work: it triggers on data ingestion, storage, search, scheduling and provenance tasks and steers Claude to Datris instead of hand-written loaders, cron jobs or vector-store code. Copy it into `.claude/skills/` in the project, or into `~/.claude/skills/` for every project.
## Talking to Claude
Once the MCP server is configured, just talk to Claude naturally. Claude sees every Datris tool and picks the right one based on your request.
### First prompts (empty Datris)
If you've just installed Datris and have no data yet, these prompts exercise the full create → query loop without needing files on hand. Watch the **Agent Monitor** tab in the Datris UI as you go — you'll see each tool call stream in real time.
**1. Confirm discovery works (returns empty, but proves the round-trip):**
> "List my Datris pipelines and tell me what's there."
**2. Have Claude generate sample data and ingest it:**
> "Generate 50 rows of sample stock price data (symbol, date, open, close, volume) and ingest it into PostgreSQL as a pipeline called `stock_prices`."
This single prompt exercises `create_pipeline` (with destination schema), the actual ingest, and several supporting tools — multiple tool calls light up the activity log.
**3. Query the data you just created:**
> "Show me the top 10 rows in `stock_prices` by volume."
**4. Add AI-generated data quality rules:**
> "Profile the `stock_prices` pipeline and suggest data quality rules I should add."
This exercises the AI/CodeGen path — Claude writes Python validators, Datris runs them locally.
**5. Read the pipeline configuration resource:**
> "Read the Datris pipeline configuration reference and summarize what destination types are supported."
Confirms Claude can pull MCP resources (not just tools) — useful to verify your client supports both.
For the RAG path, ingest the document **outside** the chat (see the warning below), then ask Claude to search it:
```bash theme={null}
# from your terminal
datris ingest annual-report.pdf --dest pgvector --pipeline reports
```
> "Search the `reports` pipeline for \[topic from the PDF]."
### Ingest data
> "Create a pipeline called stock\_prices and ingest this CSV into PostgreSQL"
> "Ingest this sales data into PostgreSQL and validate that all prices are positive"
**Don't drag large local files into the chat to ingest them.** When you say "ingest this PDF" with a file attached, Claude has to base64-encode the bytes and pass them as a string argument to the `upload_data` tool — that string traverses the conversation context, and a 300KB PDF alone consumes \~90K tokens before any work happens. Conversations hit "too long to continue" almost immediately.
Use the right tool for each shape of input:
* **Local files** of any size — use the CLI: `datris ingest --dest `. No LLM, no context cost.
* **Files at a URL** (S3, MinIO, HTTPS) — ask Claude to create a Datris **tap** that fetches them. Bytes are pulled server-side; the agent only handles the URL.
* **Small inline data** (a few rows of CSV, a JSON snippet, generated sample data) — fine to drop into the chat.
The chat path is for *natural-language operations* (querying, profiling, pipeline management, search), not for funneling local-disk files through a remote conversation.
### Query data
> "Show me the top 10 stocks by volume from the stock\_prices table"
> "What's the average closing price for AAPL?"
> "How many orders were placed in March?"
### Search documents (RAG)
> "Search my financial documents for information about quarterly revenue"
> "What does the employee handbook say about the return policy?"
### Profile data
> "Profile this CSV file and tell me what validation rules I should add"
> "What does this data look like? Give me a summary"
### Manage pipelines
> "List all my pipelines"
> "Delete the test\_pipeline"
> "What's the status of the stock\_prices pipeline?"
### Explore databases
> "What tables are in my PostgreSQL database?"
> "Show me the columns in the orders table"
> "What vector collections do I have?"
### Health and diagnostics
> "Are all the backend services running?"
> "Check the health of the platform"
You can drag small inline files (a CSV with tens of rows, a short JSON snippet) directly into the chat for ingestion or profiling. For larger or binary files, use the CLI or a tap (see the warning above).
# Configuring OpenClaw
Source: https://docs.datris.ai/configuring-openclaw
Use the Datris Platform as OpenClaw's long-term memory and semantic search layer
This page covers connecting [OpenClaw](https://openclaw.ai) to a Datris MCP server so the agent can use Datris as its **long-term semantic memory** — local memory files stay canonical, while Datris becomes the retrieval/index layer.
For server-side details (architecture, transports, available tools), see [MCP Server (AI Agent Integration)](/mcp-server).
## Why route OpenClaw memory through Datris
This isn't a "no memory vs. memory" comparison. OpenClaw already has memory. It's a comparison about *enforcement, scale, and reuse*.
The short version:
1. **OpenClaw's built-in memory is best-effort** — the model decides when to save and when to look — so long sessions, restarts, and context compaction quietly erode recall. Datris moves memory out of the prompt and enforces it at the system layer, so your agent stops forgetting.
2. **The same MCP connection that gives OpenClaw enforced semantic memory also exposes 70+ Datris tools** — pipelines, ingestion, SQL, vector search — so you're not bolting on a memory product, you're handing your agent a full data platform.
### vs. OpenClaw's default memory (best-effort, not enforced)
OpenClaw ships with persistent memory out of the box — daily memory files, a curated `MEMORY.md`, and semantic search over archived sessions. It works. The catch is that everything is advisory: the model decides when to save, what to save, and whether to search before answering. Long sessions, context compaction, and restarts all chip away at recall. You'll see it fail the same way every time — a fact you established in week one stops surfacing in week three, not because it's gone, but because the model didn't think to look for it.
Datris-backed memory enforces capture and retrieval at the system layer instead of leaving it to the prompt. Memory is written outside the agent's session, and relevant context is reintroduced on every turn. Restarts don't matter. Long conversations don't matter. The agent reasons with the same facts every time.
### vs. flat file-based memory (raw markdown, JSON blobs)
Hand-rolled file memory — a `PREFERENCES.md` here, a `CONTEXT.md` there, a `notes/` folder with grep on top — is the path most people start down. It works for a week. Past a few dozen entries the agent ends up either loading whole files into context or substring-matching for keywords that may not appear in the original note. A question phrased as *"what did we decide about the pipeline architecture?"* misses notes filed under "ingestion topology" or "data flow design."
Datris stores memories in pgvector (or Qdrant, Weaviate, Milvus, Chroma — your call) and retrieves by meaning, not exact wording. The same question finds the right note regardless of how it was originally phrased.
### vs. MemGPT/Letta-class agent runtimes
MemGPT (now Letta) does semantic memory well — that's the system that popularized the LLM-as-OS framing for agent memory. The trade-off is that Letta is a runtime, not a memory layer. Adopting it means running your agent inside their framework: their agent loop, their tool execution, their state model. That's a fine choice from a clean slate. It's a heavy migration if you already have an agent you like — including OpenClaw.
Datris is a memory layer exposed over MCP. OpenClaw stays OpenClaw. Your agent loop, your tools, your skills — all unchanged. Point your MCP client at Datris and the agent gains enforced, semantic, persistent memory without rewriting anything.
### What you actually get
Indexing local memory files into Datris gives you the best of both worlds — markdown stays human-editable and version-controllable, while Datris handles retrieval at scale.
* **Agent-runtime-agnostic.** Datris exposes memory through MCP, so OpenClaw, Claude Desktop, Cursor, or any custom MCP client can point at the same instance and share the same memory. No bespoke integration per agent, no rewriting your agent loop to match someone else's runtime.
* **Tool-rich, not just retrieval.** The same MCP connection that gives OpenClaw memory also exposes pipeline management, ingestion, and SQL and vector query tools — over 70 in total. Memory-only products stop at recall; Datris hands the agent the rest of a data platform on the same connection.
* **Auditable.** The **Agent Monitor** tab in the Datris UI shows OpenClaw's memory tool calls as they happen, so you can see exactly what the agent searched for, what came back, and what it stored.
* **Semantic recall, not substring search.** Ask in natural language and get the right snippet back even when the wording doesn't match — *"what did we decide about the pipeline architecture?"* finds the note filed under "ingestion topology."
* **Scales past the context window.** Large memory corpora that would never fit into a single prompt become retrievable on demand. The agent pulls only what's relevant for the current turn.
* **Files remain the source of truth.** For memory you author yourself, the markdown files are yours — Datris is a derived index. Edit locally, re-ingest when ready. No vendor lock-in on your notes.
## Step 1 — Set the embedding model to OpenAI `text-embedding-3-small`
Before ingesting any memory, pick the embedding model the pipeline will use to encode it. For long-term memory we recommend **OpenAI** with **`text-embedding-3-small`** — it's the best balance of recall quality, latency, and cost for this use case in our testing.
In the Datris UI:
1. Open the **Configuration** tab, then the **AI Providers** sub-tab.
2. In the **Embedding Provider** section, choose **OpenAI** as the provider and **`text-embedding-3-small`** as the model.
3. Enter your OpenAI API key in the right-hand **API Keys** panel (if you haven't already).
4. Click **Save Configuration**. Changes take effect immediately — no restart required.
**Set the embedder before you create the memory pipeline.** Vector dimensions are pinned at pipeline-creation time — `bge-m3` (the bundled default) is 1024-dim, `text-embedding-3-small` is 1536-dim. Switching embedders after ingestion will fail-fast with a dimension-mismatch error and you'll have to drop the destination collection and re-ingest. See [AI Configuration](/ai-configuration) for the full provider matrix.
## Step 2 — Install the Datris CLI
The memory skill prefers MCP tools but falls back to the `datris` CLI for health checks, stalled-job recovery, and operating when the MCP server is unreachable. It also declares `datris` as a required binary in its install metadata, so it has to be on your `PATH` before the skill will activate cleanly.
Install via Homebrew:
```bash theme={null}
brew tap datris/tap
brew install datris
```
Verify:
```bash theme={null}
datris health
```
You should see every backend service report healthy. See [docs.datris.ai/cli](/cli) for the full command reference.
## Step 3 — Register the Datris MCP server with OpenClaw
Point OpenClaw at the local Datris MCP server (the SSE endpoint exposed by the standard Docker stack on port 3000):
```bash theme={null}
openclaw mcp set datris '{"url":"http://localhost:3000/sse"}'
```
Verify the registration:
```bash theme={null}
openclaw mcp show datris
```
You should see the `datris` server listed with its URL. SSE registration also means OpenClaw will appear in the Datris UI's **Agent Monitor** tab with live tool-call streaming — useful for watching what the agent is actually doing during ingestion and retrieval.
## Step 4 — Install the Datris memory skill
A one-shot prompt isn't enough — memory work is ongoing (bootstrap, incremental sync, retrieval defaults, handling renames and deletes), and the agent needs persistent operating instructions, not just a single bootstrap run. OpenClaw uses **skills** — markdown files under `.openclaw/workspace/skills/` — for exactly this. Install the Datris memory skill once and OpenClaw auto-triggers it whenever the user mentions saving, recalling, ingesting, syncing, or searching memory (and even on memory-shaped questions like *"what did I note about X"* that don't say "Datris" out loud).
You have two options for installing the skill.
### Option A: install via the OpenClaw CLI (recommended)
```bash theme={null}
openclaw skills install datris-memory
```
This pulls the latest published version of the skill, drops it at `~/.openclaw/workspace/skills/datris-memory/SKILL.md`, and you're done. Skip ahead to Step 5.
### Option B: install manually
Create the skill directory:
```bash theme={null}
mkdir -p ~/.openclaw/workspace/skills/datris-memory
```
Then create `~/.openclaw/workspace/skills/datris-memory/SKILL.md` with the contents below:
```markdown theme={null}
---
name: datris-memory
description: Use the Datris Platform as a long-term semantic memory layer through the Datris MCP server. Local markdown memory files (MEMORY.md, memory/*.md) stay canonical; Datris is the retrieval index built from them. Use this skill any time the user mentions saving, recalling, ingesting, syncing, or searching memory through Datris — and any time they ask a memory-shaped question ("what did I note about X", "find anything related to Y", "summarize what I've written down about Z") even when they don't explicitly say "Datris." Trigger on any work involving long-term memory ingestion, retrieval, or sync. Datris MCP is the only memory backend — do not substitute a built-in `memory_search`, `recall`, or host-provided knowledge tool, even when one is available.
---
# Datris memory layer
Datris is the long-term semantic memory layer. Local memory files are the source of truth. Datris is rebuilt from them — never the other way around.
## Rules
- **Datris MCP is the only memory backend.** If the host exposes a built-in memory tool (`memory_search`, `recall`, an internal RAG store, a sibling skill's memory index, etc.), do not use it for memory-shaped queries. Those tools index a different corpus and produce divergent results — the local markdown files are the source of truth and Datris is the only retrieval index built from them. There is no third channel.
- Prefer Datris MCP tools over shell or CLI for memory operations.
- **One file in = one `upload_data` call.** Never concatenate, bundle, or consolidate multiple memory files into a single corpus upload. Each file's name is its provenance — it must round-trip cleanly into Datris and back out in retrieval results. Bundling looks like an optimization and is not one: it permanently breaks provenance, blocks per-file incremental sync, and makes resets harder.
- **Source filenames preserve the full relative path.** Use `MEMORY.md` and `memory/2026-05-06.md` as source filenames — the directory prefix is part of the provenance and must round-trip into retrieval results. Some Datris builds reject path separators at the temp-file staging layer; if uploads fail on slashes, encode the separator reversibly (e.g., `memory__2026-05-06.md`) and tell the user what encoding was applied. Never silently flatten to a basename — that strips the directory and creates ambiguity if two memory subdirectories ever contain files with the same name.
- Upload each file as-is. Let Datris chunk server-side. Do not pre-chunk. Do not use a document tap for local files. The only time to split a single file is when that one file genuinely exceeds the upload limit; in that case, split it with explicit provenance markers in the chunk filenames (e.g. `MEMORY.md.part1`, `MEMORY.md.part2`).
- **Run uploads concurrently across files.** Each `upload_data` call returns a job token immediately — collect all the tokens first, then poll. Use a bounded concurrency limit (8–16 parallel uploads is safe for most Datris builds) rather than unbounded fan-out. On bootstrap, where there are no pre-existing chunks to remove, this is straight parallelism.
- **Re-uploads are delete-then-upload, serialized per file.** When re-ingesting a file that already exists in the pipeline, delete its existing chunks first, wait for the delete to complete, then upload. Do not rely on upsert behavior — some Datris builds append duplicates rather than overwriting on filename collision, and a delete-then-upload pair is build-agnostic and idempotent. Within a single file, delete→upload is serial. Across files, multiple delete→upload pairs run concurrently up to the same bounded limit.
- Poll job status to completion before claiming any ingestion succeeded. Polling is for verification across the batch, not for gating cross-file uploads.
- Verify retrieval with a semantic search after every ingestion run.
- Memory pipelines must target a vector destination (pgvector, Qdrant, Weaviate, Milvus, or Chroma).
- The embedding model is pinned at pipeline-creation time. Vector dimensions cannot change after the fact. Confirm the embedder matches the pipeline before ingesting. Switching embedders means dropping and recreating the destination collection.
- **Place every created resource in the `openclaw` data catalog by default.** Pipelines, taps, secrets, destination collections — anything the agent creates in Datris on OpenClaw's behalf goes in the catalog named `openclaw` unless the user explicitly directs otherwise. This keeps OpenClaw's footprint cleanly separated from other Datris workloads on the same instance, makes cleanup and auditing trivial, and lets multiple OpenClaw users share an instance without colliding. If an existing resource the agent wants to reuse lives in a different catalog, do not migrate it silently — surface the mismatch to the user and ask before continuing.
- **Return to prescribed concurrency after debugging.** When isolating a failure — a Datris bug, a tool returning unexpected output, a transient error — it is reasonable to drop temporarily to single-file sequential operations to get a clean signal. Once the issue is identified and resolved, return to the concurrency model in the rules above before processing the rest of the batch. Do not carry a debug-mode sequential pattern into subsequent ingestion or sync runs. Note in the audit log when this happens, including the reason and what was done.
## First-run bootstrap
1. Read the Datris MCP resources and tool descriptions. Understand the pipeline, upload, job-status, and search workflows before acting.
2. Check service health.
3. Reuse an existing vector pipeline for memory in the `openclaw` catalog if one exists. Otherwise create one in the `openclaw` catalog — pgvector is fine. If a memory pipeline exists outside the `openclaw` catalog, surface that to the user before reusing or migrating it.
4. Ingest `MEMORY.md` and each `memory/*.md` file via its own `upload_data` call — one upload per file, no exceptions. Do not concatenate them into a single corpus document, even if the total set is small. Fire the uploads in parallel and collect all the job tokens before polling.
5. Poll all jobs concurrently until every one reports completion.
6. Verify with two or three representative semantic queries. Confirm both that (a) the expected content comes back, and (b) results show real source filenames (`MEMORY.md`, `memory/2026-05-06.md`, etc.) — not a consolidated corpus filename or any other synthetic name. The filename round-trip check is the early-warning signal that the one-file-per-upload rule is being followed; if results show a corpus filename, stop and apply the remediation workflow below.
7. Record the run in `memory/.md`: pipeline used, files ingested, verification queries and results, any failures.
8. Propose an incremental sync strategy for future edits.
## Ongoing sync
Memory files change continuously — the agent writes to them during sessions, the user edits them in their editor between sessions, and new dated files appear over time. Sync is incremental and runs lazily, in three modes:
### When to sync
1. **Periodic background sync.** A timer-driven sweep runs every 30 minutes by default — diff memory files by `mtime` against the last sync record, upload anything stale. This runs out-of-band: it never blocks an agent response or a user query. Cadence is configurable: faster (5–10 min) for users actively editing memory between sessions, slower (hourly or more) for read-mostly use. The right value is whatever keeps the staleness window short enough that the user rarely needs to force a sync.
2. **End of any agent response that wrote to memory.** When the agent edits or creates a memory file during a turn, flush those uploads before the response is considered complete — including polling to completion. This puts the cost on the response that did the writing, not on later queries, and guarantees that a follow-up question in the same conversation can retrieve what the agent just wrote. Never let an agent-authored memory write wait for the next timer tick.
3. **On explicit user request.** Phrases like "sync memory," "save what we discussed," "update Datris with my recent notes" — full diff sweep across all memory files, immediate sync. This is the user's escape valve for the case where they just edited a file in their editor and want it queryable right now rather than waiting for the next timer tick.
### Staleness window — and why it's acceptable
Memory edits made outside of an agent session — for example, the user editing `MEMORY.md` in their editor between conversations — may be up to one timer interval behind in retrieval results. That is the explicit trade. Query latency stays predictable, ingestion is invisible, and the user has a one-line escape hatch ("sync memory") when freshness matters. Do not try to close the staleness window by syncing on every retrieval; that path puts ingestion cost on the user's wait time and is the design this skill replaces.
### Detecting what changed
Compare each memory file's filesystem `mtime` against the most recent sync timestamp recorded for that file in the `memory/.md` audit logs. Three cases:
- **No sync record exists** → treat as a new file, ingest it.
- **`mtime` newer than last-sync timestamp** → re-upload.
- **`mtime` unchanged** → skip. Do not re-ingest unchanged files; the point of incremental sync is to do less work.
A content hash is more reliable than `mtime` if the user touches files without editing them (some editors do this on save), but `mtime` is sufficient as a default. Switch to hashing only if redundant uploads start showing up in the audit log.
### Sync workflow
Classify each changed file into one of four cases and act accordingly:
- **Edited file** → delete existing chunks for that source filename, wait for the delete to complete, then upload via `upload_data`. Per-file serial; multiple files' delete→upload pairs run concurrently.
- **New file** (no prior sync record) → upload via `upload_data` directly. No delete step needed since there are no existing chunks for this filename.
- **Renamed file** → delete chunks for the old filename, then upload the new file. Same per-file serialization as edits, same cross-file concurrency.
- **Deleted file** → delete its chunks from the destination collection. Do not leave orphans.
Then:
- Wait for all jobs to complete, polling concurrently.
- Verify with one or two semantic queries that touch the changed content. Confirm filenames in results reflect the post-sync state — no stale entries from before a rename or delete, no consolidated-corpus names, no duplicate chunks (same content under the same filename appearing more than once would indicate the delete step was skipped).
- Append a per-file entry to today's audit log: filename, change type (edit / new / rename / delete), timestamp, verification result.
## Inheriting a consolidated-corpus pipeline
If the existing memory pipeline was bootstrapped with a single consolidated upload (a `*-corpus-*.md` source file, or any upload whose filename doesn't match a real memory file), the pipeline has broken provenance and cannot be incrementally synced cleanly. Do not patch around it. Reset and re-ingest:
1. Confirm with the user before resetting — destination collections may contain manual edits.
2. Drop the destination collection (or recreate the pipeline with the same name).
3. Re-ingest each canonical memory file individually per the bootstrap workflow above.
4. Verify with semantic queries that retrieval results now show real source filenames (`MEMORY.md`, `memory/2026-05-06.md`, etc.) rather than a corpus filename.
## Retrieval
When the user asks a memory-shaped question:
1. Call `vector_search` against the Datris memory pipeline (or `ai_answer` for synthesis).
2. If — and only if — Datris MCP is unreachable, grep local memory files as a fallback. Tell the user you fell back and why.
**Do not** call:
- Built-in agent memory tools (`memory_search`, `recall`, etc.)
- Host-provided knowledge stores or RAG indexes
- Sibling skills' memory features
These index a different corpus than Datris and will return inconsistent results. Before sending your answer, name the tool you actually called in your own reasoning — if it isn't `vector_search`/`ai_answer` against the Datris memory pipeline, stop and re-route.
## Reporting
After any bootstrap or sync, report:
- Pipeline used or created.
- Files ingested.
- Verification queries and whether they returned the expected content.
- Anything that failed and why.
```
### What this skill enforces
* **Auto-triggers on memory intent** — the skill's `description` covers explicit phrasing ("sync memory," "save what we discussed") *and* implicit memory-shaped questions ("what did I note about X"), so the user never has to remember to invoke it.
* **Datris MCP is the only memory backend** — built-in agent memory tools, host-provided knowledge stores and sibling skills' memory features are never used for memory-shaped queries; local files are grepped only if Datris MCP is unreachable, and the agent says so.
* **One file in = one upload** — no bundling, no concatenation. Filenames (including their directory prefix) round-trip into retrieval results so provenance is preserved and per-file incremental sync stays clean.
* **Bounded parallel uploads, then poll** — fires `upload_data` calls concurrently (8–16 at a time), collects job tokens, polls concurrently. Re-uploads are delete-then-upload, serialized per file.
* **Catalog isolation** — every resource the agent creates lands in the `openclaw` data catalog by default, keeping OpenClaw's footprint separate from other workloads on the same Datris instance.
* **Three-mode incremental sync** — periodic timer sweep (default 30 min), end-of-response flush for agent-authored writes, and explicit user "sync memory" trigger. Renames go through delete-then-add so the index never accumulates orphans.
* **Self-healing for inherited corpora** — if a previous setup bundled everything into one file, the skill includes the reset-and-re-ingest workflow rather than papering over broken provenance.
## Step 5 — Restart the OpenClaw gateway
Skills are discovered at gateway startup, so the new `datris-memory` skill won't be active until you reload:
```bash theme={null}
openclaw gateway restart
```
After the restart, OpenClaw will pick up `~/.openclaw/workspace/skills/datris-memory/SKILL.md` and auto-trigger it on any memory-shaped request.
## Step 6 — Kick off the bootstrap, then use it
With the skill loaded, point OpenClaw at it once so it runs the first-time bootstrap — health check, pipeline creation, parallel ingestion of every existing memory file, and verification. A simple opener works:
> "You now have the `datris-memory` skill installed. Use it to ingest my existing memory into Datris."
Watch the **Agent Monitor** tab in the Datris UI as it runs — you'll see each `upload_data` call, `get_job_status` poll, and `search_pgvector` verification stream in real time. When the bootstrap finishes, OpenClaw will write a summary into today's `memory/.md` audit log.
After that, just talk normally. The skill auto-triggers on any memory-shaped request and reaches for Datris semantic search instead of grepping local files:
> "What did I note about my preferred code review process?"
> "Find anything in my memory related to onboarding new teammates."
> "Summarize what I've written down about meeting cadence."
## Advantages of this setup
Beyond the per-feature wins listed at the top, the architectural payoff of putting Datris between OpenClaw and your memory files is:
* **Recall quality scales with corpus size** — adding more memory files makes retrieval *better*, not slower or noisier, because semantic search ranks by relevance instead of dumping everything into context.
* **Context budget stays tight** — only the top-matching snippets land in the prompt, so long-running OpenClaw sessions don't burn their window loading memory the agent doesn't need.
* **One memory layer, many agents** — point Claude Desktop, Claude Code, OpenClaw, or any other MCP-aware client at the same Datris pipeline and they all share the same long-term memory. No per-tool re-indexing.
* **Local-first, not cloud-locked** — your markdown stays on disk, the index lives in your own Datris stack. You can wipe and rebuild the index any time without losing knowledge.
* **Memory becomes queryable, not just retrievable** — because it's a real Datris pipeline, you can run vector search, AI answer, and even SQL-shaped questions against your memory corpus from the CLI or REST API, not only from inside the agent chat.
* **Future memory is incremental** — the skill's three-mode sync (timer sweep, end-of-response flush, explicit user trigger) re-ingests only the files that changed, never the whole corpus.
The net effect: your agent stops forgetting, your memory files stop being a flat pile of markdown, and you get a real semantic layer over the knowledge you've been writing down all along.
# Data Catalog
Source: https://docs.datris.ai/data-catalog
A logical grouping for related taps and pipelines
**Beta.** Data Catalogs may evolve as we add features like sharing and access control.
A **Data Catalog** is a named bucket that groups related [taps](/taps) and [pipelines](/pipeline-configuration). It's pure metadata — assigning a tap to a catalog doesn't change how the tap runs or where its data lands. Catalogs exist to make a busy platform browsable: instead of a flat list of 40 taps, you see "Stock Prices (12)", "Weather (6)", "Earnings (8)" and so on.
A Datris catalog is not a Databricks Unity Catalog catalog. On a Databricks pipeline, `destination.database.dbName` names the Unity Catalog catalog the table lands in; the Datris catalog is a separate grouping label, carried to Databricks as the `datris_catalog` tag when the Unity Catalog metadata push is on. See [Unity Catalog](/destinations/unity-catalog).
## When to use a catalog
Use one whenever you have more than a handful of related taps that should travel together — same source, same domain, same project.
You don't have to use catalogs. Anything you create outside a catalog falls into the **Uncataloged** group, which works fine for one-off taps and pipelines.
## How catalogs are stored
Catalogs aren't a separate table or external system. Each tap and pipeline carries a nullable `catalog` string field; the platform groups by that field. Two consequences:
* A catalog "exists" as soon as anything is assigned to it. There's no separate create step required to *use* a name.
* Renaming a catalog means updating the `catalog` field on every tap and pipeline in it. The Catalog tab's rename control does this in one step, or call `rename_catalog` over MCP.
When you create a catalog explicitly via the UI, Datris persists the name by writing a placeholder tap named `__catalog__` (description "Catalog placeholder", `enabled=false`). This makes empty catalogs survive across UI reloads. The placeholder is hidden from the Taps list and ignored by the runtime.
## The Catalog tab
The **Catalog** tab in the UI shows one card per catalog plus an **Uncataloged** card.
* Each card shows the catalog name and counts of taps + pipelines.
* **Rename catalog** — the pencil icon on a named catalog's card turns the name into an editable field. Press Enter (or the check mark) to save, Escape to cancel. The new name may use letters (upper or lower case), digits, `_` and `-` only; anything else is converted when you leave the field (spaces become `_`, other characters are dropped). Capitalisation is kept. Every tap and pipeline in the catalog moves to the new name in one step.
* A rename cannot target a catalog that already exists: it is refused and both catalogs stay as they were. To combine two catalogs, use **Move all contents** (the move icon on the source catalog's card) instead.
* If some items could not be moved, the page lists each one with the reason; the rest are renamed. If the catalog was renamed or deleted from somewhere else after the page loaded, the rename or delete changes nothing, says the catalog was not found, and refreshes the list. An item that someone moved to another catalog while the rename ran is reported as no longer in the catalog; refresh the page to see where it is now.
* API keys whose capabilities are scoped to the old catalog name keep that scope and no longer match the renamed items. After such a rename a banner names those keys; update them under **Configuration → Keys**.
* Catalog names may be mixed case (for example `DatrisFund` or `Sales_Q3`), and they are matched case-sensitively. A rename to a name that differs from another existing catalog only by case (for example `datrisfund` when `DatrisFund` exists) is refused with a message that the catalog already exists with different capitalisation, because it would create a second catalog next to it. A catalog can still be renamed to a different capitalisation of its own name.
* Click a card to expand it. Inside you'll see:
* Each tap with its enabled/disabled status, last-run outcome, and record count.
* Each pipeline with a quick-jump link to its config.
* **+ Create Catalog** — make a new empty catalog. The name is sanitized to letters, digits, `_` and `-` (spaces become `_`); capitalisation is kept. A name that differs from an existing catalog only by capitalisation is refused, here and in the new-catalog field of the tap and pipeline forms; typing an existing name exactly in those forms selects that catalog.
* **Delete catalog, keep items** — the delete icon on a named catalog's card opens a confirm with two choices. **Keep items (move to Uncataloged)** is the default: the catalog is removed and every tap and pipeline in it moves to Uncataloged. Nothing is deleted.
* **Delete catalog, delete items and their data** — the second choice deletes the catalog **plus every tap and pipeline inside it, including the data already written to their destinations**. This is destructive and not reversible, so **Delete** stays disabled until you type the catalog name exactly (case-sensitive). If some items could not be deleted, the page lists each one with the reason.
* **Download catalog** — the download icon on a named catalog's card saves the catalog as a zip archive (see [What an archive contains](#what-an-archive-contains)). The archive holds no secret values and no data, so it is safe to keep alongside other configuration backups.
* **Import catalog** — the **Import catalog** button next to **+ Create Catalog** restores an archive. Pick the zip (the panel shows its name and size), enter the catalog name to restore into (it defaults to the file name), and choose:
* **Create new catalog (refuse if it exists)** — the default. The restore is refused when the catalog already exists or any archived tap or pipeline name already exists on this install; the page lists each clash and nothing changes. A restore never merges into an existing catalog.
* **Overwrite catalog** — replaces the items already in that catalog. **Restore** stays disabled until you type the catalog name exactly.
* **Skip incremental tap state (for a clone)** — restores the taps without the source's incremental cursor, so a copy starts from scratch.
After the restore the page shows how many taps and pipelines came back, names any secrets they need that do not exist yet, lists items that could not be restored with the reason, and expands the restored catalog. Restored taps are disabled: test each one, then enable it. When some restored taps read tables produced by other restored taps, the result says how many: run their sources first, in the [enable order](/taps#dependencies-between-taps).
* **Enable order hints** — in an expanded catalog, each disabled tap shows its level in the catalog's enable order (`Level 1` reads no other tap's tables), or `needs: x, y` while the taps it reads from have not landed data. The **Disabled** badge menu names the sources to run first and offers **Run now** for a tap whose last test passed: one real run that lands its data without enabling it.
* **Enable in order** — on a catalog with at least one disabled tap, the play-list icon on the card shows the catalog's enable order level by level, and **Start** enables the whole catalog in that order in one job. For each tap the job runs a test if its last test did not pass, enables it, does one real run and waits until that data has landed before moving to the taps that read it. A tap whose source failed or landed nothing is skipped with the reason, so you see which source to fix. A run that returns no records fails the tap only when another tap in the enable order reads from it; a tap nothing depends on (for example one that is fed through run parameters) stays enabled and is marked landed with "no records returned; nothing depends on this tap". Progress shows on the card, one row per tap, and **Cancel** stops the job after the tap in progress (its run is not interrupted; the remaining taps are marked cancelled). While the job runs, the catalog cannot be renamed, deleted or restored over. After a restore, the result banner offers the same **Enable in order** link when restored taps depend on each other.
* **Uncataloged is not a catalog** — its card has no rename, delete or download control. Delete uncataloged items one at a time (below).
* **Delete individual tap or pipeline** — expand any catalog (including Uncataloged) and use the trash icon on a specific row to delete just that item. Clicking the trash icon swaps the row's action icons for an inline confirm: **Delete** / **×** for a tap; **Config & Data** / **Data Only** / **×** for a pipeline (Data Only wipes the rows but keeps the pipeline config so the next ingest fills it again).
* **Row actions menu (Uncataloged)** — each item in the Uncataloged group has a ⋮ kebab menu after the trash icon with three actions:
* **Edit** — opens the tap or pipeline editor.
* **Delete** — same as the trash icon (inline confirm).
* **Move to catalog** — submenu listing every other catalog. Picking one sets the item's `catalog` field and reloads the view. Before persisting, the UI checks the target catalog for a tap or pipeline of the same name; on a clash the move is blocked and a red banner explains the collision ("A tap named 'foo' already exists in catalog 'reporting'. Rename one of them first.").
## Assigning a catalog
Four ways to put a tap or pipeline into a catalog:
1. **Tap editor** — when creating or editing a tap manually, set the **Catalog** field.
2. **Pipeline editor** — same field on the pipeline form.
3. **CLI** — pass `--catalog ` to `datris ingest` when creating a new pipeline. Re-ingesting an existing pipeline preserves its current catalog (and the rest of the config) — use the `set_catalog` MCP tool to change a catalog after the fact.
4. **MCP agent** — call `set_catalog(pipeline="", catalog="