Skip to main content
Beta. Taps are a new feature and the API and UI may evolve based on user feedback. Core functionality is stable and production-ready, but we may add fields, rename properties, or adjust workflow steps in upcoming releases.
A Tap is an AI-generated Python script that fetches data from external sources — APIs, web scraping, databases, document repositories — and pushes it into a Datris pipeline. Describe what data you want in plain English, and Datris generates the script for you. Taps can run on a schedule using CRON expressions, or be triggered manually. They support credentials management via Vault secrets and include AI-powered diagnosis when scripts fail, return zero records, or emit deprecation/warning output. Tap scripts live in Datris’s built-in object storage by default, or in a GitHub repository you connect — every save becomes a commit, runs pin to the exact commit they were saved at, and your team can review and edit tap code with normal git tooling. See GitHub Script Storage.

Tap Types

The wizard offers two ways to implement a tap:
  • I Have My Own Code — paste your own Python fetch() script directly. The script runs through the same test / auto-fix / post-run review flow and can use any of the pre-installed packages or bring its own. (AI-generated scripts are still available through the Assistant and MCP create_tap with an instruction. Generation reports the CodeGen provider and model that wrote the script (the primary model when no CodeGen model is set): POST /api/v1/tap/generate and its /status poll return provider and model, and create_tap returns them as generatedWith, so the Assistant can tell you which model generated the tap.)
  • HTTP Endpoint — you host the tap as an HTTP service, in any language; no code runs on the platform. See HTTP taps below.
Whichever you pick, the tap’s output shape — structured records vs. documents — is determined by what it returns, not by a setting. A tap that returns {uri, filename, content} dicts is a document tap (files staged for a vector-store pipeline, deduplicated via a ledger); anything else is structured. The wizard detects the shape from your test run and applies the matching pipeline rules.

HTTP taps: bring your own service, in any language

Besides Python scripts, a tap can be an HTTP endpoint you host — written in Rust, Go, TypeScript, a serverless function, or an existing internal service. Datris POSTs the run context (params, sync state, test limit) to your URL on each run, and your endpoint responds with the same envelope a script produces. Scheduling, per-run params, incremental state, run history, and retries all work identically; no code runs on the platform at all, and your service keeps its own upstream credentials. Choose HTTP Endpoint in the wizard, or --kind http --endpoint-url ... in the CLI. AI generation, fix/review/optimize, pip packages, and the platform-data callback don’t apply to HTTP taps — a tap that reads data already in Datris must stay a Python tap. Full wire spec and a worked Rust example: HTTP Tap Contract.

Creating a Tap

The Tap creation wizard walks you through 4 steps — or 5, if you link the tap to a pipeline (the optional 5th step lets you run the tap immediately and push data through the pipeline before leaving the wizard).

Step 1: Describe

Step 1 is where everything except testing happens — you name the tap, describe what you want, attach credentials, and generate or paste the script. Tap Name — A short identifier for the tap (e.g., weather-data or contracts-sharepoint). If the name matches an existing tap (and you’re not in edit mode), an amber warning appears under the field letting you know that continuing will overwrite the existing tap’s configuration and script. The warning is non-blocking — overwriting is allowed if that’s what you intend. Tap Type — Three radio options:
  • Structured/Semi-Structured (default) — AI generates a script that returns records flowing into a structured pipeline destination.
  • Document Ingestion — AI generates a script that returns {uri, filename, content} dicts for each file; the pipeline handles text extraction, chunking, and embedding.
  • I Have My Own Code — skip the AI and paste your own Python fetch() script. The AI brainstorm, Instruction textarea, and Generate Script button are all hidden; you see a full-width code textarea and a Use My Code button that uploads the pasted script to MinIO. Once uploaded, Next is enabled and the rest of the wizard (Edit & Test, Schedule, Review & Save) behaves identically to an AI-authored tap. Editing the textarea after upload invalidates the upload — you’ll see the button flip to Re-upload My Code and Next will be disabled until you re-click it.
Pick this first — the brainstorm prompts, the suggested env vars, and the Step 4 pipeline wizard all branch on this choice. The selector is locked in edit mode (change the type by creating a new tap). Brainstorm with AI (Optional) — Not sure how to describe what you want? Use the chat panel above the instruction box. Type a rough idea and the AI will:
  1. Ask one focused clarifying question at a time
  2. Suggest specific data sources (e.g., yfinance for stocks, Open-Meteo for weather, Alpha Vantage for fundamentals)
  3. Recognize when you reference a Datris table (it knows the platform’s metadata and query endpoints)
  4. Auto-update the Instruction box below on every turn as the conversation progresses
  5. Suggest the environment variable names you’ll need (e.g., ALPHA_VANTAGE_API_KEY) when an external API requires authentication
