DreamLake

Importing from the Registry

Someone declared a UDF. You want to use it. This page is the read direction — discover, pin, pull the source, run it — and, just as importantly, the four places where that path stops short of what you would expect.

The write direction is The Registry.

The import primitive is three GETs

That is the whole of it. There is no import endpoint, no bundle format, and nothing to install.

1 · Discover, or resolve by name.

discoverbash
GET /namespaces/{ns}/runnables?kind=udf
GET /namespaces/{ns}/runnables/{module}.{qualname}

The list is paginated and filterable by ?kind= and ?module=. Either way you get the head, which carries latestVersion — a sha256 hex string, or null if nobody has ever pushed a version.

2 · Read the history.

historybash
GET /namespaces/{ns}/runnables/{name}/versions

Newest first, paginated, and without source — the list projection drops it deliberately, because 200 rows of source is megabytes.

3 · Pull one version, with its source.

one versionbash
GET /namespaces/{ns}/runnables/{name}/versions/{sha256}

This is the only route in the entire collection that ever serves source. It also carries signature — { params, returns, doc } — which is what tells you how to call the thing.

Three ways to do it

With curl

import with curlbash
NS=my-namespace
NAME=examples.embed.embed_frames
API=https://api.dreamlake.ai
AUTH="Authorization: Bearer $DREAMLAKE_API_KEY"

# 1 — resolve the head and read the pointer.
VERSION=$(curl -sS -H "$AUTH" \
  "$API/namespaces/$NS/runnables/$NAME" | jq -r .latestVersion)
echo "pinning $NAME at $VERSION"

# 2 — pull that exact revision and write the source out.
curl -sS -H "$AUTH" \
  "$API/namespaces/$NS/runnables/$NAME/versions/$VERSION" \
  | jq -r .source > embed_frames.py

Two round trips, and there is no way to make it one. See no latest, no alias, no prefix below.

With the CLI

import with the CLIbash
# What is out there?
dreamlake runnable list --kind udf

# The head, including latestVersion.
dreamlake runnable show examples.embed.embed_frames

# The history, newest first.
dreamlake runnable version list examples.embed.embed_frames

# Import. Without --version this resolves latestVersion first, then pulls it.
dreamlake runnable import examples.embed.embed_frames --out ./imported

# Import a specific revision. This is what you commit.
dreamlake runnable import examples.embed.embed_frames \
  --version 9f2c1b… --out ./imported

runnable import is a composition of the GETs above, not a new capability. It writes the source, signature.json, and a PINNED.json recording { remote, namespace, name, version } — because a pin that lives only in shell history is not a pin.

With GraphQL

read the registrygraphql
query Imports($ns: String!) {
  runnables(namespaceSlug: $ns, kind: "udf") {
    name
    description
    latestVersion
  }
  runnableVersions(namespaceSlug: $ns, id: "examples.embed.embed_frames") {
    version
    signature
    createdAt
  }
}
The GraphQL declarations module is read-only, and omits `source`

Every write goes through REST, which owns the validation — the channel-secret rejection, the host-key derivation, the 409 on a duplicate. GraphQL here is a dashboard read surface and carries no mutations at all.

RunnableVersion.source is deliberately absent from the GraphQL type: "Full source is megabytes and GraphQL here has no per-field cost control", so a naive runnableVersions { source } over a busy namespace would be an unbounded response. Use the REST version-detail route for source.

One more difference, and it is intentional: GraphQL reports NOT_FOUND both for "no such namespace" and for "you cannot read it", so existence never leaks. REST 404s the first and 403s the second.

What is missing

This is the useful part of the page. Four gaps, all real, all things a reader will otherwise discover the hard way.

No latest, no alias, no tag, no prefix

The version path parameter takes the full 64-character lowercase hex and nothing else. There is no ?version=latest, no named alias, no tag, no semver range, and no short-hash or prefix match. 9f2c1b is a 404; only the whole digest resolves.

The one symbolic handle that exists is the latestVersion column on the head, which you must read and echo back. So:

