Command line programs
The second UDF type is a program you run rather than a function you import. The unit of work is an argv line, not a callable — which makes it the right shape for anything already written as a script, a binary, or a tool you did not author.
The first type, a simple function, travels as a reference the worker imports. A command line program travels as a string the worker executes. Everything else — the queue it lands in, the workers that claim it, the provider that supplied them — is identical.
Declaring the interface with params-proto
A bare script has no declared interface: the flags it accepts exist only inside its argument parser. params-proto turns the signature itself into the interface, so the flags, types, defaults and help text all come from one place.
Two things carry over into the CLI automatically. The docstring becomes the
program description, and each inline comment becomes that flag's help
text — so --help is generated from the same source the function is defined
by, and cannot drift from it:
Underscores in a parameter name become dashes on the command line, so
batch_size is --batch-size. Booleans are flags: overwrite: bool = False
gives --overwrite, and a parameter defaulting to True gives --no-<name>.
Invoking it
The queue is the same object a Python UDF submits to — same claim discipline, same workers, same elasticity. See Queue model.
You can pin to a specific worker instead of routing through a queue, but not
both; queue and worker_id are mutually exclusive.
Declaring access
A simple function declares what it may reach on the
decorator — source= and target=, patterns relative to the workdir. A command
line program has no decorator, so the declaration moves to the exec body,
alongside the command:
Same three fields, same meaning, same workdir-relative patterns — see
Declaring access. A repo-wide .dreamrc supplies defaults
for both UDF types, so a program that inherits everything it needs declares
nothing here.
The parameters above are named source and target so both UDF types read
the same way. Nothing in the runtime binds them.
That is deliberate. The declaration and the argument are different layers: the
declaration says which family of keys this program may ever touch, and the
argument says which one this run touches. A magic parameter would collapse
them — and would mean any program with a parameter incidentally called
target acquired behaviour it never asked for.
So name your primary input source and your primary output target because
it reads well and matches the spec. A program with three inputs should give
them descriptive names rather than contorting to fit one reserved word.
The exec body accepts queue, worker_id, command, env, workdir,
timeout_s, stdin_b64, and the payload fields. source, target and
mounts are not among them, and nothing enforces access on either UDF type
today.
The JSON representation
Two JSON objects matter here, and they are easy to confuse.
The parsed configuration
params-proto will hand you the resolved parameters as a plain dict, which is what you serialize when you want to record what a run was actually configured with:
Reach it with dict(encode) or encode._dict. Values are resolved by
precedence — CLI arguments beat direct assignment, which beats a bound context,
which beats environment variables, which beats the declared defaults — so the
dict reflects the run rather than the source.
The exec wire
What actually crosses to the control plane is the exec body. command is the
argv line as a single string:
| Field | Meaning |
|---|---|
queue | Queue name to route through. Required unless worker_id is given; the two are mutually exclusive. |
worker_id | Pin to one worker instead of routing. |
command | The command to run. Required, non-empty. |
env | String-valued environment overlay. |
workdir | Working directory on the worker. |
timeout_s | Wall-clock budget; exceeding it is a terminal timeout. |
stdin_b64 | Base64 stdin, if the program reads any. |
payloadInline / payloadRef | The input payload — inline base64 below the server's inline threshold, otherwise a { bucket, key, size } reference. Exactly one may be set. |
The response is a 202 carrying the handle:
Results come back by that id, the same way an invocation's do:
exit_code is the part with no analogue on the Python side — a command line
program reports failure by exiting non-zero rather than by raising.
Command line programs dispatch through POST /v1/namespaces/:ns/exec, not
through the invocation path a @udf submit takes. They share the queue and
the workers, but they are two wires: exec jobs do not appear as Invocation
rows, so they are absent from invocation listings and from anything built on
invocation lineage. Treating both as "UDF types" is the intended direction,
not a description of one unified code path.
Large inputs
payloadInline is capped server-side. Above that threshold, presign a key
through the payload store and send payloadRef instead — the key must begin
with payloads/, which is what the presign route mints. The rule is the same
one the Python side follows: bytes stay out of the wire and travel by
reference.