Docs

Pipelines

A pipeline transforms datasets. Reach for one after a sync lands raw source rows that need to be cleaned, filtered, reshaped, joined, or made ready for a projection.

A pipeline is a sequence of steps. Each step reads one or more input datasets and writes one output dataset. Steps are inert definitions — a step only runs when a pipeline references it with .then(...).

Define a pipeline

Suppose a sync has populated erpInvoicesDataset with raw ERP rows. This pipeline keeps only billable invoices, then derives a finance summary table.

TSpipelines/invoice-reporting.ts
import { definePipeline, definePipelineStep, defineSchedule, events } from "@sixb/core"
import {
  erpBillableInvoicesDataset,
  erpInvoiceSummariesDataset,
  erpInvoicesDataset,
} from "../datasets/erp"

export const billableInvoicesStep = definePipelineStep("billable-invoices")
  .inputs({ invoices: erpInvoicesDataset })
  .output(erpBillableInvoicesDataset)
  .sql(({ invoices }) => `
    select *
    from ${invoices}
    where status in ('sent', 'paid', 'overdue')
  `)

export const invoiceSummariesStep = definePipelineStep("invoice-summaries")
  .inputs({ invoices: erpBillableInvoicesDataset })
  .output(erpInvoiceSummariesDataset)
  .sql(({ invoices }) => `
    select id, number, amount, currency, status, customer_id, project_id
    from ${invoices}
  `)

export const invoicesUpdated = defineSchedule("erp-invoices-updated").on(
  events.dataset(erpInvoicesDataset).updated()
)

export const invoiceReportingPipeline = definePipeline("invoice-reporting")
  .when(invoicesUpdated)
  .then(billableInvoicesStep)
  .then(invoiceSummariesStep)

This runs whenever erpInvoicesDataset commits a new version. The first step writes billable invoices; the second reads that output and writes a trimmed summary.

PartMeaning
definePipelineStep("billable-invoices")Defines one transform step
.inputs({ invoices })Names the datasets the step reads
.output(dataset)The dataset the step writes
.sql(...) / .run(...)The transform (terminal — pick one)
definePipeline("invoice-reporting")Names the pipeline
.when(schedule)Named schedule that runs the pipeline
.then(step)Appends a step to the sequence

Step builder

definePipelineStep(id) chains in a fixed order: .inputs(...), then .output(...), then a terminal .sql(...) or .run(...). Input keys become the names you read inside the executor.

MethodPurpose
.inputs(record)Named input datasets, e.g. { invoices: erpInvoicesDataset }. At least one required.
.output(dataset, options?)Output dataset and optional write mode.
.sql(fn)SQL transform. fn receives each input as an interpolatable ref.
.run(handler)TypeScript transform. handler receives a run context.

Write mode

TS
.output(erpInvoiceSummariesDataset, { mode: "append" })
modeBehavior
"snapshot" (default)Writes a full replacement version.
"append"Appends rows to the output dataset.

SQL steps

Use SQL when the transform is a query — select, rename, filter, join, aggregate. Each input name interpolates into the query as a table reference.

TS
export const overdueInvoicesStep = definePipelineStep("overdue-invoices")
  .inputs({ invoices: erpInvoicesDataset })
  .output(erpOverdueInvoicesDataset)
  .sql(({ invoices }) => `
    select id, number, amount, currency, customer_id
    from ${invoices}
    where status = 'overdue'
  `)

SQL steps run on the DuckDB dialect and require a lake storage provider with SQL transform support. See connectors for storage providers.

TypeScript steps

Use .run(...) when the transform needs application logic, library calls, or row-by-row work that does not fit SQL. The handler receives a context with the named inputs and an output.

TS
import type { DatasetRow } from "@sixb/core"

async function* billable(rows: AsyncIterable<DatasetRow>) {
  for await (const row of rows) {
    if (row.status === "sent" || row.status === "paid" || row.status === "overdue") {
      yield row
    }
  }
}

export const billableInvoicesStep = definePipelineStep("billable-invoices")
  .inputs({ invoices: erpInvoicesDataset })
  .output(erpBillableInvoicesDataset)
  .run(async ({ inputs, output }) => {
    await output.writeRows(billable(inputs.invoices.readRows()))
  })

The run context exposes:

FieldTypeDescription
inputs[name]PipelineStepInputOne per .inputs(...) key. Has dataset, version, readRows(input?).
outputPipelineStepOutputwriteRows(rows) accepts a sync or async iterable of rows.
projectId, pipelineId, stepId, runIdstringIdentifiers for the current run.
signalAbortSignalAborts when the run is cancelled.

readRows(input?) returns an AsyncIterable of rows; pass read options to scope the read. Prefer SQL when the transform is a query; reach for TypeScript when it needs code.

Compose steps

Steps run in order, and each commits its output before the next runs. A later step can read a dataset an earlier step wrote, so chains build progressively richer tables.

TS
export const invoiceReportingPipeline = definePipeline("invoice-reporting")
  .when(invoicesUpdated)
  .then(billableInvoicesStep)
  .then(invoiceSummariesStep)

Schedules

Pass .when(...) a named schedule. Add more than one to run from multiple schedules.

Schedule sourceRuns when
defineSchedule(...).on(events.dataset(dataset).updated())A dataset commits a version
defineSchedule(...).on(events.sync(sync).succeeded())A sync succeeds
defineSchedule(...).on(events.pipeline(pipeline).succeeded())A pipeline succeeds
defineSchedule(...).cron(...)A cron expression elapses
TS
import { defineSchedule } from "@sixb/core"

const nightly = defineSchedule("nightly-invoices").cron("0 2 * * *")

export const nightlyInvoices = definePipeline("invoice-reporting")
  .when(nightly)
  .then(billableInvoicesStep)

Pipeline vs sync

NeedUse
Read from an external system (e.g. the ERP)Sync
Write raw source rowsSync
Clean or reshape rowsPipeline
Join datasetsPipeline
Create projection-ready rowsPipeline

A good rule: sync first, shape later.

Registration

createSixb() discovers exported pipeline definitions from pipelines/ automatically.

TXT
your-project/
  datasets/
    erp.ts
  syncs/
    erp.ts
  pipelines/
    invoice-reporting.ts
  projections/
    invoices.ts
  sixb.config.ts

You can also register pipelines explicitly:

TS
import { createSixb } from "@sixb/core"
import { erpBillableInvoicesDataset, erpInvoicesDataset } from "./datasets/erp"
import { invoiceReportingPipeline } from "./pipelines/invoice-reporting"

export const sixb = await createSixb({
  datasets: [erpInvoicesDataset, erpBillableInvoicesDataset],
  pipelines: [invoiceReportingPipeline],
})

Running pipelines

In local development, sixb dev co-hosts pipeline workers when pipelines are registered.

In production, run a dedicated pipeline worker:

BASH
sixb worker pipeline

For constrained deployments, sixb worker-group co-hosts several queue workers in one process:

BASH
sixb worker-group sync pipeline projection

See deployment for running workers in production.

Notes

  • Steps write snapshot output by default; use { mode: "append" } when you mean to add rows.
  • Pipeline steps run sequentially.
  • If a later step fails, earlier committed dataset versions stay durable.

Keep each pipeline focused on turning one table shape into a better one.

Next

  • Projections — map pipeline output rows onto ontology objects
  • Datasets — define the input and output tables
  • Syncs — get raw source rows into Sixb

Search docs

Search the documentation