From a question to a reusable answer

How it works: ask it once. Build an answer you can reuse.

A pipeline is the saved recipe for an answer: which sources to read, how to combine them, and which inputs can change. AI helps write it. The server executes it. Your team decides when it is ready to share.

Connect the data you already have.

An operator runs datapipelines on your infrastructure and registers the database connections. You choose which workspaces and people can use them.

The agent accesses connections by name. Database passwords stay with the server. Cross-database joins use temporary staging during the run; you do not need to build a warehouse first.

Example connections
SalesPostgres
OperationsMySQL
Reference dataSQLite

Illustrative names. Use the supported databases your organization already runs.

Give the question its business context.

Connect a compatible AI client through MCP, the protocol it uses to work with the server. The agent reads your schema, probes your data and checks recorded facts before building a pipeline.

Specify what “revenue” includes, which dates matter and what to exclude. The agent can save those definitions for later sessions and reuse SQL templates across pipelines.

Start with a business question

“Show revenue by region for the last quarter, excluding cancelled orders.”

Agree on the definition, check source coverage, then record the rule. The next question starts with that context.

Check the answer before you share it.

Review the SQL, source assumptions and output. Add release checks where they help: counts, expected totals, or other values your team can verify.

Configured checks run on the server before release. A person approves the version. The released logic is immutable; further changes go into a draft. Passing checks support review without proving every business interpretation.

A practical review
Are the dates and exclusions right?
Do totals reconcile with the source?
Is missing data handled consistently?
Has a person approved the version?

Put the dataset to work.

Publish a released, read-only pipeline as an authenticated API. Each request supplies declared inputs and executes the latest released version. Changing a draft does not change what the endpoint runs.

For Tableau, write results into a database it already connects to, or use a suitable web data connector for the API. Native dashboard creation is planned.

Example API path
GET /api/x/demo/top-company-by-borough?anchor_date=2025-01-01

The demo workspace's published endpoint. Your deployed host, released pipeline and scoped key determine access.

Built to run again

The same logic. New inputs. Fresh results.

Change the reporting date or region without asking AI to rewrite the query. The saved pipeline executes against your sources; a released version fixes the logic, not a snapshot of the data.

REUSE

Build on existing work

Share versioned SQL templates across pipelines, and compose a larger workflow from smaller pipelines.

INSPECT

See what happened

Follow execution progress and inspect results, node timings and errors when a run needs attention.

PROMOTE

Move reviewed work forward

Promote released pipelines and their dependencies between configured environments, with a person initiating the move.

Inside the application

See the work behind the result.

These spaces will show the real pipeline canvas, execution results and release workflow after testing is complete.

Try the sample data first

One small demo.
No business data required.

  1. Follow the deployment guide to prepare the host and obtain the repository.
  2. Run the demo command from the repository directory.
  3. Open the printed address and sign in with the generated login.
  4. Connect your AI client and explore a worked example.
Open the full deployment guide
./app.sh --start --demo nyc,trade,lake

Run this in your own deployment checkout. It starts the full sample stack.

Prefer help with the first connection?
Ask the founder.

Before you begin, and once you run it

A few practical questions

Do I need someone technical to set it up?

Yes. Someone deploys the server on your infrastructure, registers the database connections in the application (there is deliberately no agent tool that registers one — docs/mcp-server.md §6.2.22) and connects the AI client. After setup, business questions can be asked in plain language; a person who understands your data reviews the resulting SQL and numbers.

Does my data reach the AI provider?

The server holds your database credentials and the agent never sees them (docs/datasources.md §7), but the schema, statistics, preview rows and execution results the agent asks for are returned to it (docs/mcp-server.md §6.2.15) — and so may reach the model provider behind your client. Choose the client, the model and the datasources' access to match your requirements.

Can I build the pipeline entirely in the browser?

Not yet. Today authoring is through an AI agent over MCP or the REST API; the browser helps you inspect a pipeline, execute it, read results and errors, and manage draft and released versions (docs/pipeline-editor.md §11). Browser authoring is a roadmap item (docs/ROADMAP.md §2).

What does “published endpoint” mean?

A released pipeline gets a URL under /api/x/…; a GET runs it with the query string bound to its declared parameters and returns the rows as JSON. Keys are bound to a path prefix, so a partner can be given one endpoint and nothing else. The serving contract is docs/rest-api.md §19.