Pinning is: read latestVersion once, write the hex down, and pass it forever. Import-by-name-alone is two round trips, not one, and every "latest" in your tooling is a value you resolved at some point in the past and are now responsible for.

That is not as bad as it sounds — an immutable content hash is exactly what you want in a lockfile — but it does mean the registry cannot answer "give me the current one" in a single call, and any tool that pretends otherwise is caching the answer somewhere.

No join

No route returns a Runnable together with its RunConfig. There is no ?include=, no ?expand=, and no embedded object anywhere in the responses. A RunConfig is referenced by name from elsewhere, on purpose — the control plane resolves late, and Invocation.runConfigRef is a name for the same reason.

An importer that wants "the UDF and the environment it runs in" makes two or three calls and correlates by hand:

correlating by handbash
NS=my-namespace
NAME=examples.embed.embed_frames

# 1 — the head, for latestVersion.
HEAD=$(curl -sS -H "$AUTH" "$API/namespaces/$NS/runnables/$NAME")
VERSION=$(echo "$HEAD" | jq -r .latestVersion)

# 2 — the version, for source + signature.
curl -sS -H "$AUTH" \
  "$API/namespaces/$NS/runnables/$NAME/versions/$VERSION" > version.json

# 3 — the environment. The link is a NAME you must already know, or one the
#     publisher stashed in metadata by convention. Nothing in the API joins
#     these two rows for you.
CONFIG=$(echo "$HEAD" | jq -r '.metadata.run_config // empty')
[ -n "$CONFIG" ] && curl -sS -H "$AUTH" \
  "$API/namespaces/$NS/run-configs/$CONFIG" > run-config.json

Step 3 is a convention, not an API guarantee. There is no column on Runnable that points at a RunConfig.

DreamLake does not run anything

declarations.ts opens by saying so, and closes by repeating it:

Storage + CRUD only … Nothing here schedules, claims, launches or connects to anything.

There is no POST /run, no /invoke, no queue and no worker in the declaration routes. Execution reaches the control plane through exactly one door, and it is an opaque proxy:

