Agent guide for tubeless
Use this guide when writing or modifying Tubeless pipelines, CLI scripts, or shared helpers. It summarizes the implementation rules; the linked guides and examples explain each feature.
For consumer projects, install the agent skill pack.
Use tubeless-make-pipeline to convert existing code: check caller inputs and results,
choose domain step boundaries, preserve types and failure behavior, and verify
dry runs with fake I/O. The general tubeless skill supports ongoing authoring.
Workflow
- Read the recipe index and open the smallest matching example.
- Group application pipelines with
defineProject(id, [pipelineA, pipelineB])fromtubeless/project; retrieve one withproject.get(id)and use its ordinaryplan,run,runOrThrow, ortoMermaidmethods. Use the project example as the minimal reference. Schema-backed pipelines are directly available to the CLI from project IDs or files, and to Studio from projects or registered command files. Automatic commands require Standard JSON Schema input metadata; schema-less pipelines need explicit commandparams, even with no inputs. Pass custom adapters directly in the project’s entry list for CLI inputs, mapping, or presentation. Use a default export to select the project for CLI and Studio. Without a default, exactly one distinct project may be exported. - Declare stable IDs and operational descriptions. Add pipeline
nameonly when CLI or Studio needs a friendlier display name; commands inherit pipeline presentation unless they explicitly override it. - 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, documentation, or examples.
Choose pipeline features
Start with steps and dependencies; the later sections are independent and can be read when a pipeline needs them.
- Steps and dependencies
- Concurrency
- Caching and artifacts
- Child pipelines and iteration
- Remote work and worker threads
- Run inside a host engine
- Deploy to Cloudflare
- Projects and typed wrappers
- Commands and the workbench CLI
- Declarative documents
- Graph metadata
- Agents
Steps and dependencies
- Use
createSteps<TDomainOptions>()once per pipeline and destructure every constructor that pipeline uses:step,fromPipeline,fromRemote,iteratePipeline, and/orforEachPipeline. UsedefinePipelineonce after declaring the 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. Omit options withrun()orrunOrThrow()when no input fields are required; each call supplies a fresh{}to schema validation. Passundefinedas the first argument when supplying controls without input. - 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. - Add
skipto a step definition only for an intentional successful outcome. A string or{ reason }result makes its outputT | undefined; handle that absence explicitly. If every skip branch returns{ reason, value }, the output remainsT. - For a static family sharing one implementation, adapt the recipe-local tuple
helper in parameterized steps. Keep IDs,
constants, and source step references explicit; expand synchronously before
definePipeline. Preserve literal IDs and the factory’s output type. The recipe uses one common output type, not an arbitrary heterogeneous callback. UseforEachPipelinefor runtime items. See static step families. - Treat a
PipelineDefinitionErrorduring module loading as an authoring bug; static graph mistakes are rejected whendefinePipelineis called.
Concurrency
- Opt in to parallel independent steps with
run(options, { maxConcurrency: 4 }).plan(controls)validates this and all other static controls before execution; it still does not invoke domain schemas. Pipeline commands expose the same control as--max-concurrency 4, including Studio forms. The default is1to preserve side-effect order. Required inputs, optional inputs, and failure gates all wait for terminal prerequisites. Skip predicates and output validation occupy slots; fail-fast stops dispatch and drains in-flight steps without cancelling them.continueOnErrorkeeps eligible branches running; external cancellation stops dispatch in either mode and cancels unstarted selected steps after active work settles. Final step reports and step errors follow plan order; live hooks and traces follow event order. Every concurrent failure is kept. Child runs have separate limits, supplied through the childcontrolsfield. Parent and fan-out limits multiply: four fan-out steps at eight children each allow 32 active children. Do not share a semaphore between waiting parents and their children. See the parallel DAG recipe.maxConcurrencyand fan-outconcurrencyare the same kind of bound (an admission cap, not a guaranteed level) on different axes — DAG steps at run time vs. one step’s children at authoring time. Both must be positive finite integers; an invalid fan-out bound is rejected when the step is created, or fails the step before any child starts when a callback supplies it. See the full mapping. - Use
runConcurrentfor bounded lightweight functions that do not need child lifecycle events. UserunConcurrentPartialwhen the caller needs completed results and the first failure instead of a throw. Both stop scheduling after failure or cancellation and drain active workers without cancelling siblings. Branch onok: success has a dense input-order result array; failure has sparse results and the original first rejection or observed abort error, which may beundefined. UsecompletedIndexesto distinguish successfulundefinedoutputs from holes. Invalid concurrency still throws as an authoring error. UserunBatchedorrunBatchedPartialfor the same throw-or-partial split over fixed-size batches instead of individual items.
Caching and artifacts
- Opt ordinary deterministic steps into caching with
cache: { version: "v1" }, orcache: trueto inherit the pipeline’simplementationVersion. Defaults hash dependency inputs and validated options and store results under the run cwd’s.cache/<pipeline-id>/<step-id>/. Set pipelinecache.maxAge: "30 days"with optional step overrides; numeric ages use milliseconds. Custom keys, stores, and codecs are overrides. Run{ cache: "recompute" }or{ cache: "bypass" }applies to child runs too; CLI/Studio supply the built-in--cachecontrol. Keep it out of domain options. Dry runs, policy skips, and test overrides bypass caching. Hits validate raw results throughoutputSchemaand carryoutputSource: "cache". Cache writes and validated hits automatically record write/reuse artifacts; history and Studio label these Cached output. Hits never replay application artifact records. The default codec rejects lossy round trips, including class instances; supply a reconstructing codec or return plain data. Artifact helpers reject cache options. Cache errors fail the step. See cache contracts and the cache recipe. - Record explicit I/O with
context.recordArtifact({ operation, artifact })inside ordinary steps; useread,write, orreuseaccording to what actually happened. Domain results stay separate from trace metadata. Report each successful batch after persistence; prior records survive later failure or cancellation. Keep checkpoint advancement and atomic promotion application-owned.loadArtifactandsaveArtifactfromcreateStepsare conveniences for one read or unconditional write. Both return{ value, artifact }and publish the typed value. Savers default todryRun: "skip"; custom handlers produce previews. Loaders that populate caches also need dry-run safety. Forward cancellation into adapters. Read artifact contracts and adapt artifact lineage. Keep manifests in artifacts, omit secrets from metadata, and preserve domain validation outside tracing.
Child pipelines and iteration
- Use
fromPipelinefor one independently useful child workflow andforEachPipelinefor runtime fan-out with stable keys and bounded concurrency. OmitfromPipeline.mapOptionswhen the parent’s validated domain options satisfy the child’s input type; those options are forwarded unchanged and validated by the child. Otherwise provide a mapper. Fan-out still requires per-item mapping. KeepmapOptionsexclusively about child domain inputs. Put childtargets,stepIds, concurrency, failure policy, and optional dry-run opt-in in the separatecontrolsvalue or callback. A child cannot disable its parent’s dry run. Addskiponly when the whole fan-out may be intentionally omitted. A skip without a value producesreadonly T[] | undefined; value-bearing skip paths preservereadonly T[]. AsyncfromPipelineresult mappings publish resolved values; use that resolved shape for dependent inputs and policy-skip values. Parent plans show one wrapper step and 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-out progress displays up to 32 live item groups by default and emit the full retained tree once at completion. Setprogress.detailLimitto override that live cap and cap the final snapshot. - Use
iteratePipelineto repeat a child with per-invocation state and a required positivemaxIterations. Return{ kind: "next", state }or{ kind: "finish", result }fromtransition; useIterationDecision<State, Result>for an explicit contract. Finishing at the bound succeeds; continuing at the bound fails without another child. Keep state read-only and initialize it fresh for each run. Child controls are static; parent dry-run and cancellation propagate. Plans show the bounded region, while traces identify each actual child run. Set the child pipeline’snamefor numbered progress labels such asPage 1; unnamed children useIteration 1. See pagination and iteration semantics.
Remote work and worker threads
- 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 include that flag in the request built bymapInput. Callpipeline.runOrThrowfrom a worker or activity handler and passcorrelationIdwhen an external system owns job delivery and retries. PassparentRunIdonly when linking to a known Tubeless execution. Parent plans exposeremotewithengineand optionaltarget. Adapters may forward remote lines throughcontext.logand must rethrow remote failures asErrorwithcause/code. Follow the native-fetch example 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. - For actual CPU parallelism, use
createWorkerThreadAdapterfromtubeless/nodethroughfromRemote; never serialize arbitrary handler closures. Supply an explicit module URL/export and cloneable payload, keepoutputSchemaon the parent step, and close the adapter when all callers are done.poolSizelimits threads shared by that adapter;maxConcurrencylimits admitted DAG steps. Worker cancellation allows cooperative cleanup then terminates unresponsive threads. Read the worker protocol and lifecycle and the executable recipe.
Run inside a host engine
- For Airflow 3, adapt the paired Airflow examples: use the public REST API at the application edge, keep submission out of dry runs, verify inputs before reattaching to a stable DAG run ID, and validate XCom results. Airflow owns retries of hosted pipelines; a cancelled HTTP wait does not cancel a remote DAG.
- For Temporal, follow the Activity example: invoke Tubeless inside an Activity, keep Workflow imports type-only for the Activity implementation, heartbeat during long waits, forward cancellation, and preserve Temporal’s cancellation failure when Tubeless wraps an abort. Activity retries replay the whole pipeline; heartbeat progress alone does not resume its steps.
- For Inngest, follow the durable step example: call
runOrThrowinsidestep.run, validate events before Tubeless can wrap aNonRetriableError, and return JSON-safe values. Failed attempts repeat the whole pipeline; completed durable steps reuse their saved result. Keep Inngest step tools outside Tubeless handlers. Host cancellation does not abort an already executing step. - For Dagster, follow the Pipes example: keep the pipeline free of host imports, report materializations only after successful publication, require explicit Pipes materializations, and preview locally. Forward termination to Tubeless, correlate retries by host run and asset, and retain raw trace events for Studio. Dagster retries the entire asset invocation.
- For AWS Step Functions, follow the Lambda example: invoke Tubeless within a synchronous Lambda Task, validate the incoming job, correlate by execution and state, and let thrown failures reach Step Functions. Clear deadline timers after each invocation. Host retries repeat the whole pipeline; stopping a workflow does not cancel an already-running Lambda.
Deploy to Cloudflare
- To host Studio and a project on Cloudflare, run
tubeless init cloudflareand follow the deploy guide; do not hand-write the Worker or its configuration. The scaffold never overwrites files and loads the project first. Keep itsvite.config.tsand install the exactcf,viteand@cloudflare/vite-pluginversions it prints;cf deploybuilds through that plugin. Keep the fixed binding namesTUBELESS_STUDIO,TUBELESS_RUN,TUBELESS_BUCKET,ACCESS_TEAM_DOMAINandACCESS_AUD. - Keep handlers Workers-compatible: no durable filesystem writes, no
v8serialization. Read bindings throughimport { env } from "cloudflare:workers". - Give every cached step an explicit store and codec from
tubeless/cloudflare/cache, never fromtubeless/cloudflare, which loads only in workerd while step definitions also load in Node. Follow the guide’s pattern: a small module passes a bucket getter tocreateR2CacheStore, exportsbindCacheBucketandr2Cache, andcloudflare/worker.tscallsbindCacheBucket(env.TUBELESS_BUCKET). Steps usecache: { version, ...r2Cache }. Those steps fail in local Node runs unless another bucket or store is supplied. The default file store and V8 codec fail on Workers, andjsonCacheCodecaccepts only plain JSON data. - Respect the run limit: one run executes in one Durable Object alarm and is aborted at
14 minutes (
limits.maxRunDurationMscannot raise it). Keep single steps short, put long ones behindfromRemote, and use a host engine for multi-hour workflows. - Never put secrets in
cloudflare.config.tstext bindings. Every user Cloudflare Access admits sees all history and every command, so deploy a catalog meant for that audience.
Projects and typed wrappers
- Use
defineProjectfromtubeless/projectto collect typed pipelines. Pipeline IDs stay literal, duplicate IDs fail during project definition, andget(id)returns the exact pipeline type. Union or widened IDs retain all matching candidate pipeline types. Give the project itself a stable ID; it remains literal onproject.id. The project adds no execution layer. LetdefinePipelineinfer its type arguments. Annotate the finalizer’s return type for an explicit result contract; partial explicit type arguments default the pipeline ID tostringand lose literal-ID lookup checks. Pass optional{ name, description }as the third argument. For documents, reusecompiled.metadata?.nameandcompiled.metadata?.description, or spreadcompiled.metadatabefore explicit overrides. Metadata and its authors are immutable snapshots; authors/date stay descriptive. Project ID and cwd belong to registration. The immutableproject.namedefaults to its ID.ProjectOptionsnames the configuration type. Register each pipeline exactly once, directly or through adefinePipelineCommandentry:[pipelineA, commandB]. Duplicate pipeline IDs fail regardless of entry form; children are not registered recursively.pipelines,pipelineIds, andcommandsare derived read-only views.cwdcontrols CLI/Studio execution relative to the project file. For compiled documents, build adapters fromcompiled.get(id)and register them in the list. Pipeline IDs remain the single selection identity. ImportPipelineProject<TProjectId, TPipelines>fromtubeless/projectwhen annotating a shared project factory or a function that accepts a project. - Use
PipelineInput<typeof pipeline>andPipelineResult<typeof pipeline>when typing wrappers around an inferred pipeline.PipelineInputis the value accepted byrunbefore options-schema transformation. A command created withdefinePipelineCommandexposes that same exact inferred type ascommand.pipeline.
Commands and the workbench CLI
- Use
definePipelineCommand(pipeline)fromtubeless/clifor scripts centered on a pipeline. Flags are inferred from the Standard JSON Schema input metadata of the schema passed tocreateSteps(schema); see automatic CLI. Useoverridesonly for aliases, flag names, descriptions or environment fallbacks. Type-only or validation-only schemas need explicitparams; this replaces inference. Nested/union inputs, array defaults, non-stringenum/constconstraints, and$refsiblings with validation constraints need explicit parameters and, when shapes differ,mapOptions. Keep these advanced escape hatches out of ordinary wrappers. 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.--resumeis exposed only whencheckpointis configured or the command explicitly setsresume: trueand handlesvalues.resumeitself. Otherwise the CLI rejects the flag and the descriptor omits the Studio control. The returned command exposes an immutabledescriptor; UI adapters should render those parameter definitions instead of parsing help text. Pass finite numbers and number arrays directly toparseValues; numeric strings are rejected, while integer and min/max constraints apply to typed values too. Commandnameanddescriptiondefault to their pipeline values; the name falls back to the pipeline ID. Supply command-level values only for CLI-specific wording. Usecommand.plan()ortubeless planfor a selection-only preview. Do not simulate planning with--plan. - Use
defineCommandfromtubeless/clifor standalone scripts. AdefineProjectexport is the normal shared CLI and Studio inventory. Pass explicit adapters in the project’s entry list when pipelines need custom parameters, option mapping, or presentation. Each adapter uses its pipeline’s ID. Keep application-specific prompts in the consumer. - Use
pipeline.toMermaid()orcommand.toMermaid()when documentation needs the static graph; do not duplicate dependency edges by hand. - Use
tubeless listfor the project pipeline inventory. Usetubeless inspect <pipeline-id>for a project pipeline 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 <pipeline-id> -- <command-args>for project pipelines, ortubeless run <pipeline-file> -- <command-args>for a uniquely selected schema-backed pipeline. A markeddefinePipelineCommandexport wins when present; schema-less or unsupported inputs require one explicitly. 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. Use the pipeline definition’s ID. Agent run details include typed turn and call history, nested agents, state versions, errors, and termination;--jsonexposesagentHistory.agents. The pipeline filter selects the root while its detail includes descendants with other pipeline IDs. Raw--eventsremains run-scoped. Treat missing observations as unknown, not successful execution. Scripts should pass full run IDs; unique prefixes are a terminal convenience and become ambiguous as history grows. - CLI errors append
Did you mean …?for near-miss commands, flags, choices, pipeline IDs, targets, steps, and run IDs. Apply the hint rather than guessing, and usetubeless listor--helpwhen no hint is shown.
Declarative documents
- For YAML or JSON authoring, read declarative pipelines
and adapt the YAML recipe. Parse at the
application edge, then use
compilePipelineDocument(document, registry)fromtubeless/projectwith explicitly registered handlers, adapters, predicates, and schemas. A step declares exactly one ofrun,fromPipeline, orforEachPipeline; child pipeline IDs resolve within the document, while application code owns option, item, and result mapping through the matching adapter registry. Select a pipeline withcompiled.get(id), or passcompiled.pipelinestodefineProject. Register only the pipelines you want to expose; compiled children need no separate project entry. Export the project for CLI and Studio: Standard JSON Schema input metadata enables automatic commands; custom or schema-less inputs need explicit adapters in the project’s entry list. Unknown fields and references fail compilation; plans still do not validate domain inputs. Dynamic wiring does not infer TypeScript output types, so validate or narrow unknown values in handlers. Use YAML Peloton for a larger example with progress, retrying concurrent handlers, dry-run policies, and failure gates. Its per-rider progress is ordinary step detail by choice; the smaller YAML recipe demonstrates child-pipeline fan-out. Usetubeless validate --json <document.yaml>for a handler-free structure check, and the document JSON Schema for editor or agent validation. Optional document metadata is descriptive only.inspectorplanon a compiled command still checks graph semantics. - For user choices, boolean confirmation, or open text in a declarative pipeline,
register
waitForInput(...).runas an ordinary handler. Declare dependencies, output schemas, anddryRun: skipin the document; helper configuration does not transfer with the callback. The application supplies question rendering and normalization. Use a skip predicate for a declined confirmation. See input questions and the YAML input recipe.
Graph metadata
- Add inert
metadata(tags,owner,domain,annotations) for discovery. UsequerySteps(plan, query),inspect --tag/--owner/--domain, ortoMermaid({ query })to find steps; queries never select execution. Metadata is bounded, copied, frozen, and isolated across child pipelines. Definitions carrying metadata use identity version 3. Document v1 rejects step/pipeline metadata. Read graph metadata and adapt the recipe.
Agents
-
Use
waitForInputfromcreateStepsfor a pluggable user/application input boundary in any pipeline. It uses ordinary lifecycle and output validation, remains pending until input arrives, honors cancellation, and skips dry runs unless given a preview callback. Input steps reject caching and policy skips; gate dependent actions with ordinary steps. See input steps. -
Use
tubeless agent --model <name>for a workspace prompt loop using the same pipeline reporter astubeless run, including nested turn/tool progress, shimmer, colors, timing, and logs. Tool activity includes paths, command previews, and completion summaries in both the DAG and scoped logs. Direct and code-mode calls use the same activity settlement, including failures and cancellations. Custom handlers usecontext.reportProgressandcontext.logfor their own activity detail. Completed prompts share conversation state; each prompt has fresh budgets./modelchanges the next run’s model while retaining context;/clearstarts a fresh conversation.--promptruns once, and--no-graphhides pipeline reporting. The interactive bike-inspired prompt supports Tab completion of/model,/clear,/help,/quit, and/exit; double Tab lists matching commands.--model-moduleloads a trusted factory returning the publicAgentModelcallback. ExportOPENAI_API_KEYfor the built-in provider or select a dotenv file with--env-file; existing process environment values take precedence. Missing-key checks are local; OpenAI authenticates model requests. HTTP 401 diagnostics expose known rejection codes and safe recovery advice without raw provider messages. Restart the CLI after replacing credentials. Factories receive{ model, signal, env }with the resolved environment; asynchronous setup must honor cancellation and release its resources. Adapters append the new task oncontext.turn === 1and return the full final conversation. Failed/cancelled prompts retain the previous completed context. OrdinarydefineAgentpipeline invocations remain isolated per run; wrap one increateAgentSession(agent)to continue a conversation across prompts in your own app (session.conversationpersists,clear()resets). Sessions use the model, tool grants and limits captured bydefineAgent; later configuration mutations do not change them. See chat sessions. See agent CLI usage and the provider plugin example. -
For a typed agent inside a pipeline, use
defineAgent({ id, model, inputSchema, task, resultSchema }):taskmaps validated options (for example an extract step’s output) to the model’s task text, and the model finishes againstresultSchema. With the bundled adapters, a schema outside the provider’s strict subset fails at definition through the model’s optionalcheckDescriptorshook. Compose it withfromPipelinelike any child pipeline; adapt the extract recipe. Typed agents default to a task-neutral prompt without AGENTS.md (projectContext: false), soinstructionsis their main guidance; opt in withprojectContext: true.defineAgentLoopis for customdecidecallbacks and state. Grant autonomous agents only the standard tools they need withdefaultTools(for example["read", "list", "search"]); every default, includingbash, is granted otherwise. -
For a general workspace agent, use
defineAgent({ id, model })fromtubeless/agentwithopenaiModel()fromtubeless/agent/openaioranthropicModel()fromtubeless/agent/anthropic. Both adapters share option names; setbaseUrl(orOPENAI_BASE_URL/ANTHROPIC_BASE_URL) for compatible servers such as OpenRouter or a gateway, andcompactAfterBytes: nullwhen the server lacks compaction. See providers and compatible APIs. It supplies task/answer schemas, a customizable coding prompt, startup AGENTS.md discovery from the physical workspace directory, shared by the model and all tool calls, and per-run conversation ownership. The optional provider adapter retains native history and compacts it before subsequent decisions. The prompt requires project-guidance reads in a separate turn before file operations or task commands in their scope; filename discovery may precede them, while optional exploration follows them. Addinstructions, customtools, or tighterlimitsas needed; the OpenAI adapter requires inline tool descriptors with strict object constraints, validated before HTTP. Large batches get explicitly marked model-visible previews; the harness retains full outcomes. Read the model-agent contract and adapt the minimal recipe. The OpenAI adapter omits reasoning settings by default; the coding recipe and live evaluations explicitly requestreasoningEffort: "high", which requires a model that supports that setting. Usebun run eval:agentfor opt-in paid workspace evaluations; routine CI stays credential-free. -
To give an agent MCP server tools,
await connectMcpServers({ name: { url } | { command } })fromtubeless/agent/mcpand passtools: mcp.tools(named<server>__<tool>). Connect inside the function that runs the agent, not at module scope, and close the servers afterwards (await using). Inventories are fixed at connection. Inputs support fixed-property objects, homogeneous arrays andanyOf; unsupported structural schemas fail at connection rather than during an agent’s run. Required names must have property definitions; optional nullable fields are rejected because a strict null placeholder would erase their explicit nulls. Schema expansion is bounded, and conflictinganyOfinterpretations fail validation before dispatch rather than silently dropping values. Read MCP servers and adapt the MCP recipe. -
Code mode is off by default; enable it only when scripts can do work the model need not see: loops and aggregation, chains through tools with typed outputs, or large tool inventories. Live evaluations found no gain for small coding tasks or short lookups; see when to use code mode. Set
codemode: trueondefineAgentordefineAgentLoop(or runtubeless agent --codemode); it needs at least one tool. It is a setup-level permission;AgentCall/AgentDecision/AgentOutcomethen taketrueas their code-mode parameter and name onlycodemode. The model sees onecodemodetool whose description declares every registered tool astools.<name>(input): Promise<output>; its JavaScript script calls them and returns a JSON result with console logs. Script calls are ordinary budgeted child runs of that call. Recoverable tool errors throw inside the script; fatal handler errors fail the run. Dry runs execute scripts with each tool’s dry-run policy. Sandboxing is best-effort; the default executor (worker thread plusnode:vm) needs Node or Bun; on Cloudflare passcodemode: { executor: createWorkerLoaderCodemodeExecutor(env.LOADER) }fromtubeless/cloudflare, or supply your ownCodemodeExecutor. Read code mode and adapt the code-mode recipe. -
Use
defineAgentLoop,defineTool, andpipelineToolfromtubeless/agentfor an in-process decision loop over registered handlers, pipelines, and child agents. Read agents and adapt the credential-free scripted recipe or the OpenAI recipe for a real provider callback. Keep model requests and prompts indecide; validate every tool input/output and the final result. Returncontinuewith a nonempty batch orfinishwith a raw result. Every agent includesread,write,edit,bash,list, andsearch.toolsadds capabilities or replaces defaults by name; narrow reducer outcomes byokandtool. Paths resolve from the run cwd. Follow the workspace recipe for real file edits and shell checks. Writes and edits stage complete files before atomic replacement, requiring a writable destination directory and permission to preserve the original owner/group. These permission failures leave the original intact; there is no in-place fallback. Writes follow symlinks even when the target must be created. Bash bounds pipe draining during cancellation; escaped descendants may outlive the tool. List and search order entries by filename before applying caps. Directory searches retain matches when descendants become unavailable and count skipped entries. State is copied and frozen per initialization/reduction. Register children withpipelineTool; reuse their options schema or supplyinputSchemaandmapOptionstogether. Follow the delegation recipe. -
Calls, decisions, depth, and leaf concurrency obey every ancestor limit; turns are local. Child options are prevalidated once for the whole batch. Add
durability: { store, key }and an explicitimplementationVersionfor checkpointed decisions, outcomes, child state and budgets. Live children that inherit recovery also require an explicitimplementationVersion; bump it when their semantics change. Reuse the key and inputs to resume; use a new key for changed semantics. See durable execution and the SQLite recipe. Running unsafe calls become recoverable interruption outcomes; mark only repeatable or idempotent toolsreplay: "safe". -
Supply an
AgentEnvironmentfor remote guidance, files and commands; children inherit it. The local adapter’snode:localauthority id is host-independent, so a recreated container or pod resumes; passidto name another authority. Custom tools receivecontext.environment. Environment operations preserve signal cancellation across SDK-specific errors and late results, while still draining active work. Guidance paths are nonblank and limited to 4,096 characters; guidance is checked against the 32 KiB UTF-8 prompt budget before each section is retained. Remote tools obey the same UTF-8 byte limits as local tools. Oversized write/edit arguments fail before batch dispatch; oversized read, bash, listing and search results fail before state commits. See the environment contract. Use the environment recipe and the optional local and SQLite adapters fromtubeless/agent/node. Dry runs skip decisions and handler tools unless both provide preview handlers. Agents remain ordinary pipelines for projects and CLI use. -
Use
fromPipelineto feed a validated answer into ordinary dependent work; the composition recipe includes credential-free previews. Resolve provider credentials only during execution, pass cancellation to the HTTP request, and keep provider response parsing in application code. The OpenAI recipe has no live dry-run callback; use the scripted recipe for previews. Bound questions, accumulated observations, and encoded provider requests before sending them. Reject oversized text without silently changing tool results.
Runtime rules
- Library entrypoints are ESM-only and require Node.js 22.6 or later. The
tubelessCLI uses a#!/usr/bin/env bunentrypoint and requires Bun 1.3.14 or later onPATH. - Use
context.log, never directconsolecalls inside steps. Wide interactive terminals show recent logs beside progress automatically (120 columns by 8 rows minimum). Set commandreporter.logPaneto"off"to disable the pane; logs appear only in the pane while it is visible. Use--traceor--storefor complete recordings. Forward subprocess output throughcontext.logwhen it should appear there. See the live TUI recipe. - 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.cwdwithnode:path. - Import optional Node helpers from
tubeless/node:definePathsresolves named paths against each run’scontext.cwd;writeJsoncreates parents and atomically replaces JSON files, rejecting unserializable top-level values before touching disk;readJson<T>parses trusted JSON without schema validation;resetDirdeletes and recreates generated output directories;requireEnvchecks required environment values when called. Mark writes and resets withdryRun: "skip"; these helpers do not inspect pipeline controls themselves. - 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. Branch onrun.finalizedbefore consumingrun.value; the true branch narrows the value to the exact pipeline result type, including when that type containsundefined. Finalization is independent of success, so a failed best-effort run can still carry a finalized value. Its versionedPipelineRunexposes its uniquerunId, optional reusablecorrelationId, 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 explicitly select their literal IDs through run controls. Without selection controls, run every step; no target is selected automatically. Omitted definitiontargetsexposes the last step in execution order as a selectable goal;targets: []exposes none. An explicit run target selects its required inputs and failure gates recursively. UsestepIdsonly when exact low-level filtering of any step is intentional.PipelineRunControlsrejects both selections together at compile time; dynamic inputs retain the runtime planning check. Omitted orundefinedselectors remain valid. - Read
PipelinePlanStep.selectionReasonswhen explaining selection. It already includes originating targets and immediate dependents; do not reconstruct selection reasons by walking dependency arrays in application code. The workbench owns terminal plan formatting and reporting. - 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. Hook metadata is copied separately for each callback; mutations cannot change execution, tracing, or other callbacks. Finalized domain values retain their identity and must be treated as read-only by hooks. - Use
createPipelineTestRuntimefromtubeless/testingfor deterministic pipeline tests. Inspect its structured logs, statuses, and latest progress; keep test-framework matchers outside the package. Supply typed intermediate values withoverrideStep(step, value)and test-run{ overrides: [...] }controls. Values use the handler output type beforeoutputSchemavalidation/transformation; successful substitution reportscompletedand emitsonStepCompletewithoutputSource: "override". This provenance also survives failed or cancelled validation attempts. Overrides apply only to selected steps, bypassing their handlers, skip predicates, dependency gates, and dry-run policies. Use exactstepIdsto exclude upstream I/O; overrides do not prune the graph or propagate into children. Ordinary pipeline, CLI, and Studio launch controls do not accept overrides. See the override recipe. - 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. - Inspect
error.fanOutin reports or recorded trace history for bounded keyed failures fromforEachPipeline. CheckomittedFailureCountandkeyTruncatedbefore selecting rerun inputs; unstarted items are not failures. The caller starts a new run for any retry. - Standard Schema failures use
kind: "validation", retain normalizedissues, and have separate codes for options, step outputs, and final results. Async schemas run duringrun; synchronousplan()previews graph and selection only and never invokes schemas. - A single-goal pipeline can be
{ id, steps }. Omitfinalizeto return the output of the last step in execution order, orundefinedif absent. Defaults use the full graph’s topological order and do not limit an unfiltered run.resultSchemastill validates the value, including absent output. See the minimal recipe. - Use
finalize: stepto require and return a declared step’s exact output type. It preserves output-schema transformations and validundefinedvalues; missing outputs fail finalization. It does not change targets or selection. AddresultSchemato validate or transform that output. See the precise result recipe. - Wrap explicit 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. Implement concrete JSON or telemetry SDK
adapters at the application edge against
PipelineTraceExporter; see the tracing recipe. UsecomposeTraceExportersfromtubeless/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
tubeless run --storefor SQLite history andtubeless run --tracefor portable NDJSON traces. Inspect them withtubeless historyortubeless ui. An older SQLite store is not upgraded on open. Runtubeless upgrade --yes --store <path>before an unattended job writes to it. Treat trace files as sensitive: logs, errors, and event payloads 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. A crash can lose buffered events from a live writer;tubeless run --storeflushes at completion so finished runs are durable. Do not make pipeline definitions depend on storage or the studio. Version 3 trace events are a discriminated union keyed byname; use their typedpayloadrather than parsing scalar attributes. Recorded history keeps the lastreportProgressdetailsplusdetailCount, and child wrapper steps keepnestedPipelinewith the originalstepCount. Studio renders those snapshots; it does not flatten child DAGs into the parent step. Compiled pipelines expose immutabledefinitionmetadata. SetimplementationVersionondefinePipelineto a release/source/build identity for handlers, schemas, mappings, and finalizers; a structural fingerprint alone never proves code equality.pipeline.startedrecords carry a definition identity and a bounded snapshot. Ordinary definitions retain identity version 1; iteration and its ancestors use version 2, including the bound and static child controls in their fingerprints. Repeated child runs carry aniterationrelation to the owning run, step, attempt, and one-based index; each child still has a fresh execution ID. Children started by pipeline steps or agent tools recordpayload.parentStepIdonpipeline.started; runs linked by a hand-passedparentRunIddo not. Studio retains versions, groups their runs, and compares complete snapshots. Legacy recordings have unknown identity; oversized snapshots retain identity but cannot be compared. Storage rejects complete snapshots whose contents, including recorded child implementation identities, do not match their hashes. Definitions live as long as their run events. See definition history. Legacy observed definitions pick the latestpipeline.startedbytimestampMs, then store-local id. Storage readers, projections, and Studio embedding are workbench internals rather than application extension points. Readers accept saved version 2 recordings as well as new version 3 events; concrete exporter entrypoints are not part of that durability contract. - Pass caller-owned
correlationIdthroughPipelineContextwhen joining an external job or workflow.runIdis package-generated for every execution; pass a known executionrunIdasparentRunIdonly to link that parent. - Use
tubeless uito inspect local recordings and launch pipelines from a checked-indefineProject. Supply explicitdefinePipelineCommandadapters in its entry list; never infer executable modules from observed history. Keep the default loopback binding for local use. For deployed Studio, follow the gateway hosting contract: use--public-urlwith a server-onlyTUBELESS_STUDIO_GATEWAY_TOKEN, preserve the mount, and require application session/admin checks on every forwarded request. Studio owns gateway authentication and process-local execution; the application owns identity, the explicit deployed catalog, private transport, supervision, and persistent SQLite. Never retry uncertain launch requests. The internal JSON protocol is version-coupled and is not a public application API. History pages contain summaries; selected-run and definition details load separately. Definition run pages refresh independently of metadata and comparisons. Search includes descendants across the complete history, and clearing succeeds once deletion commits even if subsequent compaction fails. 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.
Cloud command use
Use tubeless auth login, then cloud list to select an already-loaded pipeline
by exact name or listed slug. Use cloud run --id <id> when selectors collide.
A single session workspace is automatic; multiple workspaces require
--workspace <id>. Load and synchronize pipelines in the dashboard.
cloud run executes only the existing deployment and never reads Git, project
configuration or local modules. Pass JSON object input via --input-file, use
--json for automation and --detach for admission only. Preserve an uncertain
admission key rather than launching a fresh run. Ctrl-C stops following;
cancellation remains explicit in the dashboard.
Read the Cloud CLI contract for credentials, roles,
workspace selection, retention and exit codes.
Failure and safety rules
- Do not use
skipto 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 uior project command adapters. - 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.
- Adapt advanced custom command adapters from the project with custom adapters.
- 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 public declarations change intentionally, regenerate the checked API artifacts
with bun run api:generate from the package root.