---
title: Development workflow examples
meta:
  description: Examples for iterating on Tinybird projects with the Tinybird CLI, the TypeScript SDK, the Python SDK, branches, CI, and deployments.
---

# Development workflow examples

These examples follow the order you usually use in a project: build first, test locally or in a branch, validate in CI, then deploy.

Use the command set that matches your project format.

{% table %}
  * Task
  * Tinybird CLI
  * TypeScript SDK
  * Python SDK
  ---
  * Build or validate
  * `tb build`
  * `npx tinybird build`
  * `uv run tinybird build`
  ---
  * Preview a pull request
  * `git checkout -b ... && tb build`
  * `npx tinybird preview --check`
  * `uv run tinybird preview --check`
  ---
  * Check production deployment
  * `tb --cloud deploy --check`
  * `npx tinybird deploy --dry-run`
  * `uv run tinybird deploy --check`
  ---
  * Deploy to production
  * `tb --cloud deploy`
  * `npx tinybird deploy`
  * `uv run tinybird deploy --wait --auto`
{% /table %}

## Local workflow

Use Local for fast checks with fixture data. See [Tinybird Local](/forward/core-concepts/tinybird-local) for installation, Docker settings, persistence, local APIs, and local Tokens. Use a [Cloud Branch](/forward/development-workflow/using-cloud-branches) when you need production-like data or connector behavior.

### Build the project

Run a build before testing changes. It catches invalid resource definitions and dependency issues.

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb build
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird build
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird build
```

{% /tab %}
{% /tabs %}

For Tinybird CLI projects, `tb build` loads matching fixtures automatically. For example, `fixtures/events.ndjson` is loaded into `datasources/events.datasource`. See [Test your project](/forward/guides/test-your-project#fixture-files).

### Change an Endpoint and rebuild

Edit the resource, then rebuild the project.

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb local start --daemon
tb build
```

```tb {% title="datasources/events.datasource" %}
SCHEMA >
    `timestamp` DateTime `json:$.timestamp`,
    `user_id` String `json:$.user_id`,
    `event` LowCardinality(String) `json:$.event`,
    `plan` Nullable(String) `json:$.plan`

ENGINE "MergeTree"
ENGINE_SORTING_KEY "timestamp"
```

```tb {% title="endpoints/events_by_plan.pipe" %}
NODE endpoint
SQL >
    SELECT
        plan,
        count() AS events
    FROM events
    WHERE timestamp >= now() - INTERVAL 7 DAY
    GROUP BY plan
    ORDER BY events DESC

TYPE ENDPOINT
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
tb local start --daemon
npx tinybird build --local
```

```ts {% title="lib/tinybird.ts" %}
import { defineDatasource, defineEndpoint, engine, node, t } from "@tinybirdco/sdk";

export const events = defineDatasource("events", {
  schema: {
    timestamp: t.dateTime(),
    user_id: t.string(),
    event: t.string().lowCardinality(),
    plan: t.string().nullable(),
  },
  engine: engine.mergeTree({
    sortingKey: ["timestamp"],
  }),
});

export const eventsByPlan = defineEndpoint("events_by_plan", {
  nodes: [
    node({
      name: "endpoint",
      sql: `
        SELECT
          plan,
          count() AS events
        FROM events
        WHERE timestamp >= now() - INTERVAL 7 DAY
        GROUP BY plan
        ORDER BY events DESC
      `,
    }),
  ],
  output: {
    plan: t.string().nullable(),
    events: t.uint64(),
  },
});
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
tb local start --daemon
uv run tinybird build
```

```python {% title="tb_project/resources.py" %}
from tinybird_sdk import define_datasource, define_endpoint, engine, node, t

events = define_datasource("events", {
    "schema": {
        "timestamp": t.date_time(),
        "user_id": t.string(),
        "event": t.string().low_cardinality(),
        "plan": t.string().nullable(),
    },
    "engine": engine.merge_tree({"sorting_key": ["timestamp"]}),
})

events_by_plan = define_endpoint("events_by_plan", {
    "nodes": [
        node({
            "name": "endpoint",
            "sql": """
                SELECT
                    plan,
                    count() AS events
                FROM events
                WHERE timestamp >= now() - INTERVAL 7 DAY
                GROUP BY plan
                ORDER BY events DESC
            """,
        }),
    ],
    "output": {
        "plan": t.string().nullable(),
        "events": t.uint64(),
    },
})
```

{% /tab %}
{% /tabs %}

Call the local Endpoint after the build finishes:

```shell
curl -H "Authorization: Bearer <local-token>" \
  "http://localhost:7181/v0/pipes/events_by_plan.json"
```

## Branch workflow

