Skip to content

Data Sources & Variable Binding

Overview

Every block on a page can have a data source that tells the runtime where to fetch data from. Data sources can also declare variable bindings — instructions for injecting runtime values (URL parameters, authenticated user info, parent record fields, or static defaults) into queries and filters.

Variable bindings are the mechanism that makes pages dynamic. They let you build patterns like list-to-detail navigation, user-scoped views, and pre-filtered dashboards without writing any code.

Data source types

A data source connects a block to your data. There are two types:

Type Description Use case
structure Fetches records directly from a collection Tables, record cards, forms, metrics
query Executes a saved query (smart query) Complex joins, pre-built reports

Structure data source

A structure data source points to a collection by ID and can specify fields, filters, sorting, and pagination:

{
  "dataSource": {
    "type": "structure",
    "ref": "COLLECTION_ID",
    "config": {
      "fields": ["name", "email", "status", "createdAt"],
      "sort": [{ "field": "createdAt", "direction": "desc" }],
      "limit": 50
    }
  }
}

The mode field controls how data is fetched:

Mode Behavior
list Returns multiple records (default for tables)
single Returns one record by ID (default for detail pages)
aggregate Returns computed metrics (count, sum, avg, min, max)

Query data source

A query data source executes a saved query by ID. Variables are passed as query parameters:

{
  "dataSource": {
    "type": "query",
    "ref": "QUERY_ID",
    "config": {},
    "variables": {
      "status": { "source": "static", "value": "active" }
    }
  }
}

When variables are resolved at runtime, they are passed to the query as parameters. The query definition determines how those parameters are used (e.g., as filter values, in WHERE clauses).

Variable binding

Variable bindings map named variables to runtime values. They are declared in the variables field of a data source:

{
  "dataSource": {
    "type": "structure",
    "ref": "COLLECTION_ID",
    "config": {},
    "variables": {
      "assignedTo": { "source": "auth", "field": "userId" },
      "status": { "source": "static", "value": "open" }
    }
  }
}

At runtime, the Pages service resolves each variable binding against the current page context and injects the resulting values as filters (for structure sources) or parameters (for query sources).

The four variable sources

1. URL — source: "url"

Reads a value from the URL query string. Use this for navigation-driven filtering, such as passing a record ID from a list page to a detail page.

{
  "source": "url",
  "param": "id"
}

When the page is loaded at https://pages.centrali.io/acme/order-detail?id=abc-123, this binding resolves to "abc-123".

2. Auth — source: "auth"

Reads a value from the authenticated user's context. Use this for user-scoped views.

{
  "source": "auth",
  "field": "userId"
}

Available fields:

Field Description
userId The authenticated user's ID
email The authenticated user's email address
name The authenticated user's display name

Requires authentication

Auth bindings only resolve when the page's access policy is set to authenticated or role-gated. On public pages, auth bindings will fail to resolve.

3. Record — source: "record"

Reads a value from the primary record on a detail page. Use this for related lists that should be scoped to the current record.

{
  "source": "record",
  "field": "id"
}

The field can be any field on the primary record: "id", "status", "customerId", or any custom data field.

Two-phase resolution

The runtime resolves data sources in two phases. In phase 1, it fetches all blocks that do not depend on record context (including the primary record). In phase 2, it resolves blocks with record bindings using the primary record from phase 1. This means record bindings work automatically on detail pages — no extra configuration needed.

4. Static — source: "static"

Provides a literal default value. Use this for pre-filtered views where the filter value is known at design time.

{
  "source": "static",
  "value": "active"
}

Variable precedence

When a data source has multiple variables, each is resolved independently according to its declared source. The four sources have a defined precedence order when evaluating fallback behavior:

Priority Source Description
1 (highest) url URL query parameters
2 record Primary record context
3 auth Authenticated user context
4 (lowest) static Literal default value

Note

Precedence applies within the variable resolver when determining which source to check first. Each variable binding declares exactly one source — precedence matters when you are reasoning about which runtime values are available and in what order they are evaluated.

Common patterns

A common pattern: a list page shows all orders. Clicking a row navigates to a detail page that shows the order and its line items.