From data to an API in a day — is that real?

For the API half, today: an agent authors the pipeline against your databases, runs it, fixes what fails, you review and release, and the endpoint exists — no deployment, no service to write, no gateway to configure. Dashboards your agent creates and you embed in your own product are a planned roadmap item; the roadmap page says so. Publishing is docs/rest-api.md §19.

Where does it run?

One Docker image beside a PostgreSQL metadata database and Redis. ./app.sh --start --demo nyc builds the image, downloads the published sample data and brings the stack up; the reference compose file and the demo quickstart are docs/deployment.md Appendix A and Appendix B.

What happens when a query is too slow?

Every statement carries a query timeout and the run an overall deadline; a node that outlives its budget fails with pipeline.node.query_timeout and the elapsed time — and the SQL probe returns the plan — so the agent fixes the shape instead of guessing. The limits and cancellation are docs/dag-executor.md §5.

Is it a Tableau or dbt replacement?

Not today. It sits beside them: it can feed Tableau a governed dataset through an API or as Parquet on your own bucket, and it replaces the warehouse-plus-dbt step for teams that never wanted one. Dashboards, scheduling and alerts are on the roadmap page, undated. The comparison pages cite docs/versioning.md §3 for what is versioned.

For the engineer who has to run it

How it works, for the engineer who has to run it

The page above promised the outcome. From here down is the mechanism: what a dataset is here, where it runs, what the agent may and may not do, and what a release costs you. Every card below cites the spec section it comes from — view source and spot-check us.

The whole mechanism in one paragraph

A dataset here is a pipeline: a versioned JSON DAG of SQL nodes the agent authors over MCP against your real schemas. Each node runs on its own engine — Postgres, MySQL, SQL Server, Oracle, SQLite, DuckDB, Parquet and Iceberg on S3 through dp-lake — and cross-engine joins happen in an in-memory staging database that exists only for the run. Nothing is copied to a warehouse; nothing lands. A release is immutable, and a released pipeline can be published as a GET endpoint.

One server, eight engines

An MCP server for your SQL database

Postgres, Oracle, SQL Server, MySQL, H2, DuckDB, SQLite and dp-lake, through one endpoint and one set of scoped keys. Each page has that engine's driver facts and a copy-paste client config.

Every dialect has its page — H2 (also the staging engine) and dp-lake (LAKE: Parquet and Iceberg on S3 read in place) included. The dp-lake product page →

Start with the SQL MCP server overview →

The agent loop

How it works

Agents are first-class principals here — not a plugin bolted onto a human UI. Your agent gets its own scoped credentials, its own protocol surface, and errors it can parse without scraping stack traces.

  1. Mint an API key

    Mint it on the API section — /api-console, where the endpoints it may call and the MCP connection already live — or with POST /api/v1/auth/api-keys.

    Keys are shown once, stored Argon2id-hashed, pinned to one workspace, given an expiry, and carry hierarchical scopes (read, execute, author — admin is for people, never issued to a key). An agent's key and a program's key are the same kind of credential.

  2. Point the agent at /mcp

    The MCP endpoint authenticates API keys only — DP-API-Key header or Bearer dpk_…. No cookies, no session roulette.

    Exactly 41 tools cover the full lifecycle: introspect datasources, author templates and pipelines, publish endpoints, register lake tables, execute, inspect.

  3. Author as a JSON contract

    The agent creates a SQL template and a pipeline as declarative JSON — nodes, dependencies, parameters. Diffs review like code, because they are.

  4. Execute and watch

    Execution streams per-milestone events over SSE, with heartbeats, so the agent knows where things stand.

  5. Read row-level results

    executions_get_result returns the actual rows. The agent verifies its own work instead of asserting success.

Agent-to-engine flow An AI agent with a scoped API key talks to the /mcp endpoint (41 tools), authors a pipeline as a versioned JSON contract, which executes against PostgreSQL, MySQL, SQLite and the rest of the engine catalog through a per-execution staging tempdb, returning row-level results and stable error codes to the agent. Your AI agent scoped API key · pinned to one workspace /mcp — 41 MCP tools API-key-only: DP-API-Key or Bearer dpk_… Pipeline — versioned JSON contract templates · nodes · depends_on · parameters PostgreSQL MySQL SQLite + SQL Server, Oracle, DuckDB, H2, LAKE Per-execution staging tempdb isolated in-memory H2 · joins across engines Row-level results + stable error codes back to the agent, machine-readable