When the AI suggests environment variables, a panel appears below the chat with the variable names as chips and a + Create tap secret with these keys button that pre-fills the credentials section with those keys — you just paste in the values. You can stop chatting at any time and edit the Instruction box directly, or skip the brainstorm entirely and write the instruction yourself. Instruction — The technical directive used to generate the Python script. Filled in by the brainstorm chat or written directly. Credentials (Optional) — Select an existing tap secret, edit one, or create a new one. Secret keys are injected as environment variables at runtime — the AI generates code that reads them via os.environ.get('KEY_NAME'). Never hardcode credentials in scripts. Generate Script — Click to have the AI produce a Python script with a fetch() function. The result appears below in a scrollable preview, along with any extra pip packages it needs. The generated script can use these pre-installed packages:
  • requests, beautifulsoup4, pandas, lxml, feedparser
  • boto3 (AWS S3), google-cloud-storage (GCS), azure-storage-blob (Azure Blob)
  • openpyxl (Excel), pyyaml (YAML), python-dateutil, pytz
  • Additional packages can be specified and are installed at runtime via pip into a throwaway per-run environment. In in-process mode the pip download cache lives in a Docker volume so reinstalls skip the download; in the isolated runner (compose default) packages are fetched fresh on every run.
Example instructions:
  • “Fetch current weather data for New York from the Open-Meteo API”
  • “Retrieve daily stock prices for all S&P 500 companies from yfinance”
  • “Get the latest news headlines from the BBC RSS feed”
  • “Query the consumer_discretionary_earnings table on Datris to get tickers, then fetch historical earnings from Alpha Vantage”
Two optional fields on the tap describe it for discovery and lineage: Tags (free-form labels that find_data and the catalog search rank over) and Source — where the data really comes from, such as SEC EDGAR or financialmodelingprep.com. Leave Source blank and the platform derives it from the endpoint host (HTTP taps) or the host the script calls most; see Provenance.

Step 2: Edit & Test

Review and edit the generated script. You can:
  • Modify the Python code directly
  • Add or remove pip packages
  • Edit the instruction and click Regenerate to produce a new script
  • Copy the script to your clipboard with the Copy button next to the editor
  • Click Test Script to execute and preview results. Preview renders as JSON (up to 100 records) in a scrollable pane
  • If the script fails, an AI Diagnosis explains what went wrong and is auto-applied — the platform fixes the script and retests, up to 3 attempts. If all retries fail, the diagnosis stays visible for you to review and apply manually
  • Click Stop Test to cancel an in-flight test (also halts the auto-fix chain)
Limit test sample (checkbox, default on) with an inline record-count input (default 20, minimum 1): when enabled, the runner injects DATRIS_TAP_TEST_LIMIT=<n> into the script process. Well-written tap scripts honor this env var to cap both their /api/v1/query/* reads and per-item iteration loops, making tests fast and low-cost. When fetch() is a generator (it yields records), the platform also stops pulling after n records and closes the generator; a returned list is never truncated, so a script that returns a list must cap its own reads. Test runs started from the MCP test_tap tool, the CLI (datris tap test) and POST /api/v1/tap/run with mode: "test" default to 20 records as well — pass limit (MCP/CLI) or testLimit (REST) to raise the cap, or 0 to disable it and stream the whole source. Because of that, the recordCount a test reports is a preview size, not what a real run will produce. Cron runs and mode=run runs (the Run Tap button, run_tap) never set this env var, so production runs always read every row from every source.

Post-run script review

When a test passes, the platform first sends the working script and its captured stderr/stdout to the LLM for a functional review — not a performance pass. The reviewer looks for signals in the script’s own output that the script should behave differently:
  • Rate-limit / throttle / burst warnings — add time.sleep, lower max_workers, add adaptive backoff on HTTP 429
  • Deprecation / migration hints — switch to the recommended replacement endpoint, method, or field
  • Pagination / partial-response hints (“truncated”, “page 1 of 5”, “next_cursor=…”) — add pagination so the tap keeps fetching until the source says no more
  • Schema-drift / auth warnings — update parsing for renamed fields; for auth errors, add a clear stderr note naming the suspect env var
If the reviewer finds a signal, it regenerates the script with the fix applied, the UI swaps it in, and the test auto-reruns. An amber banner then lists what the reviewer changed (e.g. Added 0.25s sleep between API calls — source reported burst pattern). A passing script is otherwise kept exactly as tested — there is no automatic performance rewrite. This review step runs once per successful test, so the reviewer won’t loop on its own output. If you edit the script yourself and click Test again, the reviewer gets a fresh look at the new output. The test must return records successfully before you can proceed to step 3.

Step 3: Schedule (Optional)

Set up automatic runs with a CRON expression. Datris uses Quartz CRON: 6 fields — seconds, minutes, hours, day-of-month, month, day-of-week (plus an optional year), e.g. 0 0 * * * ? for hourly. A malformed expression — including the familiar 5-field Unix form such as */1 * * * * — is refused with HTTP 400 and the error quotes the 6-field equivalent to resubmit.
  • Presets: Every Hour, Daily (Midnight), Weekdays (Midnight), Weekly (Monday)
  • Custom: Describe your schedule in plain English (e.g., “Every weekday at 4pm ET”) and click Generate to create the CRON expression
