Simple functions
One decorator, four body kinds. The SDK inspects your function once at decoration time and the call convention follows the body you wrote. Everything on this page runs in-process, so you can paste any of it into a REPL.
Sync body
A plain def returns a plain value.
Async body
An async def returns a coroutine. Await it.
Generator body
A generator returns an iterator. Nothing runs until the consumer pulls, so a long sequence costs only what you actually consume.
Async-generator body
An async def with yield returns an async iterator. Pull it with async for.
Chaining
A UDF's output feeds the next one. With plain values, chain by awaiting in sequence.
With streaming bodies, chain by passing the iterator itself into the next stage. Each stage stays lazy, so the whole pipeline pulls one item at a time.
Fan-out over async bodies
Async UDF calls are ordinary awaitables, so asyncio.gather fans them out with
no extra machinery.
Scopes and key-based returns
dls.scope(prefix) sets the ambient prefix that dls.run.read and
dls.run.write resolve against. The body names prefix-relative string keys and
returns the keys it wrote — never the bytes.
Every call runs in its own forked context, and the contract is checked on the way out: every key you wrote must appear in what you return. Return a single string, a list or tuple of strings, or a dict whose values are the keys.
Scopes nest, so a parent prefix composes with a child one:
Generator bodies may yield partial manifests; they are concatenated and checked once the generator is exhausted.
Declaring what it may touch
A scope sets where keys resolve. It does not say what the body is allowed to reach. Declaring the readable source, the writable target, and S3 mounts on the decorator — or once for the whole repo — is its own page: Declaring access.
The registry payload
A simple function does not travel as source. It is registered — the control plane keeps its identity and signature, and the invocation carries only a reference to that record plus the arguments.
The inline comment on each parameter is its description — the same
convention a command line program uses to
generate --help. One rule covers both UDF types.
That decoration produces one Function row, keyed by
(namespaceId, module, qualname, version):
Three things about that record are worth knowing:
A Google- or NumPy-style Args: block is a second copy of the signature.
Rename a parameter and the docstring keeps the old name, silently — nothing
checks them against each other. An inline comment sits on the line it
describes, so it moves with the parameter or disappears with it.
Extraction costs nothing new: the SDK already captures source at
decoration time for the source column, so the comments are in hand
before anything is parsed. typing.Annotated[str, "…"] is the runtime-visible
alternative and stays available for cases that need it — it is just noisier
for the common one.
versionis the sha256 of the source, not a number you bump. Edit the body and you get a new row; the old one stays, so an invocation recorded last week still names the code that actually ran.sourceis captured at decoration time and is what the dashboard shows when you click into an invocation.signatureis where the prose lands.signature.docis__doc__, and each entry insignature.paramscarries its owndoctaken from the parameter's inline comment. That is what makes the registry self-describing — a reader asking "what isdest?" gets an answer without opening the source.
Comments are discarded by the interpreter, so this is a source parse. Where
source cannot be retrieved — a function defined in a REPL, built by exec,
or shipped by value because it was not importable — the parameters still
register with names, annotations and defaults, and simply carry no doc.
Missing prose is never an error, and it is the same condition that already
pushes transport from ref to pickle.
The envelope on the wire is much smaller — it names the function rather than carrying it:
kind: "ref" is the importable case — the worker imports module:qualname,
fetching and extracting the code snapshot only on
ImportError, cached by commit. kind: "pickle" is the fallback for lambdas,
closures, and functions defined in __main__, where there is nothing importable
to name.
The Function model above is real and the dashboard reads it, but the native
HTTP dispatch currently sends a fixed stub — {"module": "_native_wire", "qualname": "_native_wire.envelope", "version": "v1"} — rather than the
function's own identity. So a registry keyed by real module and qualname is
the design, not yet the behaviour on that path. Worker-side resolution already
works from module:qualname or an explicit registry={qualname: fn}.