This isn't a toy loop: the published demo set is ~4.9M sampled NYC taxi rows across Postgres, MySQL and SQLite, US trade at HS-6 grain on DuckDB, and 471,851,707 rideshare trips as Parquet on S3 that nothing downloads. An agent authors and runs against exactly that.

Shipped, not promised

What's in the box

Each card cites the spec section it comes from — view source and spot-check us. Caveats included where they exist — honesty is the brand.

Author

Versioned JSON contracts

Pipelines are declarative JSON — schema_version, nodes, depends_on, parameters, settings. Every update mints a new immutable integer version, and the contract is additive-only.

Freemarker SQL templates

A save-time scan walks the parsed template AST against a forbidden-construct list — ?eval, ?new, ?api, in-body imports — so SSTI vectors are rejected before a template is ever saved.

Declared parameters bind as :name on a prepared statement — never interpolated. Interpolating one into the SQL text instead is refused at save with template.validation.parameter_interpolated.

Template versions, pinned by hash

Every template version is immutable — body, imports, dialect and engine are frozen once stored, versions are never reused or renumbered, and each carries a content hash. Pipelines pin {name, version}, so a new template version is invisible to them until an author moves the pin.

Templates are typed: sql (dialect required) or html (dialect absent, auto-escaping on). The type is fixed at creation and a database constraint enforces the pair.

Names are paths, and a folder is mandatory

A template's or pipeline's name is its path — nyc/mobility/revenue_by_borough, 2 to 10 segments, and the first one is a folder: the root holds folders only. A flat name is refused with details.reason = "folder_required".

An agent that invents a NEW top-level root is refused until it says so on purpose: confirm_new_root, with the roots that already exist in the error's details. Asking a person before minting a namespace is a guarantee here, not advice.

Read the spec →

Draft → release → discard

One draft per entity, at most. Releasing locks the version: we never modify a released pipeline — the next change copies it into a new draft. Discard drops the draft and returns the number to the pool.

The editor's version select is read-only for anything but the working version, and Edit reuses the existing draft or copies the selected version into a new one — the rule is visible on screen, not just enforced behind it.

Used-by: impact before you change

The reverse arrow, answered as two different questions. Who uses this version now? scans every pipeline's working version and names the pipeline, the node id and the version carrying the pin — a fact you can go and change, not a count.

Is it safe to delete? scans every version ever, because historical versions stay executable. Delete is refused with 409 template.in_use while any pipeline version pins it.

Run

Eight engines, one contract

Postgres, Oracle, SQL Server, MySQL, H2, DuckDB, SQLite and dp-lake — dp-lake is object storage read in place. One pipeline contract over all of them.

Oracle and MySQL drivers are opt-in for license reasons (-Poracle/-Pmysql, or drop the jar in lib/); a missing driver fails at save with datasource.driver_not_loaded. Everything else is in the published image, the DuckDB extensions included — a hardened deployment needs no egress to an extension repository.

dp-lake: Parquet and Iceberg, read in place

A LAKE datasource queries Parquet and Apache Iceberg on S3 or any S3-compatible store with no warehouse and no load step. DuckDB is the engine; the catalog is the server's own registry, dp-catalog — tables are registered over REST or by an agent through the lake_tables_* tools.

Hive-partitioned Parquet is scanned with partition pruning, so a date predicate reads the days it names and not the table. It is a read connector: there is no write path to the bucket. The whole dp-lake story →

Read the spec →

Cross-engine staging joins

Each execution gets its own isolated in-memory H2 tempdb — created at start, destroyed at end. Nodes stage ResultSets there, so downstream nodes join across engines.

Pipeline composition

A PIPELINE node executes a version-pinned child pipeline as a linked child execution — in-process, with lineage and family cancellation. The child keeps its own execution record, tempdb, stats and stream.

The pipeline editor

The DAG renders on a full-bleed canvas as node cards, with the facts inside the box: name, a type badge, datasource · dialect, template @ version, and after a run, elapsed time and rows.

One dock, four tabs — Details, Results, Errors, Events: a failure opens Errors, every event in arrival order.

It executes, streams, cancels and pages results. What it deliberately does not do is author: no edit mode in v1 — authoring is agent-first.

Read the spec →

Failures you can actually read

