TUBELESS v0.2.12

Tubeless on Inngest

Run a Tubeless pipeline inside an Inngest step.run. Inngest owns event delivery, retries, and durable step results; Tubeless owns the typed graph, progress, and execution report within each attempt.

Event sender → Inngest function → step.run → Tubeless pipeline

Keep the pipeline invocation inside the durable step callback. Inngest may execute the function handler several times as it restores saved step results. Code outside steps can execute again. See how Inngest functions execute.

The example

File Responsibility
pipeline.ts Normalize and deduplicate rows using public Tubeless imports
functions.ts Validate event data and run the pipeline in a durable step with correlated logs
client.ts Configure the shared Inngest client
server.ts Serve the function at /api/inngest using Node’s HTTP server
send.ts Send a sample event and print its accepted event IDs

The pipeline has no Inngest imports and also runs locally. The example uses Inngest TypeScript SDK 4.21.0 and @inngest/test 1.0.0, installed as development dependencies here. Consumer applications install inngest themselves. Tubeless keeps its dependency-free runtime and adds no Inngest public API.

SDK v4 puts triggers in the createFunction options. Older v3 examples use a separate trigger argument; see the v4 migration guide.

Run locally

Use Node.js 22.6 or later and Bun 1.3.14 or later. From the repository root:

bun ci
bun run build
bunx tsc -p examples/inngest/tsconfig.build.json
INNGEST_DEV=1 node .context/inngest/server.js

The compiler writes JavaScript to .context/inngest/. The server listens on 127.0.0.1:3000. In a second terminal, start the Inngest Dev Server:

npx inngest-cli@latest dev -u http://127.0.0.1:3000/api/inngest

Open http://localhost:8288 and confirm that tubeless-example has synced and lists normalize-rows. In a third terminal, send the sample event:

INNGEST_DEV=1 node .context/inngest/send.js

The sender prints accepted event IDs. It does not wait for the pipeline result. Open the resulting run in the Dev Server UI: run-pipeline returns rows: ["alpha", "beta"], count: 2, a Tubeless runId, and preview: false. Sending again creates another event and function run. This example does not deduplicate separate submissions.

INNGEST_DEV=1 selects local development for both server and sender. For a deployment, host the endpoint where Inngest can reach it, remove that setting, configure INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY through your host’s secret configuration, and sync the app. The example server always binds to 127.0.0.1:3000. To reuse it in production, run a reverse proxy on the same host and forward your public HTTPS /api/inngest endpoint to http://127.0.0.1:3000/api/inngest, preserving the request method, body, and headers. Sync the public URL with Inngest. If your deployment cannot proxy to that loopback listener, register normalizeFunction using your application’s Inngest framework adapter instead of running server.ts. See serving functions and client configuration.

Validation, results, and observation

The function validates event.data before calling runOrThrow. Invalid rows or control metadata throw Inngest’s NonRetriableError directly. A TypeScript type on the sender cannot validate incoming events. Keep this validation outside the Tubeless invocation so its execution-error wrapper cannot hide the host’s non-retryable error type. See Inngest errors.

The correlation ID combines the Inngest run ID and durable step ID. It stays stable across retries of that step. Each pipeline attempt gets a fresh Tubeless runId. Only pass parentRunId when it identifies a known Tubeless execution; an Inngest run ID belongs in correlation metadata.

The pipeline returns plain JSON data. Inngest serializes step results, so avoid returning contexts, errors, signals, class instances, or a whole execution report. Large datasets should live in external storage with a small reference returned from the step. See step result serialization.

Pipeline logs, progress hooks, and trace events go through the function logger. The Inngest UI records one durable step for the pipeline. To inspect individual Tubeless steps in Studio, retain raw trace events using an application-owned exporter. Logger envelopes are not importable Tubeless NDJSON traces. Apply redaction before retaining or forwarding events.

Retries and durable boundaries

The function sets retries: 2: up to three attempts for the durable step. runOrThrow lets execution failures reach Inngest. A failed attempt reruns the entire pipeline, including any successful internal steps. Once Inngest saves the durable step’s successful result, subsequent handler executions reuse that result, including the original Tubeless runId, without invoking the pipeline.

Normalization and deduplication are safe to repeat. If you add writes, use business-level idempotency keys: a process can fail after a write but before its result is saved. Correlation IDs do not deduplicate side effects. See Inngest steps.

For independently retryable phases, define multiple Inngest steps at the function level, each calling a smaller Tubeless pipeline and passing JSON results to the next phase. Do not call Inngest step tools from inside Tubeless handlers or hooks nested in step.run. Place durable sleeps and event waits between these phases. Tubeless context.sleep is an ordinary process-local wait, and internal progress is not a durable checkpoint.

Cancellation and dry runs

Inngest cancellation prevents subsequent steps from running; it does not stop an already executing step. This adapter therefore does not supply a host cancellation signal. Cancelling a run in the dashboard can leave its active Tubeless pipeline running to completion. Closing the sender does not cancel the function. See Inngest cancellation.

The pipeline cooperates with signals supplied by local callers. Applications that need an execution deadline can supply their own signal and forward it to I/O, but a local abort is separate from Inngest cancellation. Keep work within the execution limits of your deployment and make side effects safe to retry.

Set dryRun: true in the sender’s job to forward Tubeless dry-run controls. This still sends a real event and runs an Inngest function. The example performs only pure transformations. Mark any added writes with dryRun: "skip" or a side-effect-free preview handler.

To plan or preview without contacting Inngest, use the project registration:

bun run tubeless -- plan --project examples/project/tubeless.project.ts inngest-normalize
bun run tubeless -- run --project examples/project/tubeless.project.ts inngest-normalize -- \
  --lines " Alpha " --lines "Beta" --dry-run

Verification

make check compiles the examples against the real SDK and uses InngestTestEngine to exercise the durable step with the real Tubeless pipeline. Tests cover JSON results, progress and traces, invalid input, dry runs, and mocked durable step output. The mocked-output test checks that the function returns the supplied value without calling the pipeline; it does not test persisted-state recovery. A separate test substitutes a real two-step Tubeless pipeline with fake storage I/O: its save handler fails once, then a fresh attempt reruns both load and save with stable correlation. These tests need no service or credentials and do not test server-side persistence or retry scheduling. Use the local server flow above to verify event delivery and inspect the run; test deployment authentication and side-effect idempotency separately.