List pageorders (page type: list):

{
  "sections": [
    {
      "id": "sec-1",
      "kind": "content",
      "title": "Orders",
      "layout": "single-column",
      "blocks": [
        {
          "id": "block-orders",
          "blockType": "table",
          "dataSource": {
            "type": "structure",
            "ref": "ORDERS_COLLECTION_ID",
            "config": {
              "fields": ["orderNumber", "customer", "total", "status"],
              "sort": [{ "field": "createdAt", "direction": "desc" }]
            }
          },
          "actions": [
            {
              "id": "action-view",
              "type": "navigate-to-page",
              "label": "View Order",
              "targetRef": "order-detail",
              "activation": "row-click",
              "config": { "useQueryParams": true },
              "paramConfig": {
                "source": "row",
                "mode": "selected",
                "selectedFields": ["id"]
              }
            }
          ]
        }
      ]
    }
  ]
}

Detail pageorder-detail (page type: detail):

{
  "sections": [
    {
      "id": "sec-header",
      "kind": "content",
      "title": "Order Details",
      "layout": "single-column",
      "blocks": [
        {
          "id": "block-order",
          "blockType": "record-card",
          "dataSource": {
            "type": "structure",
            "ref": "ORDERS_COLLECTION_ID",
            "mode": "single",
            "config": {
              "fields": ["orderNumber", "customer", "total", "status", "createdAt"]
            }
          }
        }
      ]
    },
    {
      "id": "sec-items",
      "kind": "content",
      "title": "Line Items",
      "layout": "single-column",
      "blocks": [
        {
          "id": "block-items",
          "blockType": "related-list",
          "dataSource": {
            "type": "structure",
            "ref": "LINE_ITEMS_COLLECTION_ID",
            "mode": "list",
            "config": {
              "fields": ["product", "quantity", "unitPrice", "lineTotal"]
            },
            "variables": {
              "data.orderId": { "source": "record", "field": "id" }
            }
          }
        }
      ]
    }
  ]
}

When the detail page loads with ?id=abc-123:

  1. Phase 1: The block-order record card fetches the order with ID abc-123.
  2. Phase 2: The block-items related list resolves data.orderId from the order record's id field, then fetches line items where data.orderId = abc-123.

User-scoped views ("My Approvals")

Show only records assigned to the currently signed-in user:

{
  "dataSource": {
    "type": "structure",
    "ref": "APPROVALS_COLLECTION_ID",
    "mode": "list",
    "config": {
      "fields": ["title", "requestedBy", "amount", "status"],
      "sort": [{ "field": "createdAt", "direction": "desc" }]
    },
    "variables": {
      "data.assignedTo": { "source": "auth", "field": "userId" }
    }
  }
}

At runtime, data.assignedTo is resolved to the authenticated user's ID and applied as a filter. The user only sees approvals assigned to them.

Pre-filtered dashboard with static defaults

Show only active records on a dashboard metric:

{
  "dataSource": {
    "type": "structure",
    "ref": "TICKETS_COLLECTION_ID",
    "mode": "aggregate",
    "config": {},
    "aggregation": {
      "operations": {
        "openTickets": { "count": "*" }
      }
    },
    "variables": {
      "data.status": { "source": "static", "value": "open" }
    }
  }
}

The data.status variable is resolved to "open" and applied as a filter before the aggregation runs.

Smart query with URL parameter

Use a saved query that accepts a parameter from the URL:

{
  "dataSource": {
    "type": "query",
    "ref": "QUERY_ID",
    "config": {},
    "variables": {
      "customerId": { "source": "url", "param": "customerId" }
    }
  }
}

The query receives customerId as a parameter. The query definition determines how it uses that value (e.g., filtering by customer ID).

Error handling

When a variable cannot be resolved, the runtime returns an empty result set for that block instead of showing unfiltered data. This is a deliberate safety measure — a page should never accidentally display all records when a filter variable is missing.

The block's response includes a variableError field explaining what went wrong:

{
  "data": [],
  "meta": { "total": 0, "page": 1, "pageSize": 50 },
  "variableError": "Missing required variable: data.orderId (source: record, field: id) — primary record not available"
}