A failed node carries the catalogued code, the message, the node's own facts (datasource, dialect, template @ version), the rendered SQL in :name form — never a bound value — and the original exception chain with frames, root cause last.

The same record, built once, reaches the node_failed SSE event, the stored error_json, the editor's error surface and the agent's executions_get. No surface strips what another shows.

Live execution streams, and a cancel that means it

POST /api/v1/pipelines/{id}/execute returns text/event-stream with per-milestone events and heartbeats. No stream resumption — a dropped connection cancels the execution rather than orphaning it.

Cancel reaches the executing instance through Redis from any instance, and a per-node latch refuses to enter the driver once it has fired. A cancelled execution cannot come back SUCCESS — the window where it could was closed and is covered by a test that injects the descheduling rather than racing it.

Operate

Promotion between environments

Push dev → staging → prod from the UI. The screen lists only released versions newer than the target's; drafts never leave the authoring environment, and templates and child pipelines travel first, as a dependency closure.

The credential is a server-kind API key, not a principal — no user account, so rotation and offboarding are independent of any person. Rotation is: mint a second, set it on the sender, revoke the first — no restart on either side.

Read the spec →

Multi-instance ready

Instances are symmetric and stateless for API, MCP, UI and auth — no sticky sessions. Only a running execution is pinned to one JVM; cancel works from any instance through Redis, and results and history are readable from all of them.

Datasource pool invalidation rides a Redis channel: repoint or rotate a credential on one instance and every other evicts its pool and rebuilds from the row.

A crashed instance's in-flight executions are swept to ABORTED by an idempotent stale-execution sweep every replica runs.

Read the spec →

One API section

Everything a program uses to talk to a workspace, on one screen: the endpoints you have published as GET /api/x/…, the keys that may call them, and the MCP connection block — the tool count rendered live from the catalog, never a literal.

It is honest about what it cannot show: no "calls in the last 24 h" column — every serve is audited, but nothing tallies them per endpoint — so the card states the absence instead of filling it from a sample.

Read the spec →

Connection pools you can see and set

Eight HikariCP settings are on the datasource form, each prefilled with its effective default and labelled with where that default came from. A field left alone is not persisted, so a later change to a product default still reaches the datasource.

Out-of-range values are refused, not accepted — every floor restates a rewrite Hikari would otherwise perform silently, and the test that justifies them builds a real pool and watches it happen.

Read the spec →

A saved datasource never kills a running query

Editing or deleting a datasource retires its pool instead of closing it: new leases build a fresh pool from the new row at once, idle connections close now, and a connection still in use closes when its borrower returns it — never mid-statement.

Delete asks the question first: the dialog runs the any-version usage scan before it offers anything, and lists pipeline › node (v3 released) for every reference. The 409 and the dialog cannot disagree — same scan.

Read the spec →

Append-only audit log

Logins, key lifecycle, scope denials, user admin, workspace events, promotion accept/reject, every MCP tool call and each credential-decryption point — recorded with actor, cause and source IP.

Catalogued error contract

Failures arrive as stable {domain}.{entity}.{failure} codes in a uniform error envelope — a catalog enforced against the code by drift tests, and the same codes over MCP. Agents get errors they can branch on, not prose to parse.

The pipeline the agent built, as the product draws it: three sources read in place, two staging steps in the per-run scratch database, one answer node — every count and timing from the recorded run.
SOURCES STAGING · H2 IN MEMORY ANSWER sample-lake · Parquet on S3 hvfhv_zone_day · 377k rows sample-reference · SQLite zones · 265 rows calculator · trailing_periods anchor 2025-01-01 → Q4 2024 stage_company_zone 523 rows · 482 ms stage_zones 263 rows · 204 ms answer 6 rows · caller DRAFT demo/top_company_by_borough · v1 · awaiting a human release · then GET /api/x/demo/…
The run, recorded: the agent's steps with their row counts and timings, then the rows themselves — which is how an agent checks its own work instead of asserting it.
workspace: demo · agent session

you › Which rideshare company carried the most trips in each borough last quarter?

  • read the datasource facts, columns and stats — hvfhv_zone_day is a census at zone × day × company
  • resolved "last quarter" from the data's last day — Q4 2024
  • rendered 3 templates, ran the draft: 4 nodes · 763 ms
  • draft demo/top_company_by_borough v1 — left for a human to release