Branches are short-lived Cloud environments. Use them to test changes without changing production.

### Develop in a Cloud Branch

For everyday branch development, create a Git branch and build. Tinybird creates or reuses a Cloud Branch with the same name as the Git branch.

```shell
git checkout -b add-plan-to-events
tb build
```

Rebuild after editing resources:

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb build
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird build
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird build
```

{% /tab %}
{% /tabs %}

Query the branch after the build finishes:

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb --branch=add-plan-to-events sql "SELECT plan, count() FROM events GROUP BY plan"
```

{% /tab %}
{% tab label="TypeScript SDK" %}

Copy a branch Token for the Endpoint you want to call, paste it into `TINYBIRD_TOKEN` in `.env.local`, and run your application code:

```shell
tb --branch=add-plan-to-events token copy <token_name>
```

```ts {% title="query-branch.ts" %}
import { tinybird } from "./lib/tinybird";

const result = await tinybird.eventsByPlan.query({});

result.data.forEach((row) => {
  console.log(row.plan, row.events);
});
```

{% /tab %}
{% tab label="Python SDK" %}

Copy a branch Token for the Endpoint you want to call, paste it into `TINYBIRD_TOKEN` in `.env.local`, and run your application code:

```shell
uv run tinybird --branch=add-plan-to-events token copy <token_name>
```

```python {% title="query_branch.py" %}
from tb_project.client import tinybird

result = tinybird.events_by_plan.query({})

for row in result["data"]:
    print(row["plan"], row["events"])
```

{% /tab %}
{% /tabs %}

When you need production-like data, create the branch explicitly with `--last-partition` before building:

```shell
tb branch create add-plan-to-events --last-partition
tb build
```