Common resolution errors:

Error Cause Fix
Missing URL param URL does not contain the expected query parameter Ensure the navigation action passes the required parameter
User not authenticated Auth binding on a public page Set the page access policy to authenticated or role-gated
Primary record not available Record binding but no record ID in URL Ensure the detail page receives a record ID via the id query parameter
Static value empty Binding declared but value is empty string Check the value field in the static binding

Never unfiltered

If any variable in a data source fails to resolve, the entire data source returns empty. The runtime will not fall back to an unfiltered query. This prevents accidental data exposure.

Worked example: complete list-to-detail setup

This example walks through a complete setup: a Projects list page that navigates to a Project Detail page showing the project record and its related tasks.

Step 1: Create the list page

curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Projects",
    "slug": "projects",
    "pageType": "list"
  }'

Step 2: Save the list page definition

curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages/PAGE_ID/versions \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "definition": {
      "sections": [
        {
          "id": "sec-projects",
          "kind": "content",
          "title": "Projects",
          "layout": "single-column",
          "blocks": [
            {
              "id": "block-projects",
              "blockType": "table",
              "dataSource": {
                "type": "structure",
                "ref": "PROJECTS_COLLECTION_ID",
                "config": {
                  "fields": ["name", "owner", "status", "dueDate"],
                  "sort": [{ "field": "dueDate", "direction": "asc" }]
                }
              },
              "actions": [
                {
                  "id": "action-open",
                  "type": "navigate-to-page",
                  "label": "Open",
                  "targetRef": "project-detail",
                  "activation": "row-click",
                  "config": { "useQueryParams": true },
                  "paramConfig": {
                    "source": "row",
                    "mode": "selected",
                    "selectedFields": ["id"]
                  }
                }
              ]
            }
          ]
        }
      ],
      "theme": { "inherit": true }
    }
  }'

Step 3: Create the detail page

curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Project Detail",
    "slug": "project-detail",
    "pageType": "detail"
  }'

Step 4: Save the detail page definition with variable bindings

curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages/DETAIL_PAGE_ID/versions \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "definition": {
      "sections": [
        {
          "id": "sec-project",
          "kind": "content",
          "title": "Project",
          "layout": "single-column",
          "blocks": [
            {
              "id": "block-project",
              "blockType": "record-card",
              "dataSource": {
                "type": "structure",
                "ref": "PROJECTS_COLLECTION_ID",
                "mode": "single",
                "config": {
                  "fields": ["name", "owner", "status", "dueDate", "description"]
                }
              }
            }
          ]
        },
        {
          "id": "sec-tasks",
          "kind": "content",
          "title": "Tasks",
          "layout": "single-column",
          "blocks": [
            {
              "id": "block-tasks",
              "blockType": "related-list",
              "dataSource": {
                "type": "query",
                "ref": "TASKS_BY_PROJECT_QUERY_ID",
                "config": {},
                "variables": {
                  "projectId": { "source": "record", "field": "id" }
                }
              }
            }
          ]
        }
      ],
      "theme": { "inherit": true }
    }
  }'

Step 5: Publish both pages

# Publish the list page
curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages/LIST_PAGE_ID/publish \
  -H "Authorization: Bearer YOUR_TOKEN"

# Publish the detail page
curl -X POST https://api.centrali.io/workspace/my-workspace/api/v1/pages/DETAIL_PAGE_ID/publish \
  -H "Authorization: Bearer YOUR_TOKEN"

How it works at runtime

  1. User visits https://pages.centrali.io/my-workspace/projects
  2. The list page shows all projects in a table
  3. User clicks a row — the navigate-to-page action redirects to https://pages.centrali.io/my-workspace/project-detail?id=PROJECT_RECORD_ID
  4. The detail page resolves:
    • Phase 1: Fetches the project record using the id from the URL
    • Phase 2: Passes the project's id into the projectId variable of the tasks query, returning only tasks for that project
  5. The user sees the project details and its related tasks
  • Pages Overview — Page types, sections, blocks, and access control
  • Actions — Navigation, record operations, and trigger invocation
  • Queries — Creating saved queries that accept parameters