The Schedule is active checkbox controls whether the cron actually fires. Uncheck it to pause a tap without losing its schedule definition — useful when you want to temporarily stop a tap but keep its CRON expression intact for later.
Test before you schedule. A tap can only be saved with a schedule once the exact script (or HTTP endpoint) that will run on it has passed a test run. Saving a schedule on a never-tested or just-edited script is refused with HTTP 409. The schedule is applied only after a server-side test run of the saved tap: the wizard does this for you automatically on save (save unscheduled, test, then schedule — if the test fails the tap is saved without its schedule and the error says so); via MCP save without the cron, call test_tap, then update_tap with the cron; via REST save without cronExpression, call POST /api/v1/tap/run with mode: "test", then save again with the cron. The wizard’s own Test Script button previews an unsaved script and does not satisfy the gate by itself. Cadence changes on an already-tested tap need no re-test.

Step 4: Review & Save

Review all settings (including a human-readable description of the CRON schedule, e.g. 0 0 16 ? * MON-FRI — at 4:00 PM, on MON-FRI). From step 4 you can also link the tap to a pipeline so its output flows through validation, transformation, and into your destination automatically. Two options — both branches change shape based on the tap’s type: Structured taps
  • Attach to Pipeline — pick an existing pipeline whose source columns match the tap’s test output exactly. If columns don’t match, you’ll see exactly which columns are missing or extra so you can fix the mismatch before attaching.
  • Generate Pipeline — create a new pipeline pre-wired to the tap’s output. The new pipeline’s destination is Postgres (public.<tap_name>) for tabular data, or MongoDB for nested/document data. All columns are created as string for safe ingestion — tap output shape can vary across runs, and string columns ingest reliably regardless of what the script returns. You can promote individual columns to richer types in the pipeline editor once you’re confident about the data shape.
Document taps
  • Attach to Pipeline — the picker is filtered to only compatible pipelines: those whose source is unstructuredAttributes and whose destination is a vector store (Qdrant, pgvector, Weaviate, Milvus, Chroma). Column matching doesn’t apply — every document has the same {uri, filename, content} shape. The selection card shows the destination (e.g. pgvector → public.contracts).
  • Generate Pipeline — creates an unstructured → vector-store pipeline. You pick:
    • Vector store — only stores whose secret is configured and the service is reachable are listed (server probes each on modal open via /api/v1/vector-stores/available). If exactly one is available it’s preselected; if none are, the modal blocks with a link to Configuration.
    • Destination name — collection name (Qdrant / Milvus / Chroma), class name (Weaviate), or table + schema (pgvector).
    • Chunking strategy — recursive (default), fixed, sentence, paragraph, or none. Plus chunk size (default 500 chars) and overlap (default 50). These live on the pipeline, not the tap.
    • Embedding is inherited from the environment’s default embedding secret — no picker this pass.
The server rejects any save attempt that points a document tap at an incompatible pipeline (HTTP 400), regardless of where the save came from (UI, CLI, MCP, or raw API). If a pipeline is linked when you click save, the wizard advances to step 5; otherwise it returns to the Taps list.

Step 5: Run the Tap (Optional)

Step 5 only appears when the tap is linked to a pipeline. It’s a one-time launch screen with two buttons:
  • Run the Tap Now — runs the tap script immediately and pushes the records it returns into the linked pipeline. The pipeline applies any data quality and transformation rules and writes to its destination. After the run completes you’re returned to the Taps list.
  • Done — skip the run and return to the Taps list. You can run the tap later from the Taps page or via the MCP run_tap tool.

Querying Datris Data

Tap scripts can query data already stored in the Datris platform. The following environment variables are always automatically injected at runtime — your script does not need to fall back to defaults: Calls to the platform are authenticated for you. When API keys are required, the platform attaches DATRIS_PLATFORM_TOKEN as x-api-key to any requests or urllib call whose host is DATRIS_PLATFORM_HOST — and only that host, so it never reaches a third-party API. The token is read-only (queries, search, metadata), scoped to the one run, and shows up in the audit log as the tap itself. If your script uses a different HTTP client, send the header yourself: {"x-api-key": os.environ["DATRIS_PLATFORM_TOKEN"]}. A tap never needs a platform credential in its secret. In single-tenant deployments the postgres and mongo database names may differ (postgres defaults to datris, mongo defaults to oss); in multi-tenant mode both resolve to the same tenant name. Always use the variable that matches the backend you are querying. Scripts can call the Datris API directly using these: Discover tables (PostgreSQL):
Column names returned by /api/v1/metadata/postgres/columns reflect the live table schema, which always satisfies the platform’s [A-Za-z0-9_]+ rule (see Column Naming Rules). You can use them directly in SELECT clauses without quoting. Query PostgreSQL data:
Query MongoDB data:
The brainstorm AI knows about all of these endpoints, so if you mention “the X table on Datris” or “the Y collection on Datris” it will write the instruction to use the appropriate backend — no need to describe schema or column names; the script can introspect those at runtime via /api/v1/metadata/postgres/columns or /api/v1/metadata/mongodb/collections.

