In this tutorial you’ll build a working pipeline — a database source, a cleanup step, and a daily-revenue aggregate — and run it, all through the MCP server from a connected client like Claude Code. You never touch the UI: the client authors the computation graph as a single document, then asks Panels to execute it. By the end you’ll understand the core authoring loop every MCP client uses: discover → write → run → verify → iterate.
The MCP tool surface is self-describing. get_schema(<kind>) returns the live, apply-layer-enforced shape of any document or registry — call get_schema(graph_dsl) before authoring and get_schema(node_types) for the current node-type alphabet. This tutorial names specific tools and types, but get_schema is always the source of truth if anything here has moved on.

Prerequisites

  • A Panels account (sign up free).
  • An MCP client connected to your workspace. If you haven’t done this, follow Connect a client first — one command in Claude Code:
  • A database connection secret in your workspace (this tutorial uses one named postgres_prod). Any reachable Postgres works; swap the secret name and table for your own.

What you’ll build

Three nodes, one graph document. Each node declares its type, a config (how it runs), its inputs (upstream node names), and a typed payload — source for ingest, transform for SQL.

Build it

1

Find your project

Every graph lives inside a project. List what you can reach and keep the project_id you want to build in:
2

Find or create the graph

A project’s computation graph is a scope document at project.dsl/graph/<graph_id>. List existing graphs; if there are none, reading the bare path once lazy-creates the first graph, after which it has a stable id:
Keep the etag — every write is optimistic-concurrency checked against it. If a write returns ETAG_STALE, re-read and retry with the fresh etag.
3

Write the pipeline

When the graph is empty you can author the whole thing in one core_write_scope call. orders_raw pulls a Postgres table; orders_clean drops cancelled rows; revenue_by_day aggregates and materialises as a table:
Pass it as the content of the write:
Reference upstream nodes by their node name, unqualified — FROM orders_raw, not the fully-qualified materialised table name. The default schema is already the project schema, and the resolver substitutes it automatically. The SQL engine is DuckDB, so author DuckDB spellings (date_trunc('day', ts), date_diff('day', a, b)) rather than Postgres or Snowflake function names.
4

Run it

core_run_node walks the upstream chain automatically — calling it on the leaf materialises orders_raw and orders_clean first, then revenue_by_day:
5

Verify

core_list_nodes reports each node’s schema_summary, row_count, and last_run.status. Filter to the nodes that produced data:
core_read_scope never returns table rows inline — to read the actual data, use the artifact-presign URL on a node’s last_run.
6

Iterate

For follow-up changes, prefer core_edit_scope (a surgical text splice that preserves the rest of the document and produces a readable diff) over rewriting the whole graph. Say the source table was misnamed:

Make it interactive: add a selection

A selection node is an interactive filter over upstream data — a status picker, a date range, a region list. Add one and wire a query to it, and that query re-filters whenever the value changes. The important part is how selections connect: you do not reference a selection in SQL. Its active clauses are composed into the downstream WHERE by the server at evaluation time, so wiring the input edge is the whole job.
There is no selection. template grammar — an invented token like {{ selection.status }} is not rewritten by anything; it reaches DuckDB with its braces intact and dies on a syntax error. The only template namespaces are {{param.X}} (and {{slot.X}} for component slots). And parameter is not a graph-DSL node type — it exists in the domain but authoring one in a graph document rejects with UNKNOWN_TYPE. When in doubt, get_schema(node_types).
Declare the selection node. Its payload rides definition, and data_type is the value shapescalar, list, or range — not a SQL type:
Then wire the consumer to it by adding the selection to that node’s inputs — and leave the SQL untouched:
Now any query downstream of status_filter re-filters server-side when the selection changes — no SQL edit, no extra wiring. To give it a UI affordance, add a selection element to a view (see view authoring).

Next steps

MCP server reference

Endpoints, OAuth, and the operator scope for privileged operations.

Build a dashboard

Turn a pipeline’s output into an interactive, shareable dashboard.