Agent guide for tubeless
Use this guide when generating or modifying pipelines, pipeline-backed scripts, or shared helpers in this repository.
Workflow
- Read the recipe index and open the smallest matching example.
- Copy file layout and stable IDs from the project manifest.
- Declare stable IDs and operational descriptions. Add
nameonly when printed output needs a friendlier display name. - Model data dependencies before failure policy or CLI concerns.
- Typecheck the example or consumer, then run focused tests.
- Run
make checkfrom the package root after changing the package or its learning surface.
Primitive selection
- Use
createSteps<TDomainOptions>()once per pipeline anddefinePipelineonce after declaring its steps. Domain option types contain domain input only; callers pass those options and optional built-in controls torun(options, controls?), whileplan(controls)accepts controls alone. - Treat a
PipelineDefinitionErrorduring module loading as an authoring bug; static graph mistakes are rejected whendefinePipelineis called. - Use
dependsOnwhen the output is required,optionalDependsOnwhen absence is expected, andskipAfterFailureOffor a failure gate that supplies no data. - Use
createSteps(optionsSchema)when external domain options need Standard Schema validation or transformation. AddoutputSchemaonly at step boundaries that receive untrusted or independently checked values, andresultSchemawhen the finalized public result must be checked. Core imports no schema library. - Set
dryRun: "skip"on filesystem writes, database mutations, publication, email, and other steps whose normalrunmust not execute in a dry run. Use a typeddryRunhandler when the step should produce a preview value instead. - Use
step.skippableonly for an intentional successful outcome. Handle its resultingT | undefinedoutput type explicitly. - Use
fromPipelinefor one independently useful child workflow andforEachPipelinefor runtime fan-out with stable keys and bounded concurrency. UseforEachPipeline.skippableonly when the whole fan-out may be intentionally omitted; handle itsreadonly T[] | undefinedoutput explicitly. AsyncfromPipelineresult mappings publish resolved values; use that resolved shape for dependent inputs and policy-skip values. Parent plans keep these steps opaque but exposenestedPipelinewith the child pipeline id, declared step ids, and single/fan-out mode for presentation. The interactive CLI automatically expands selected child steps and fan-out items into nested progress rows and retains completed states. Inner progress counts and details propagate through both adapters; do not forward raw child hooks or duplicate this bookkeeping in consumers. Fan-outs materialize up to 32 live item groups by default and emit the full retained tree once at settlement. Setprogress.detailLimitto override that live cap and cap the final snapshot. - Use
fromRemotefor a unit of work that lives on another engine. Required fields areadapter,mapInput, andoutputSchema. OmittingdryRuncontacts the engine during a pipeline dry run; the adapter and remote worker must honorcontext.dryRunand authors prove the flag crossed the boundary inmapInput. Host-embed withpipeline.runOrThrowand passrunId/parentRunIdwhen the graph must outlive the process. Parent plans exposeremotewithengineand optionaltarget. Adapters may forward remote lines throughcontext.logand must rethrow remote failures asErrorwithcause/code. Copy the native-fetch boundary inremote-steps.ts, including validation of unknown JSON and forwarding the signal. Usehost-embedding.tsfor host-owned invocation; correlation IDs do not provide persistence or checkpoint/resume. - Use
runConcurrentfor bounded lightweight functions that do not need child lifecycle events. UserunConcurrentSettledwhen the caller needs completed results and the first failure instead of a throw. - Use
definePipelineCommandfor scripts centered on a pipeline. Do not parseprocess.argvmanually or redeclare built-in dry-run,--step, or--targetflags.mapOptions, validation, and hooks receivestepIdsandtargets, notsteportarget. OmitmapOptionswhen validated flags already satisfy same-name pipeline options; provide it when names, types, defaults, or derived values differ. The returned command exposes an immutabledescriptor; UI adapters should render that structured parameter contract instead of parsing help text. Usecommand.plan()ortubeless planfor a selection-only preview. Do not simulate planning with--plan. - Use
pipeline.toMermaid()orcommand.toMermaid()when documentation needs the static graph; do not duplicate dependency edges by hand. - Use
tubeless listfor the explicit project command inventory. Usetubeless inspect <registered-id>for a registered module inventory,tubeless planto preview selection without domain options or execution, andtubeless graphwhen generating documentation.inspect,plan, andgraphaccept a pipeline or a marked command and prefer the command when both are exported. Let the workbench discover the sole matching export or pass--export. - Use
tubeless run <registered-id> -- <command-args>for project commands, or the existing file form only for modules exporting adefinePipelineCommand. Keep application flags after--; the command must continue to own domain validation and option mapping. Pass--trace <path>for NDJSON traces (-writes NDJSON to stdout and moves command output to stderr) and--storefor SQLite; they compose. Usetubeless historyto list or show recorded runs from SQLite, or pass--traceto inspect a finished NDJSON artifact without importing it. Filter shared history with--pipeline <recorded-pipeline-id>in any output mode. This is the pipeline definition's ID, not its registered command ID.
Runtime rules
- Library entrypoints are ESM-only and require Node.js 22 or later. The
tubelessCLI requires Bun 1.3.14 or later; its#!/usr/bin/env nodetrampoline relaunches the binary with Bun under Node and reports an actionable install message when Bun is missing. - Use
context.log, never directconsolecalls inside steps. - Pass
context.signalinto network calls, batching, retry, rate limiting, and long waits. Usecontext.sleepfor retry-aware or testable delays. - Call
context.reportProgressfor long loops andcontext.reportAttemptfor retries that operators should see. - Resolve relative files from
context.cwd; prefer helpers fromtubeless/node. - Use
runOrThrowwhen every step must succeed and the caller expects a value. It always throws for an unsuccessful run, includingcontinueOnErrorruns. Userunwhen the caller must inspect failures, skips, timings, or best-effort output. Its versionedPipelineRunexposesrunId, terminal status and timestamps, errors, and timestamped step reports with correlated attempt IDs; structural skips have no attempt ID or start timestamp. Use hooks or tracing for streaming logs and progress. Exporter failures warn once per emitter and do not fail the run; passonExporterErrorto observe the first drop. Useplanwhen nothing may run. - Declare supported downstream goals with
targets: [step]ondefinePipeline, then select their literal IDs through run controls. Required inputs and failure gates are selected recursively. UsestepIdsonly when exact low-level filtering of any step is intentional; never combine the two. - Read
PipelinePlanStep.selectionReasonswhen explaining selection. It already includes originating targets and immediate dependents; do not reconstruct provenance by walking dependency arrays in application or CLI code. UserenderPipelinePlanfromtubeless/renderfor shared human or JSON output instead of maintaining another selection-reason formatter. UsecreatePipelineReporter/createRunReporterfromtubeless/reporterfor TTY presentation; do not import reporters from the root kernel. - Use focused hooks for ordinary observation:
onStepStart,onStepProgress,onStepComplete,onStepSkip,onStepCancel, andonStepFail. Their event metadata is already narrowed. Use additiveonStepStatusonly when one consumer genuinely needs the whole discriminated lifecycle, such as a status-aware renderer or event store. - Use
createPipelineTestRuntimefromtubeless/testingfor deterministic pipeline tests. Inspect its structured logs, statuses, and latest progress; keep test-framework matchers outside the package. - Branch on
PipelineError.code,phase, andkind, never message prose. Underlying thrown codes live insourceCode. Failed and cancelled reports expose the structured error undererror; skipped reports exposereason, optionalmessage, and optionaldependencyId.PipelineError.causeis a bounded JSON-safe snapshot; a thrownPipelineExecutionErrorretains the original value through nativeError.cause. UserenderPipelineErrorwhen a diagnostic crosses a human or JSON presentation boundary. - Inspect
error.fanOutin reports or recorded trace history for bounded keyed failures fromforEachPipeline. CheckomittedFailureCountandkeyTruncatedbefore selecting rerun inputs; unstarted items are not failures. Reruns remain caller-owned new runs. - Standard Schema failures use
kind: "validation", retain normalizedissues, and have boundary-specific codes for options, step outputs, and final results. Async schemas run duringrun; synchronousplan()previews graph and selection only and never invokes schemas. - Wrap normal finalizers in
requireOutputswhen a valid result requires specific step outputs. Use a plain finalizer only when partial output is a valid domain result. - Preserve the dependency-free runtime. Keep application telemetry SDKs at the
exporter boundary. Use
composeTraceExportersfromtubeless/tracingwhen one run must fan out to multiple destinations;onExporterErrorreports the first partial drop, the failed destination is retired, and healthy exporters keep receiving events. - Keep durable local observation opt-in. Use the append-only adapter from
tubeless/run-store/sqliteortubeless run --store; inspect recorded runs withtubeless history. Use the strictly read-only adapter fromtubeless/run-store/ndjson,tubeless history --trace, or the--traceoption totubeless uifor a finished portable trace. Treat trace files as sensitive: logs, errors, and attributes are displayed as recorded, and malformed or oversized artifacts are rejected.tubeless historyinspects a finished artifact and refuses a store with a live writer or multiple hard links. SQLiteexport()may return before the row is on disk. Other connections cannot see that tail until a batch of 64,flush(), same-instancelistEvents/clearHistory, orclose(). A crash can lose up to 63 unflushed events;tubeless run --storeflushes at completion so finished runs are durable. Do not make pipeline definitions depend on storage or the studio. Recorded history keeps the lastreportProgressdetailsplusdetail_count, and opaque child steps keepnested_pipelinewith the originalstep_count. Studio renders those snapshots; it does not flatten child DAGs into the parent step. Observed definitions pick the latestpipeline.startedbytimestampMs, then store-local id. - Use
createPipelineRunProjectorfromtubeless/run-storewhen a custom reader pageslistEvents({ afterId }). Append each newer page and callsnapshot(); a refresh with no new ids returns the cached view. Duplicate and out-of-order ids are ignored;0is a valid first id. Pass{ retainLogs: false }only when the snapshot should keeplogCountwithout log bodies. UseprojectPipelineRunStoreonly for a one-shot fold of a complete list. Seelocal-observability.ts. - Pass caller-owned
runIdandparentRunIdvalues throughPipelineContextwhen joining an external execution tree. Do not derive correlation from step IDs or timestamps. - Treat
tubeless uias a local projection with no execution capability by default. UsedefinePipelineProjectfor a checked-in command catalog with stable registered IDs, and register only explicitdefinePipelineCommandmodules; never make execution require the studio server or infer executable modules from observed history. Every studio request, including reads, must send aHostthat matches the bound authority, or, on a wildcard bind,localhostor a literal IP on the same port. Browser plan, launch, cancel, and clear-history also sendx-tubeless-studio-*headers; those are same-origin guards, not authentication. Studio HTTP errors contain stablecode,message,hint, and a backward-compatibleerroralias; the local-only contract is published athttps://tubeless.io/openapi.json. Cancel a live top-level launch from the running detail pane; that abort is process-local, leaves sibling launches running, and is not crash-resume.
Failure and safety rules
- Do not use
step.skippableto swallow an exception. - Do not treat dry run as rollback; external side effects require
dryRun: "skip"or a side-effect-free customdryRunhandler. - Do not publish after a failed or cancelled validation step. Make validation
required, or use
skipAfterFailureOfwhen its output is intentionally optional; the gate blocks both unsuccessful terminal statuses. - Do not hide absent required finalizer outputs behind defaults. Declare them
with
requireOutputs; handle optional outputs explicitly. - Keep step IDs stable because reports, hooks, traces, and CLI selection use them.
- Treat
nameas presentation only. Dependencies, outputs, traces, and CLI selection continue to use the stable step ID.
Required references
- Read core concepts for skip, failure, or selection changes.
- Read the CLI for list, inspect, plan, graph, run, history, and exit codes.
- Read the studio before changing
tubeless ui,definePipelineProject, ordefinePipelineStudiocompatibility. - Read child composition before changing child propagation, progress, or parent/child selection.
- Read the relevant executable example linked from the recipe index before writing new usage.
- Copy consumer layout, export names, and IDs from the project manifest.
- Use the generated API inventory only to verify exports; it is not implementation guidance.
Validation
For package changes:
make check
For a consumer-only change, run its focused tests and the repository typecheck.
If the public surface changes intentionally, regenerate the checked API artifacts
with bun run api:generate from the package root.