Dependencies between taps

A tap that reads another tap’s destination table (through /api/v1/query/postgres, /api/v1/query/mongodb or the metadata endpoints) can only test successfully once that other tap has landed data: until then the table does not exist. Datris records these dependencies so it can tell you the order to enable a catalog’s taps in.
  • Inferred. When a tap’s script is saved, the platform reads the table names its queries use (FROM / JOIN in SQL, collection in a Mongo query) and matches them against the destination tables of the pipelines in the same catalog. The taps that feed those pipelines are stored on the tap as inferredDependsOn. Common table expressions, tables of other catalogs and the tap itself are ignored. A tap saved before dependency tracking existed is analysed the first time its catalog’s enable order (or the tap itself) is read.
  • Declared. Set dependsOn to a list of tap names in the same catalog to state the dependencies yourself (for example for a read inference cannot see, such as a warehouse or object-store read). A declared list replaces the inferred one; an empty list [] declares that the tap has none. Blank names are refused and repeated names are stored once. Every name must be a tap of the same catalog, not the tap itself, and the list may not close a cycle: a save that breaks either rule is refused (400 naming the tap, or 409 with the cycle). Leaving dependsOn out of an update keeps the stored list.
  • Order only. Dependencies are never enforced: saving, testing, running and scheduling a tap behave exactly as without them.
Enable order. GET /api/v1/catalog/{name}/enable-order (MCP: catalog_enable_order) groups the catalog’s taps into levels: level 1 holds taps that read no other tap’s tables, level 2 the taps that read level 1, and so on. For each tap it reports hasPassingTest (the last test run succeeded on the tap’s current script; a test stamped for an older script, for example after an overwrite restore, does not count), lastRealRunLanded (the last real run succeeded with at least one record), blockedBy (dependencies that have not landed data yet) and ready (tested and not blocked). Taps on a dependency cycle are listed under cycles instead of a level. It needs tap:read; taps the key may not read are left out and named in warnings. On the Catalog tab, each disabled tap shows its level, or needs: x, y while its sources have not landed data. The Disabled badge menu says which sources to run first, and offers Run now once the tap’s current script has a passing test (hasPassingTest; a green test from before a script edit or an overwrite restore does not count, and the menu offers Test, then enable instead for a scheduled tap): one real run that lands its data without enabling the tap or changing its schedule. Enabled taps with a passing test have the same Run now action next to Test run.

Credentials / Secrets

Tap secrets are stored in HashiCorp Vault and injected as environment variables when the script runs. They are managed directly in the tap wizard (Step 1).
  • Select existing: Choose from previously created tap secrets
  • Create new: Define key-value pairs inline (e.g., API_KEY=your-key)
  • Edit existing: Modify an existing secret’s fields
  • Use suggested keys: When the brainstorm AI suggests environment variables, click + Create tap secret with these keys to jump straight into the create form with the keys pre-filled
Tap secrets are tagged with _type=tap and only appear in the tap dropdown — not mixed with system secrets like database credentials.

Taps can only use tap secrets

A tap’s secret must be a tap secret. Platform secrets (AI provider keys, warehouse and database credentials, the GitHub token, anything without _type=tap) hold values the Secrets API never shows in clear text, and every field of a tap’s secret is handed to the script, so a tap on a platform secret would expose them. Saving a tap that names one returns HTTP 400, and a tap saved that way before this rule fails its next run (scheduled runs included) with the same message, before any value is injected:
To fix an affected tap:
  • Create a tap secret under Configuration → Secrets → Tap with the fields the script reads, and select it on the tap.
  • Tag a hand-made tap secret. A secret you created for a tap outside the wizard, without _type=tap, counts as a platform secret. Add the field _type with the value tap to it in the Secrets tab. Tagging a secret as a tap secret makes its values readable by any tap, so only an admin should do it, and only for credentials a tap is meant to use.