boroughtop companytripsborough tripsshare
Manhattan Uber 17,660,839 23,669,163
Brooklyn Uber 11,946,362 16,057,699
Queens Uber 9,786,602 12,847,231
Bronx Uber 6,078,750 7,572,899
Staten Island Uber 715,593 918,360

GET /api/x/demo/top-company-by-borough?anchor_date=2025-01-01 · after release · key scoped to /demo

from the demo workspace — a real result, not a promise

One API section: the keys, the endpoints those keys may call, and the MCP connection — with the tool count read from the catalog at request time, exactly as this page reads it.
workspace: demo · API
endpointserveskey reach
GET /api/x/demo/top-company-by-borough?anchor_date=2025-01-01 demo/top_company_by_borough · v1 /demo/**

from the demo workspace — released by a human, served as this version forever

Govern

Workspaces & team isolation

Every request resolves exactly one workspace; all authored content is workspace-scoped; API keys are pinned to one workspace; membership roles are additive flags — viewer, author, promoter, workspace admin and super admin.

The management UI ships: create, switch, manage members, a navbar switcher and add-by-email.

A key has a kind, and the kind decides its reach

Three kinds. A user key is an agent's key and a program's key — its authority is its scopes. An endpoint key authorises the published paths it is bound to. A server key opens the promotion routes and nothing else.

The confinement is decided centrally, before any handler's annotation is read — a new route cannot become quietly reachable to a scopeless key.

Read the spec →

The agent never sees a credential

There is deliberately no tool that registers a datasource, and none that mints a key: a credential through a tool call transits the agent's context and transcript. A person adds the datasource in the UI; the agent uses it by name.

What the agent keeps is everything that needs no secret: list, get, test, schemas, tables, columns, preview rows — enough to author without a password.

Read the spec →

OIDC SSO + scoped agent keys

Humans sign in through OIDC SSO. Agents get their own API keys with hierarchical scopes — separate principals, separate blast radii.

Local password accounts

No identity provider? Turn on local accounts and the whole product runs with zero external setup. Hashes are Argon2id through the same hasher API keys use; there is no self-registration — an admin creates the account, or config seeds the first admin only.

Every seeded or admin-reset credential is one-time and the app refuses every other screen until it is changed. Five consecutive failures lock the account — and the unknown-email path spends the same hashing memory, so timing cannot enumerate.

Read the spec →

Read-only datasource governance

is_readonly forbids write-shaped uses — enforced at save time, at an executor backstop, and as Hikari readOnly. Admin-gated on globals. The flag is a contract, not containment: pair it with a SELECT-only DB user.

Encrypted datasource credentials

Passwords are AES-256-GCM encrypted with the row's name bound as additional authenticated data, so a ciphertext moved between rows fails to decrypt. The app refuses to start without a valid key.

Every credential carries a key version and datapipelines.db.key-provider selects where the key comes from — one contract test every provider passes.

Charts and dashboards are next — see the roadmap.

Your infra, your rules

Security & self-hosting

Runs on your hardware, under your policies. The defaults are paranoid so you don't have to be.

  • Authenticated by default. Every endpoint requires auth unless it's explicitly allowlisted.
  • Two variables, two files. An org sets what it calls the environment (DATAPIPELINES_ENV) and how careful the product should be (DATAPIPELINES_POSTURE: development or hardened). Configuration lives in exactly two files — a tracked deploy/env/defaults.env carrying every non-secret default, and a git-ignored deploy/secrets.env that ./app.sh --scaffold writes for you with every secret generated. The hardened posture refuses the demo at boot.
  • The rate limiter fails closed. If Redis — where the counters live — is unreachable, requests are refused with 429 rate_limit.unavailable rather than waved through. A limiter that opens under its own outage is not a limit.
  • Encrypted credentials. Datasource passwords are AES-256-GCM encrypted with the row name bound as AAD — and the app refuses to start without a valid key.
  • Least privilege as the documented pattern. The demo provisions a SELECT-only database login; the same pattern is documented for your own datasources.
  • No surprise egress. The DuckDB extensions dp-lake needs are baked into the published image and loaded from disk, so a hardened deployment never reaches out to an extension repository at runtime. What a lake query costs in bytes is bounded by the query's own predicates, and the docs say so before you turn it on.
  • TLS is your job. Terminate TLS at your own proxy — we don't pretend a built-in would fit every network.

Start with one question your team needs answered.

Explore the sample data first. Connect your own databases when you’re ready.