the one execution door
ALL /namespaces/:slug/lakeshores/:id/proxy/*

It forwards the wildcard path verbatim to a registered Lakeshore's base URL with a decrypted stored bearer token injected, requires lakeshore:read for GET and lakeshore:use for anything mutating, answers 502 when the upstream is unreachable, and streams SSE straight back. It is deliberately a dumb single-target forwarder so it cannot be pointed anywhere but that Lakeshore's control plane.

The proxy is not reachable without a control plane

The proxy requires a registered Lakeshore row holding an encrypted control plane token. If you are evaluating DreamLake with no cloud account and no running control plane, that door is closed to you and no amount of registry access opens it.

So the honest "…and run it" story for a reader with no infrastructure is: import the source, then run it locally, through the SDK, against an in-memory dispatch.

run the imported source locallypython
import dreamlake.lakeshore as dls
from dreamlake.lakeshore.dispatch import Dispatch

# The file you just imported. Nothing about it is import-specific — a version
# row holds the source exactly as it was captured at decoration time, decorator
# included, so it is an ordinary module.
from imported.embed_frames import embed_frames  # @dls.udf(queue="gpu")

# No control plane, no queue service, no credentials — the whole plane is an
# in-memory SQLite database and the worker runs in this process.
plane = Dispatch(":memory:")
q = dls.SyncQueue("gpu", dispatch=plane)

inv = embed_frames.submit("clip-0001", _dispatch=plane)   # a durable string id
dls.run_worker(q, once=True)                              # claim + execute here
print(q.result(inv, timeout=5))

The queue name has to match the one the imported UDF declared — routing is caller-side and baked into the decorator. If the UDF you imported has no queue=, it is local by default and a plain call runs it directly, with no dispatch and no worker at all.

The starter kit's 05_import_from_registry directory does exactly this end to end: resolve, pin, pull, write PINNED.json, and run the pulled source locally.

The registry cannot say what shape a body is

Runnable.kind is a lifetime discriminator — udf, session, agent — and nothing more. There is no field on Runnable and no field on RunnableVersion that records whether the body is sync, async, a generator or an async generator.

That matters because the four shapes are not interchangeable at the call site. A generator's results arrive as a stream of frames; a sync UDF's arrive as one value. An importer that guesses wrong writes the wrong caller. Today the only ways to find out are to read signature, if the publisher put something useful there, or to read the source.

Proposal — carry the form in `signature`

RunnableVersion.signature is free JSON whose shape is explicitly owned by the SDK, not by the server. So a key like:

json
{ "params": [...], "returns": "...", "doc": "...", "form": "async-generator" }

would let the registry answer "is this a streaming UDF?" with no schema change, no migration, and no new column — the cheapest honest fix available. The starter kit already stamps signature.form as a convention and labels it as one.

The alternative — a real nullable column on RunnableVersion, which would let the dashboard badge it and let a list query filter on it — is a genuine improvement and a genuine migration. It has not been decided.

Importing an agent

An agent imports through the same three GETs, because an agent is a Runnable — one with kind: "agent" and two extra columns on the head:

FieldWhat it is
descriptionOne-line documentation. What it is for.
markdownThe agent's instructions. Behaviour, not documentation.
channel{ kind, target?, config? } — where the pipe attaches. A stored declaration; never a live connection.
import an agentbash
dreamlake runnable list --kind agent
dreamlake runnable show agents.triage.triage_agent
dreamlake runnable import agents.triage.triage_agent --out ./imported

markdown and channel come back on the head response, in plaintext — there are no encrypted columns on these models, and no read-path stripping. That is also why channel is credential-scanned on the way in; see the registry page.

Importing an agent at a version pins its code, not its instructions

This is a genuine gap and it is worth being blunt about. markdown and channel live on the mutable head, so they are whatever they are right now. The version hash covers the source and only the source.

Pin agents.triage.triage_agent at 9f2c1b… and you have pinned its code. Someone editing the agent's instructions an hour later changes what your "pinned" agent does, and nothing in your lockfile, your diff or your import output will show it. There is no history to compare against either — see the split tripwire.

If that matters to you today, copy markdown into your own repo at import time and treat drift from it as a review item.

What you get back

The version-detail route — the only one that serves source:

GET …/runnables/{name}/versions/{sha256}json
{
  "id": "665f1c2a9b4e7d0012ab34cd",
  "runnableId": "665f1c2a9b4e7d0012ab34c9",
  "namespaceId": "665f1c2a9b4e7d0012ab3400",
  "version": "9f2c1b7e5a0d3c8846b1f2e9d47a05c3b8e6142f9ad0cb75e3812f4a6d9c0b17",
  "signature": {
    "params": [
      { "name": "clip_id", "annotation": "str" },
      { "name": "stride", "annotation": "int", "default": 4 }
    ],
    "returns": "dict",
    "doc": "Embed every Nth frame of a clip and return the key it was written to."
  },
  "metadata": null,
  "createdAt": "2026-08-06T11:42:07.918Z",
  "source": "import dreamlake.lakeshore as dls\n\n\n@dls.udf(queue=\"gpu\")\ndef embed_frames(clip_id: str, stride: int = 4) -> dict:\n    ...\n"
}

Notes on the shape, since two of them catch people:

  • source is nullable. A version may be registered without one. Check before you write a file.
  • signature is always an object, defaulting to {} — never null. Its contents are the SDK's business, not the server's, so treat every key inside it as optional.
  • metadata is null when unset, not {}.
  • The list projection (GET …/versions) returns every field above except source, enveloped as { "versions": [...], "page", "pageSize", "total", "totalPages" }.
  • Errors are always { "error": "…" }.
The Registry →

The write direction — declaring, versioning, and what is not versioned.

Preloaded functions →

Once you have imported it, making the expensive half happen once.

Local development →

Running against an in-memory dispatch with no infrastructure at all.