The server names every affected tap in one warning line at startup, and the tap.secret_scope doctor check lists them. Opt-out. DATRIS_TAP_SECRET_SCOPE=any lets taps use platform secrets again. It re-opens the exposure above: anyone who can create and run a tap (an editor, or an API key that can create taps) can read any platform secret’s values. Use it only as a bridge while you move affected taps to tap secrets. The server-managed secrets (field protection keys, API-key stores) stay refused whatever the setting. See the Configuration Reference. Never hardcode credentials in scripts. Always use os.environ.get('KEY_NAME').

Running Taps

On docker compose, tap code runs in an isolated runner container by default (USE_TAP_RUNNER=true), separated from the platform’s credentials and internal services; sbt / IDE runs stay in-process. See Tap Execution & Isolation.

Test Run (from UI)

Click the play button on the taps list to open the Run page:
  • Shows the tap instruction
  • Send to pipeline checkbox (only if a target pipeline is configured)
  • Displays script output, results table, and errors
  • Run Again button for re-execution

CRON Schedule

Datris schedules are Quartz CRON expressions: 6 fields — seconds, minutes, hours, day-of-month, month, day-of-week — plus an optional year. Note the leading seconds field: the 5-field Unix form most cron references show (*/1 * * * *) is not a Datris schedule. POST /api/v1/tap refuses a malformed expression with HTTP 400; for a 5-field input the error quotes the 6-field equivalent (here 0 */1 * * * ?) to resend. Use ? in whichever of day-of-month / day-of-week you leave unspecified — Quartz requires exactly one of the two to be ?. A schedule can only be saved once the tap’s current script (or HTTP endpoint) has passed a test run — POST /api/v1/tap returns HTTP 409 otherwise, and the error names the tap and the remedy: save the tap without cronExpression, call test_tap, then update_tap with the cron. Editing the script of a scheduled tap is refused the same way until the new script is re-tested. Cadence-only changes, clearing the cron, and metadata edits (description, tags, target pipeline, enabled) on a tested tap are always accepted. Taps scheduled before this rule existed keep running untouched; their first cron or script change is accepted if the tap has ever tested or run successfully. Taps with a cronExpression run automatically. The scheduler checks every 30 seconds for taps that are due. A tap won’t fire if it’s already running. For a tap that has never run before, the scheduler anchors the “next valid cron time” to updatedAt or createdAt, so the first scheduled slot after the tap was saved fires on its own — no manual bootstrap run needed.

Sending Data to Pipelines

When “Send to pipeline” is checked:
  1. The tap executes the script
  2. Records are converted to the pipeline’s expected format (CSV or JSON)
  3. Data is fed through the pipeline’s processing chain (data quality, transformation, destinations)

Watching a Run

run_tap returns as soon as records are handed off — the pipeline load runs async after that. The response carries:
  • persisted: true/false with persistedReason when false (no_target_pipeline, test_mode, no_records, run_error, debounced) so callers know whether the data actually landed. debounced means another run for this same tap was triggered within the last 5 seconds — that earlier run is still executing; do not retry, just track it via get_tap_logs / get_pipeline_status.
  • publisherToken — a single ID covering every ingestion job this run submitted. Document taps fan out to many jobs per run; one publisherToken covers them all.
  • pipelineTokens — the per-job IDs (length 1 for structured, N for document).
  • recordCount — how many records the script produced.
run_tap does not return the records themselves. Use test_tap (or the Test Script button in the UI) when you need to preview what a script produces before pushing it — test_tap previews the first 20 records by default (limit changes the cap, limit: 0 removes it) and returns a sample of up to 20 rows with a recordsTruncated flag when trimmed — its recordCount is the preview size, not the size of the source. Poll GET /api/v1/pipeline/status?publishertoken=...&withrollup=true (or the MCP get_pipeline_status tool, which sets withrollup=true automatically) until rollup.allDone is true, then read rollup.status for the outcome and rollup.jobs[].lastError for any failure detail.

Per-run Parameters

run_tap accepts an optional params object — a map of caller-supplied values that get injected into the script as environment variables for that one run only. Use this for values that vary per call (date windows, ticker lists, page cursors, batch sizes, geographic regions). Each key/value becomes an env var the script reads:
  • Key constraints: must match [A-Za-z_][A-Za-z0-9_]* so the keys map cleanly onto env var names. Anything else is rejected with an actionable error.
  • Value handling: strings pass through; numbers/booleans are stringified; nested objects/arrays are JSON-encoded so the script can json.loads() them back.
  • Scheduled runs supply no params — cron-triggered runs have an empty params bag. Scripts MUST apply sensible defaults when the env var is absent.
Use params for things that change between runs; use secret_name for credentials that don’t. Rewriting a tap secret on every run to smuggle per-call values through is an anti-pattern — it clobbers concurrent runs, pollutes audit history, and wastes Vault writes.

Incremental Sync (State)