For connector Data Sources, create the branch with `--with-connections` and start or sample the connector when you are ready to test. See [Cloud Branches](/forward/development-workflow/using-cloud-branches#test-connector-data).

## Pull requests and CI

Run the same checks in CI that you run locally. For pull requests, create a preview branch so reviewers and preview apps can query the changed Tinybird project.

### Preview a pull request

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb branch create tmp_ci_my_feature --last-partition
tb --branch=tmp_ci_my_feature build
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird preview --check
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird preview --check
```

{% /tab %}
{% /tabs %}

Use `preview` in GitHub Actions, GitLab CI, Vercel, or another CI system to create an ephemeral branch for each pull request. See [Preview deployments](/forward/development-workflow/cicd#preview-deployments).

### Validate before deploying

Use deployment checks for changes that affect Data Source schemas, Materialized Views, engine settings, or connector configuration.

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb build
tb test run
tb --cloud deploy --check
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird build
npx tinybird deploy --dry-run
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird build
uv run tinybird deploy --check
```

{% /tab %}
{% /tabs %}

## Data Source evolution

For the migration rules behind Data Source changes, read [Evolve Data Sources](/forward/guides/evolve-data-source). The examples below show the most common cases.

### Rename a column with FORWARD_QUERY

Column renames need an explicit mapping. Tinybird sees a removed column and a new column. Without a mapping, the new column gets a default value.

Update the Data Source definition and run a deployment check before merging. With the Tinybird CLI, add `FORWARD_QUERY` to the `.datasource` file. In SDK projects, update the schema in code and review the generated deployment plan. If the plan requires a forward query, use the expression from the Data Source evolution guide as the source of truth.

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```tb {% title="datasources/events.datasource" %}
SCHEMA >
    `timestamp` DateTime `json:$.timestamp`,
    `account_id` String `json:$.account_id`,
    `event` LowCardinality(String) `json:$.event`

ENGINE "MergeTree"
ENGINE_SORTING_KEY "timestamp"

FORWARD_QUERY >
    SELECT timestamp, user_id AS account_id, event
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```ts {% title="lib/tinybird.ts" %}
import { defineDatasource, engine, t, type InferRow } from "@tinybirdco/sdk";

export const events = defineDatasource("events", {
  schema: {
    timestamp: t.dateTime(),
    account_id: t.string(),
    event: t.string().lowCardinality(),
  },
  engine: engine.mergeTree({
    sortingKey: ["timestamp"],
  }),
});

export type EventRow = InferRow<typeof events>;
```

```shell
npx tinybird deploy --dry-run
```

{% /tab %}
{% tab label="Python SDK" %}

```python {% title="tb_project/resources.py" %}
from tinybird_sdk import define_datasource, engine, t

events = define_datasource("events", {
    "schema": {
        "timestamp": t.date_time(),
        "account_id": t.string(),
        "event": t.string().low_cardinality(),
    },
    "engine": engine.merge_tree({"sorting_key": ["timestamp"]}),
})
```

```shell
uv run tinybird deploy --check
```

{% /tab %}
{% /tabs %}

The `FORWARD_QUERY` runs against the live Data Source during deployment. It must include the `SELECT` list only. Do not include `FROM` or `WHERE`.

### Add a nullable column with ALTER

Adding a nullable column is usually an automatic `ALTER` during deployment. The change is applied when the deployment is promoted to live, not while it is in staging. See [Automatic ALTER operations](/forward/guides/evolve-data-source#automatic-alter-operations).

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```tb {% title="datasources/events.datasource" %}
SCHEMA >
    `timestamp` DateTime `json:$.timestamp`,
    `user_id` String `json:$.user_id`,
    `event` LowCardinality(String) `json:$.event`,
    `plan` Nullable(String) `json:$.plan`

ENGINE "MergeTree"
ENGINE_SORTING_KEY "timestamp"
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```ts {% title="lib/tinybird.ts" %}
import { defineDatasource, engine, t, type InferRow } from "@tinybirdco/sdk";

export const events = defineDatasource("events", {
  schema: {
    timestamp: t.dateTime(),
    user_id: t.string(),
    event: t.string().lowCardinality(),
    plan: t.string().nullable(),
  },
  engine: engine.mergeTree({
    sortingKey: ["timestamp"],
  }),
});

export type EventRow = InferRow<typeof events>;
```

{% /tab %}
{% tab label="Python SDK" %}

```python {% title="tb_project/resources.py" %}
from tinybird_sdk import define_datasource, engine, t

events = define_datasource("events", {
    "schema": {
        "timestamp": t.date_time(),
        "user_id": t.string(),
        "event": t.string().low_cardinality(),
        "plan": t.string().nullable(),
    },
    "engine": engine.merge_tree({"sorting_key": ["timestamp"]}),
})
```

{% /tab %}
{% /tabs %}

Run a deployment check before merging the change:

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb --cloud deploy --check
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird deploy --dry-run
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird deploy --check
```

{% /tab %}
{% /tabs %}

### Change a type with safe conversion

If existing values might not cast cleanly, make the conversion explicit. This example changes `session_id` from `String` to `UUID` and uses a default UUID when the old value is invalid.

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```tb {% title="datasources/sessions.datasource" %}
SCHEMA >
    `timestamp` DateTime `json:$.timestamp`,
    `session_id` UUID `json:$.session_id`,
    `user_id` String `json:$.user_id`

ENGINE "MergeTree"
ENGINE_SORTING_KEY "timestamp"

FORWARD_QUERY >
    SELECT
        timestamp,
        accurateCastOrDefault(session_id, 'UUID') AS session_id,
        user_id
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```ts {% title="lib/tinybird.ts" %}
import { defineDatasource, engine, t, type InferRow } from "@tinybirdco/sdk";

export const sessions = defineDatasource("sessions", {
  schema: {
    timestamp: t.dateTime(),
    session_id: t.uuid(),
    user_id: t.string(),
  },
  engine: engine.mergeTree({
    sortingKey: ["timestamp"],
  }),
});

export type SessionRow = InferRow<typeof sessions>;
```

```shell
npx tinybird deploy --dry-run
```

{% /tab %}
{% tab label="Python SDK" %}

```python {% title="tb_project/resources.py" %}
from tinybird_sdk import define_datasource, engine, t

sessions = define_datasource("sessions", {
    "schema": {
        "timestamp": t.date_time(),
        "session_id": t.uuid(),
        "user_id": t.string(),
    },
    "engine": engine.merge_tree({"sorting_key": ["timestamp"]}),
})
```

```shell
uv run tinybird deploy --check
```

{% /tab %}
{% /tabs %}

After the deployment is live and you no longer need the migration expression, remove the `FORWARD_QUERY` in a follow-up change. Leaving an old `FORWARD_QUERY` can overwrite values in a later backfill.

## Deployments

After the checks pass, deploy the project or create a staging deployment for manual promotion.

### Deploy to production

{% tabs initial="Tinybird CLI" %}
{% tab label="Tinybird CLI" %}

```shell
tb --cloud deploy
```

{% /tab %}
{% tab label="TypeScript SDK" %}

```shell
npx tinybird deploy
```

{% /tab %}
{% tab label="Python SDK" %}

```shell
uv run tinybird deploy --wait --auto
```

{% /tab %}
{% /tabs %}

### Promote a staging deployment

If you prefer explicit staging and promotion, create a staging deployment, inspect it, then promote it:

```shell
tb --cloud deployment create --wait
tb --staging --cloud endpoint ls
tb --cloud deployment promote
```

For regular production changes, put the same checks in CI/CD. See [CI/CD](/forward/development-workflow/cicd).
