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.