For taps that run on a schedule, the platform persists a small JSON state object (a bookmark) between runs, so each run fetches only what’s new instead of re-fetching everything. A well-written incremental tap runs at constant size forever, no matter how large the source grows. The contract:
  1. Before each run, the last committed state is injected into the script as the DATRIS_TAP_STATE environment variable (absent on the very first run).
  2. The script saves new state by assigning a dict to the module-global DATRIS_STATE inside fetch().
  3. The platform commits the new state only after a successful run. A failed run leaves the old bookmark in place, so the automatic retry re-fetches the same window — no data holes. With keyFields configured on the destination, overlapping re-fetches upsert safely.
What to track depends on what the source offers, in order of preference: a modified-since filter (state = newest timestamp seen), append-only ordering (state = highest ID seen), an opaque continuation token (store it verbatim), or — for full-fetch-only sources — a hash of the payload so unchanged runs return [] and record a clean no_records. When you create a tap from a plain-English instruction, the script generator applies this ladder automatically for scheduled taps whenever the source supports it. Rules:
  • State is a cursor, not a data store — a timestamp, an ID, a token, or a hash. The platform rejects state over 64 KB.
  • Cursors must be monotonic: advance with max(previous, newest_seen) so re-processing old data never moves the bookmark backwards.
  • Backfills don’t move the bookmark: when an explicit window is passed via run_tap params, the script should leave DATRIS_STATE unset for that run.
  • Never put credentials in state — it is stored and displayed unmasked.
  • Test runs read state but never commit it.
Inspecting and resetting: the bookmark icon on the taps list shows a tap’s current state, when it was committed, and a Reset button (the next run then does a full first-run fetch). Agents use get_tap_state / set_tap_state; the REST surface is GET/POST/DELETE /api/v1/tap/state. Deleting a tap deletes its state, so a recreated tap starts fresh.

Run Status Outcomes

Each tap run produces one of three states on its lastRunStatus field and matching run log entry: Cron-scheduled runs that legitimately produce zero records (incremental tap that’s caught up, polling tap with no new entries) record no_records rather than failure and the Ops Activity dashboard surfaces them as healthy.

Output Size Guard

A tap run’s records never sit in the server’s heap: the wrapper writes them to a staged file on the datris-staging volume as the script returns them, and the pipeline reads that file. What is capped is disk — PIPELINE_MAX_PAYLOAD_MB (property pipelineMaxPayloadMB) is the per-run payload disk budget in megabytes, default 4096, 0 = unlimited. A run that stages more than the budget is stopped with an actionable error naming the variable (The payload exceeded the configured disk budget for one run (PIPELINE_MAX_PAYLOAD_MB = 4096 MB, got ~4097 MB). Raise it, or split the source (for a tap, chunk the source range via run_tap params).). A multi-GB fetch runs on the default 2 GB heap. To size a run against the budget: records are staged as JSON with their keys repeated on every row, so a staged record costs roughly 26 bytes × column count for numeric, timestamp and short-string data (long text or nested fields add their own length) — a 25-column record is about 650 bytes, so 20,000,000 rows stage about 13 GB. Reading only the columns the destination needs cuts that linearly. TAP_MAX_OUTPUT_MB is the deprecated alias of PIPELINE_MAX_PAYLOAD_MB, honoured for one release when the new variable is unset; the server logs a deprecation warning at boot when the alias is set. If a backfill blows the budget, raise it, or reduce the source range using run_tap params (shorter date window, smaller page, per-symbol chunks) and call again. Multiple smaller runs all land in the same destination pipeline; with keyFields configured on a Postgres or MongoDB pipeline, overlapping ranges upsert safely. For a scheduled tap that keeps growing toward the cap, the durable fix is making it incremental — see Incremental Sync (State) — so every run carries only the delta.

Data Types

The system automatically detects the data type from what fetch() returns: The uri + content detection takes priority — if your script returns records with those keys, the runner routes every element through the document pipeline path even if the tap’s type is structured. In practice the type radio and the return shape should always agree; this rule just makes misconfigurations fail loudly rather than silently corrupt a structured destination.

Pipelines and Taps

The Pipelines page shows a Tap column for each pipeline, indicating which tap (if any) feeds it. Click the tap name to jump straight to that tap’s edit page. On the Pipeline creation wizard, select From Tap to auto-populate the pipeline’s source type and schema from a tap’s test results. The tap’s data type and columns are used to configure the pipeline automatically.

Fetch-only taps

Every tap has a pipeline. When you want a tap to fetch and hand the rows back — an answer now, a validation result, a transformed view — rather than land them, wire it to a Live Read pipeline (the scratch destination). The run goes through data quality and transformation as usual; the rows ride on the run’s status rollup (resultPreview) and can be paged with get_pipeline_result, datris pipeline result, or the View result panel on the run detail page. Live Read results are not catalogued or queryable later and expire, so read them promptly. If the rows turn out to be worth keeping, change the pipeline’s destination — the tap, its schedule and its incremental state do not move.

