# 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](/lakeshore/registry.md).

## 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.**

```bash file="discover"
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.**

```bash file="history"
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.**

```bash file="one version"
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`

```bash file="import with curl"
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](#no-latest-no-alias-no-tag-no-prefix) below.

### With the CLI

```bash file="import with the CLI"
# 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

```graphql file="read the registry"
query Imports($ns: String!) {
  runnables(namespaceSlug: $ns, kind: "udf") {
    name
    description
    latestVersion
  }
  runnableVersions(namespaceSlug: $ns, id: "examples.embed.embed_frames") {
    version
    signature
    createdAt
  }
}
```

> **Note:** 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:

```bash file="correlating by hand"
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:

```text file="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.

> **Warning:** 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.

```python file="run the imported source locally"
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.

> **Warning:** `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:

| Field | What it is |
| --- | --- |
| `description` | One-line documentation. What it is for. |
| `markdown` | The agent's **instructions**. Behaviour, not documentation. |
| `channel` | `{ kind, target?, config? }` — where the pipe attaches. A stored declaration; never a live connection. |

```bash file="import an agent"
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](/lakeshore/registry.md#channel-is-shape-checked-and-credential-scanned).

> **Warning:** 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](/lakeshore/registry.md).
>
>   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`:

```json file="GET …/runnables/{name}/versions/{sha256}"
{
  "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 write direction — declaring, versioning, and what is not versioned.

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

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