Run History

Click the history icon on the taps list to view a tap’s run history. Each entry shows:
  • Status (success/failure)
  • Timestamp and duration (formatted using the configured dateFormat and dateTimezone — see Configuration Reference)
  • Record count
  • Whether data was sent to a pipeline
  • Expandable script output logs and error messages

MCP Tools

AI agents can manage taps via MCP: End-to-end agent flow when a tap needs credentials:

CLI Commands

Configuration

Document Taps

Document taps feed files (PDFs, DOCX, HTML, Markdown, etc.) into a vector-store pipeline. The pipeline owns text extraction, chunking, and embedding — the tap’s only job is discovery and retrieval.

Return shape

Each element of the list fetch() returns is a dict describing one document: Rules the generator and users must follow:
  • Return one entry per source file. Never chunk, split, or segment. Never decode the bytes.
  • Don’t generate embeddings, pick embedding models, choose a vector store, or create tables — those are pipeline concerns.
  • Don’t invent metadata fields that describe pipeline behavior (chunk_size, embedding_model, target_table); those belong on the pipeline.
The AI brainstorm chat and script generator both enforce these rules: if you ask for chunking or embedding choices, the assistant redirects you to pipeline configuration.

The Document Ledger

Every document the tap successfully stages is recorded in a ledger — a MongoDB collection keyed by {tapName}|{uri}. On each run, the tap:
  1. Loads uri → contentHash for every existing ledger entry owned by this tap.
  2. For each document returned by fetch(), computes (or reads) its content hash.
  3. Skips the document if the ledger already has an entry with the same hash (just refreshes lastSeenAt for operator visibility).
  4. Otherwise, stages the bytes to MinIO, writes a ledger entry (status: staged), submits to the pipeline, and marks the entry processed (or failed on error).
This is what makes document taps safe to run on a schedule: re-runs only process new or changed files. Ledger fields: uri, tapName, stagedPath (MinIO key), filename, contentHash, firstSeenAt, lastSeenAt, status (staged/processed/failed), metadata. Managing the ledger:
  • UI — the document-ledger button (page icon) next to a document tap in the Taps list opens a modal listing every entry. Actions: Clear All (force full re-scan on next run), or delete a single entry (force that one file to re-process).
  • REST — GET /api/v1/tap/ledger?name={tap}, DELETE /api/v1/tap/ledger?name={tap}&uri={uri} for a single entry, DELETE /api/v1/tap/ledger?name={tap} to clear all.
  • MCP — get_tap_ledger with optional clear_uri / clear_all.
Automatic cleanup:
  • Deleting a tap clears its ledger entries and deletes all staged MinIO objects.
  • Deleting a pipeline’s data (from the Pipelines page → Delete Data Only, or DELETE /api/v1/pipeline?pipeline=X&deleteData=true) clears the ledger for every document tap pointed at that pipeline — so the next run re-ingests every file into the now-empty destination. Without this, taps would skip docs they already “processed” and the pipeline would stay empty.

Pipeline compatibility

The server validates at two points:
  1. On save (POST /tap) — if tapType == "document" and targetPipeline is set, it checks the pipeline’s shape. Rejects with HTTP 400 if the source isn’t unstructuredAttributes or the destination isn’t a vector store.
  2. On feed (TapRunner.feedDocumentPipeline) — re-checks before submitting bytes, so a pipeline that was reshaped after the tap was saved fails the run with a clear error instead of silently corrupting a destination.

Concurrency

Document taps feed many documents through StreamNotifier.process in rapid succession. Each becomes its own JobRunner, and all five vector-store loaders are now race-safe when multiple runners hit ensureCollection / ensureTable at the same time on a fresh pipeline:
  • pgvector — transactional advisory lock plus CREATE TABLE IF NOT EXISTS; serializes concurrent sessions at the schema-qualified table name.
  • Qdrant / Milvus / Weaviate — try-create; on error, re-check existence and swallow if a concurrent runner won the race.
  • Chroma — sends get_or_create: true plus a re-GET fallback for older servers.

Script Requirements

The fetch() function must:
  • Take no arguments
  • Return a list of dictionaries (records), a list of lists (rows, header first), or a string (JSON/XML/text) — or yield the records / rows one at a time (any generator or iterator works)
  • For large sources, yield. The platform streams whatever fetch() produces to disk one record at a time, so a run is never limited by memory — but only if the script does not build the whole result first. A fetch() that appends millions of rows to a list (or loads a whole file into pandas and calls to_dict("records")) is killed for memory, and the run fails with an error naming memory and yield. Read the source in chunks / pages / batches (pd.read_csv(..., chunksize=50000), pyarrow.parquet.ParquetFile(...).iter_batches(), one API page at a time) and yield each row before fetching the next chunk. Never cap the rows or split the source into several runs for size — the one exception is when the estimate below exceeds a budget and the user has chosen to chunk the range.
  • Yield batches for a large columnar or tabular source. For a large columnar or tabular source, yield each batch (a pyarrow RecordBatch, a pandas DataFrame, or a list of dicts) instead of each row — the platform serialises a batch natively, several times faster — and read only the columns the pipeline needs (columns=[...]). Yielding one row at a time remains correct for small or API-paged sources. Each batch is written as its rows, so nothing downstream changes: the record count is rows, the columns are the union of the batches’ column names, and the staged records are identical to yielding the batch’s own rows (df.to_dict("records") / batch.to_pylist()) one at a time. Floats inside a batch are written with up to 15 significant digits rather than Python’s shortest round-trip form — the type and the destination schema are unchanged. Two other cosmetic differences in a batch: -0.0 lands as 0.0, and a / inside a string is escaped as \/; both parse to the same value.
  • Progress is reported for you. A fetch() that yields records makes the platform print [wrapper] streamed N records (M s) to the run log every 100,000 records or every 30 seconds, so a long or timed-out run shows how far it got. No script change is needed, and a fetch() that returns a list prints none.
  • Stream the source, never download it. The isolated tap runner has no disk of its own: its only writable area is a 512 MB in-memory scratch space shared with the per-run venv and any pip-installed packages (see Tap execution isolation). A fetch() that saves a large file locally before reading it — urlretrieve, open(path, "wb"), requests with stream=True written to a file, a library that spools under TMPDIR — fails with No space left on device. Read remote files directly: pyarrow.parquet.ParquetFile(fsspec.open(url).open()) (or an io.BytesIO of a single small file), pd.read_csv(url, chunksize=...), requests.get(url, stream=True).iter_lines(), and yield records as they arrive.
  • Check the time budget too. A test is killed after TAP_SCRIPT_TIMEOUT_SECONDS (default 300 s) and a real or scheduled run after TAP_RUN_TIMEOUT_SECONDS (default 3600 s); both ceilings in force are reported by get_version / GET /api/v1/version. A test is also capped at 20 records, so a passing test says nothing about how long a real run takes — only an estimate does. A timed-out run says which mode it ran in and which variable to raise.
  • Check both budgets before you build. Estimate the run from two measured numbers, and show them before creating anything:
    • Disk — records are staged as JSON with their keys repeated on every row, so a staged record costs roughly 26 bytes × column count for numeric / timestamp / short-string data (long text or nested fields add their own length). rows × that is what the run stages, capped by PIPELINE_MAX_PAYLOAD_MB (default 4096 MB, 0 = unlimited). Read the value in force from get_version / GET /api/v1/version (pipelineMaxPayloadMB) — an install that raised the variable runs with a different ceiling, and the estimate is compared with that, not with the default.
    • Time — a fetch() that yields one record at a time moves about 15,000 rows per second; one that yields batches moves about 90,000 rows per second. Say which rate the estimate used, and prefer batches for any source over about a million rows. Compare the minutes with TAP_RUN_TIMEOUT_SECONDS (default 3600 s) — again, the value in force is tapRunTimeoutSeconds from get_version / GET /api/v1/version.
    • Show the estimate as a four-line table — rows, bytes/record, GB staged, minutes — so the row-count assumption can be corrected. For example, 20,000,000 rows × 25 columns → ~650 bytes/record → ~13 GB staged → ~4 minutes at the batch rate.
    • If either estimate is over its budget, choose before building: raise the variable (the staging volume needs matching free space; a longer timeout holds a runner slot that long), narrow the window, or chunk the range via run_tap params — several runs land in the same pipeline. Reading only the columns the destination needs cuts both numbers linearly. Finding out after several minutes of streaming is the expensive way.
  • Let exceptions propagate (the wrapper captures the traceback for diagnosis); only catch what you can actually recover from
  • Include timeouts on network requests (30 seconds recommended)
  • Use os.environ.get() for any credentials
  • Read DATRIS_POSTGRES_DATABASE, DATRIS_MONGODB_DATABASE, DATRIS_PLATFORM_HOST, and DATRIS_PLATFORM_PORT directly without fallback defaults — they are always injected by the platform
  • Column names are auto-normalized for tabular results (list of dicts). The platform converts each key to lowercase snake_case using only [a-z0-9_] so that downstream pipeline registration succeeds and SQL queries don’t need quoting. Examples: EPS Estimate → eps_estimate, Surprise(%) → surprise_percent. You can return raw source column names — the platform will clean them — but for clarity in the test preview, prefer to emit clean keys directly. JSON/XML results destined for MongoDB are not normalized (they go through as raw blobs in the _json field). See Schema Definition → Column Naming Rules for the underlying validator rule.