Developer docs

splime framework, daemon, and server documentation.

This page documents the current splime developer surface: how Python functions become versioned, reusable SPL objects, how the local daemon builds isolated worker environments, how remote runs move through the server, and how to inspect or call Functions and Pipelines from notebooks, scripts, services, and the console.

New here? Follow the path: InstallationQuickstartCore concepts. Want the product story first? See the overview and project status.

Overview

splime is a private Python control plane for reusing trusted code. The framework serializes trusted Python functions and Pipeline graphs into SPL/YAML. The local daemon stores those object versions, creates per-spec virtual environments, and runs workers. The central server coordinates libraries, machines, access, remote runs, events, and artifacts. The server does not execute user code.

Execution rule `SPLClient()` always speaks to a local daemon first. A plain `client.call("name")` runs on this machine. Passing `owner`, `library`, or `target_machine` creates a server-side remote run that is executed by an allowed daemon machine.
01

`spl.core`

Python object model, Pipeline builder, adapters & artifacts, SPL/YAML import/export, and `SPLClient`. Ships in the `splime` package.

02

`spl-daemon`

Local registry, object versions, worker subprocesses, environment cache, server sync, and run history. The daemon command, also in `splime`.

03

`spl-server`

Central registry, libraries, machines, tokens, grants, remote run queue, artifacts, and console API.

Installation

splime ships as a single package - the framework and the local daemon together. It requires Python 3.13 or newer. The distribution is named `splime`; the import package is `spl`, and the daemon command is `spl-daemon`.

Install from PyPI

Use the current SPLime 0.4.9 release on PyPI.

python3.13 -m pip install "splime==0.4.9"
# isolated CLI for a worker:
pipx install "splime==0.4.9"        # or: uv tool install "splime==0.4.9"
spl-daemon --version

Upgrade from PyPI

python3.13 -m pip install --upgrade "splime==0.4.9"
spl-daemon --version

Editable contributor checkout

Use an editable install only when developing SPLime itself; it is not an alternative release-install path.

git clone https://github.com/yastrebovks/splime
cd splime
python -m pip install -e ".[test]"

Package names

Project Package Console command Role
`splime` `spl` `spl-daemon` Python framework, SDK, and local daemon runtime (one package).
`spl-server` `spl_server` `spl-server` Central control plane server.

Quickstart

This is the smallest useful local workflow: start the daemon, register the current Python interpreter as an environment, publish a function, inspect its signature, and call it.

Start the local daemon

python -m spl.daemon serve --host 127.0.0.1 --port 8765 --home .\.spl-daemon

Use the framework from Python

from spl import SPLClient

def add(a: int, b: int, scale: int = 1) -> dict:
    value = (a + b) * scale
    return {
        "a": a,
        "b": b,
        "scale": scale,
        "value": value,
        "formula": f"({a} + {b}) * {scale} = {value}",
    }

client = SPLClient()
client.health()
client.register_env("default")

published = client.publish(add, name="demo_add", env="default")
print(published.name, published.entrypoint)

print(client.describe("demo_add"))

result = client.call("demo_add", kwargs={"a": 2, "b": 3, "scale": 10})
print(result.mode)        # local
print(result.output)      # 50 — the unwrapped value

Expected result shape

Object kind Selector How to read
Function No `output` needed `result.output` (same as `result.value` — functions return a plain value)
Pipeline with alias `output="alias"` `result.output` unwraps the port dict; `result.value` keeps it raw
Pipeline without selector No `output` Result is a map keyed by output aliases or node ports.

Core concepts

Object

An SPL object is a versioned callable unit. It is either a Function or a Pipeline. The object name is the stable lookup key in a local daemon or server library. `display_name` is the human-friendly label shown in descriptions and the console.

Function

A Function wraps one Python callable. The framework extracts its signature, annotations, default values, source body, and distribution dependencies when exporting to SPL/YAML.

Pipeline

A Pipeline is a graph of nodes. Nodes can be local Python functions (`NodeFunction`), remote SPL objects (`NodeRemote`), or scalar values linked into node inputs. Pipeline aliases are named outputs that users can select with `output="alias"`.

Environment

A daemon environment is a named base Python executable. For each unique dependency spec, the daemon creates a separate cached venv under `environment-builds/<spec-hash>/venv`. The hash is based on the base Python executable and exact `package==version` dependencies captured in the object metadata.

Library

A server library is an owner-scoped namespace and access boundary. Objects are unique inside the owner/library namespace. The default library is `default`; custom libraries can define execution policy and a default machine.

Owners, libraries, and handles

A library is always the pair (owner, slug), so your default and a teammate's shared default are two different libraries. Every user has a unique @handle — a human reference you can pass anywhere an owner id is accepted; canonical ids stay the stored identity, handles resolve on the server.

When you pass library= without an owner, splime looks in your own namespace first. If the object is not there and exactly one accessible library of the same slug has it, the call resolves through that library and the receipt says so. Several candidates fail loudly with a copy-pasteable list. Writes never auto-resolve. Since 0.4.4.

client.whoami()                                   # who am I, on which server
client.users()                                    # id / @handle directory (no emails)
client.libraries()                                # rows carry owner + owned
client.library.get("default", owner="@alice")     # someone else's library, explicitly
client.call("report", library="default")          # own first; unique shared match auto-resolves

Run & manifest

Since 0.4.0 every run can be retained as data. `keep="on_failure"` is the default for local runs: a failed run keeps a versioned JSON manifest (per-node statuses, fingerprints, applied adapters and runtimes with their resolution source) plus completed artifacts, with a 7-day retention TTL. Retained runs are listed, inspected, and pruned with `run-list` / `run-show` / `run-prune`, and resumed from a recalculation set: the selected nodes and their descendants recompute, everything else is frozen and digest-validated. Each resume creates a new run that records its `parent_run_id` lineage.

In the 0.4 series, connected daemons can also mirror visible server objects into the local cache with `pull()` or a dry-run-first `pull_all()`, so signatures and normal calls keep working after the server connection goes away.

Node runtimes

Runtime is a per-node property. Pipelines can tag individual nodes with `native` (in the conductor process, the default), `venv-subprocess` (an isolated process over the stdlib-only SPL-free runner), or `docker` (0.4 series: the node runs in a container built from the object environment spec, or from an explicit `runtime_config.docker.image`). Resolution mirrors the adapter hierarchy — runtime config, node tag, then run-level override — and the selected runtime with its source is recorded in the run manifest. Subprocess and Docker nodes accept JSON-native inputs and honor `node_timeout_seconds`.

Offline cache & identity

The local daemon is a working offline cache, not just a proxy. In the 0.4 series the daemon remembers the enrolled owner identity across restarts, offline periods, and server restarts: publishing and bare-name lookups keep resolving against your own objects with no network. Local operations never wait on the central server — when it is unreachable, server-side listings simply come back empty and lookup misses explain the remediation (`client.connect_server(...)` to re-enroll, `client.pull(...)` to mirror an object for offline use). Identity diagnostics and stored-connection hygiene live in `spl-daemon doctor`, `connections-list`, and `connections-prune`.

Framework API

`SPLClient`

`SPLClient` is the main user API. It talks to the local daemon, even when the daemon later talks to the central server.

from spl import SPLClient

client = SPLClient()
client = SPLClient(daemon_port=8766)
client = SPLClient(base_url="http://127.0.0.1:8766")
Method Purpose
`health()` Checks the local daemon endpoint.
`register_env(name="default", python=None)` Registers a Python executable. `python=None` means the current interpreter.
`publish(obj, name=None, env="default")` Serializes a live function or Pipeline and stores a new daemon object version.
`publish_yaml(yaml, name, entrypoint, env)` Registers an already exported SPL/YAML bundle.
`objects(scope="auto" | "local" | "server" | "all")` Lists the connected server catalog by default, local daemon objects, or both.
`signature()`, `inputs()`, `outputs()`, `describe()` Reads callable metadata for objects and Pipeline child functions.
`submit()`, `call()` Starts local execution or a server-coordinated run on an allowed worker daemon. `submit()` returns a run handle immediately; `call()` waits for the result.
`pull(name, owner=None, library=None, version=None)` Mirrors one visible server object into the local cache for offline use. Repeat pulls are idempotent by content hash.
`pull_all(owner=None, library=None, dry_run=False)` Mirrors the visible server catalog in a batch. `dry_run=True` plans without writing; per-object failures are collected in the receipt.

Import and export helpers

from pathlib import Path
from spl.core import spl_export_to_file, spl_import_from_file

spl_export_to_file(Path("bundle.yaml"), [my_pipeline])
namespace = {}
spl_import_from_file(Path("bundle.yaml"), namespace)
restored = namespace["my_pipeline"]

Functions and pipelines

Use `lift()` to turn a function or node into a Pipeline builder. Use `bind()` to link inputs to constants or outputs from other builders. Use `alias()` to name the node result, and `render(name)` to produce a publishable Pipeline.

from spl.core.common import Deployment, lift

def happiness(a: int):
    return "Happy" if a == 300 else "Saaad :c"

def traktorist(a: int, b: int, scale: int = 1, happiness_val=None) -> dict:
    value = (a + b) * scale
    return {
        "a": a,
        "b": b,
        "scale": scale,
        "value": value,
        "formula": f"({a} + {b}) * {scale} = {value}",
        "feeling": happiness_val,
    }

pipeline = (
    lift(traktorist)
    .bind(happiness_val=lift(happiness))
    .alias("result")
    .render("demo_traktorist_pipeline")
)

deployment = Deployment(pipeline)
result_node = pipeline.get_node_by_alias("result")
run = deployment.run(a=300, b=0, scale=1)
print(run[result_node]["default"]["feeling"])

`Deployment(pipeline)` is supported for compatibility. Use `Deployment(client, pipeline)` when the graph contains `NodeRemote`, because remote node execution needs the local daemon and server connection.

Publish and call the Pipeline

client.register_env("spl_core")
client.publish(pipeline, name="demo_traktorist_pipeline", env="spl_core")

print(client.describe("demo_traktorist_pipeline"))

result = client.call(
    "demo_traktorist_pipeline",
    kwargs={"a": 300, "b": 0, "scale": 1},
    output="result",
)
print(result.output)

Calling objects

Local call

result = client.call("demo_add", kwargs={"a": 2, "b": 3})
assert result.mode == "local"
print(result.output)

Server-coordinated remote call through the local daemon

result = client.call(
    "risk_report",
    owner="alice",
    library="risk",
    target_machine="alice-gpu-01",
    kwargs={"customer_id": 42},
    output="report",
    timeout_seconds=60,
)

assert result.mode == "server"
print(result.output)

Call a Function inside a Pipeline

result = client.call(
    "demo_traktorist_pipeline",
    function="happiness",
    kwargs={"a": 300},
)
print(result.output)

The same function can also be referenced with the compact name `demo_traktorist_pipeline::happiness` where the API accepts an object reference.

Inspect before calling

print(client.describe("demo_traktorist_pipeline"))
print(client.describe("demo_traktorist_pipeline", function="happiness"))

display(client.signature("demo_traktorist_pipeline", function="happiness"))
display(client.inputs("demo_traktorist_pipeline", function="happiness"))
display(client.outputs("demo_traktorist_pipeline", function="happiness"))

Server catalog versus local catalog

client.objects(compact=True)                         # connected server catalog, local if offline
client.objects(scope="local", compact=True)          # local daemon registry
client.objects(scope="server", compact=True)         # global/server-visible catalog
client.objects(scope="all", compact=True)            # both surfaces
client.objects(scope="server", owner="alice", library="risk")

NodeRemote

`NodeRemote` represents a remote SPL object inside a Pipeline. When `inputs` and `outputs` are omitted, it asks the local daemon to resolve the signature through the active server connection. The default version is `latest`.

Create by full remote name

from spl.core.entities.node_remote import NodeRemote

node = NodeRemote(
    name="demo_traktorist_pipeline::happiness",
)

pipeline = lift(node).bind(a=300).alias("feeling").render("remote_feeling")

Create by Pipeline and Function

node = NodeRemote(
    pipeline="demo_traktorist_pipeline",
    function="happiness",
)

pipeline = lift(node).bind(a=300).alias("feeling").render("remote_feeling")

Run a Pipeline with remote nodes

deployment = Deployment(client, pipeline)
print(deployment.run(a=300, output="feeling"))  # plain value

Run a single node

# Canonical: wire the node into a (one-node) pipeline
from spl import Deployment, lift

value = Deployment(client, lift(node).alias("out").render("one_node")).run(
    a=300, output="out",
)

# client.run_node(...) / client.run_node_result(...) warned through 0.1.x
# and were removed in 0.2.0 - Deployment(...).run(...) is the canonical path.
Requirement Auto-resolved `NodeRemote` needs a running local daemon connected to splime. If the daemon is not connected, pass explicit `url`, `inputs`, and `outputs`, or connect the daemon first.

Adapters & artifacts

When one node's output flows into the next - or is returned as a file - splime can materialize it as an artifact instead of passing an in-memory value. An adapter decides how a Python value of a given `(type, format)` is written to and read back from that artifact. Artifacts are content-addressed: every `ArtifactRef` records a `sha256` and `size` that are verified on load.

Pick a wire format on an edge

Call `as_format(...)` on a producer to serialize that edge with a named format:

from spl.core.common import lift

producer = lift(make_frame)
pipeline = (
    lift(consume)
    .bind(value=producer.as_format("csv"))   # this edge crosses as a CSV artifact
    .alias("report")
    .render("daily_report")
)

Register a custom adapter

Built-in formats cover common cases; for your own type, register `save`/`load` functions with `add_adapter(py_type, format, save=..., load=...)`. It returns a new pipeline (pipelines are immutable), keyed by `make_key(py_type, format)`.

def save_box(path: str, value: Box) -> None:
    with open(path, "wb") as f:
        f.write(value.to_bytes())

def load_box(path: str) -> Box:
    with open(path, "rb") as f:
        return Box.from_bytes(f.read())

pipeline = pipeline.add_adapter(Box, "bytes", save=save_box, load=load_box)
Verified on load Both `save` and `load` must be functions. On read, splime checks the artifact's `size` and `sha256` against the `ArtifactRef` before decoding, so a corrupted or swapped file fails fast.

Libraries & access

Libraries group versioned objects and control who can see and run them. Creating and curating libraries needs a server-connected client - pass `user_token` and `machine_token`, or connect the daemon to splime first.

Create and publish into a library

client = SPLClient(user_token="...", machine_token="...")

client.library.create(
    "risk",
    display_name="Risk",
    description="Production scoring functions",
    visibility="private",
)
client.publish(risk_score, name="risk_score", library="risk")

Reference or copy across libraries

`add_reference` adds a live link that follows the source (use `version="latest"`); `copy_object` takes an owned snapshot and keeps provenance (`source_object_id`).

client.library.add_reference(
    "risk", "shared_score",
    owner="admin2", from_library="source",
    version="latest", alias="partner_score",
)

client.library.copy_object(
    "shared_score",
    into_library="risk",
    from_owner="admin2", from_library="source",
    version=3, new_name="partner_score_v3",
)

client.library.remove_entry("risk", "partner_score")

Grant and revoke scoped access

client.library.grant(
    "risk", "analyst1",
    scopes=["metadata:read", "objects:read", "execute"],
)
client.library.revoke("risk", "analyst1")

client.libraries(include_accessible=True)   # libraries you own or can use

Local daemon

The local daemon is the private runtime. It should run close to the Python environment and data it needs. It stores object versions in SQLite and launches each run in a worker subprocess using a cached virtual environment.

Start and health check

python -m spl.daemon serve --host 127.0.0.1 --port 8765 --home .\.spl-daemon
python -m spl.daemon health

Data layout

Path Meaning
`daemon.sqlite3` Local registry, versions, runs, envs, server connections, and sync queue.
`objects/<name>/versions/<n>.yaml` Human-readable YAML cache for diagnostics.
`environment-builds/<spec-hash>/venv` Cached worker venv for one dependency specification.
`runs/<run-id>/` Inputs, materialized object YAML, stdout, stderr, result, and artifacts.
`daemon-endpoint.json` Current daemon URL used by `SPLClient()` auto-discovery.

Environment commands

python -m spl.daemon env-add spl_core C:\Python313\python.exe
python -m spl.daemon env-list
python -m spl.daemon env-build-list
python -m spl.daemon env-build-show <spec_hash>
python -m spl.daemon env-build-rebuild <spec_hash> --wait

Connect daemon to the server

python -m spl.daemon server-connect ^
  --server-url https://splime.io/api ^
  --machine-token <machine-token> ^
  --user-token <user-token> ^
  --machine-id <machine-id> ^
  --display-name "Kirill laptop"

You can also connect from Python by passing tokens to `SPLClient` or by calling `client.connect_server(...)`.

client = SPLClient(
    server_url="https://splime.io/api",
    machine_token="<machine-token>",
    user_token="<user-token>",
    machine_id="machine-123",
    display_name="Kirill laptop",
)

Server

`spl-server` is the central coordinator. It stores users, tokens, teams, libraries, machines, object versions, machine snapshots, remote runs, events, artifacts, settings, and audit activity. It coordinates execution but does not run user code.

Run locally

cd spl-server
python -m pip install -e ".[test]"
python -m daemon_server serve --host 127.0.0.1 --port 9876 --home .\.spl-daemon-server

Run with systemd on Ubuntu

sudo cp /opt/spl-server/deploy/systemd/spl-server.service /etc/systemd/system/spl-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now spl-server
systemctl status spl-server
journalctl -u spl-server -f

To inspect the effective `ExecStart`, use `systemctl cat spl-server.service` or `systemctl show spl-server.service -p ExecStart`. After editing a unit file, run `sudo systemctl daemon-reload` and then `sudo systemctl restart spl-server`.

Direct server client

from spl.server_client import SPLServerClient

server = SPLServerClient(
    token="<user-or-service-token>",
    base_url="https://splime.io/api",
)

print(server.signature("risk_report", owner="alice", library="risk"))

result = server.call(
    "risk_report",
    owner="alice",
    library="risk",
    target_machine="alice-gpu-01",
    kwargs={"customer_id": 42},
    output="report",
    wait_timeout_seconds=60,
)
print(result.output)

External execution token client

from spl.server_client import SPLServerClient

external = SPLServerClient.external_token(
    token="<library-execution-token>",
    base_url="https://splime.io/api",
)

print(external.signature("risk_report", library="risk"))
result = external.call(
    "risk_report",
    library="risk",
    kwargs={"customer_id": 42},
    output="report",
    wait_timeout_seconds=60,
)

External token clients intentionally expose only callable metadata, run launch/read, events, and artifact download. They cannot manage machines, tokens, grants, settings, cancel/retry, broad object lists, or raw YAML.

Security and access model

splime assumes trusted published code and explicit execution boundaries. Access is enforced through token scopes, ownership, library grants, machine grants, delegated machine subtokens, and external execution tokens.

Credential Typical use Limits
User token Console, SDK owner actions, library and token management. Limited by scopes and owner context.
Machine token Daemon heartbeat, sync, job claim, artifact upload. No broad admin or token issuance scopes.
Machine subtoken Delegated launch onto another user's machine. Bound to one machine and allowed users.
Library execution token External service integration for one library or callable. No broad listing, raw YAML, cancel/retry, admin, grants, or token management.

Raw token values are one-time response fields. Store them immediately. List/detail APIs return hints, hashes, and metadata instead of the secret value.

Recipes

See the server library instead of only local objects

server_objects = client.objects(compact=True)
all_objects = client.objects(scope="all", compact=True)

Get source/YAML for an object version

from spl.server_client import SPLServerClient

server = SPLServerClient("<user-token>")
obj = server.get_object(
    "demo_traktorist_pipeline",
    library="default",
    include_yaml=True,
)
print(obj["yaml"])

Raw YAML is available only to authorized user or machine contexts. Library execution tokens cannot request `include_yaml=1`.

Queue a run for an offline machine

run = client.submit(
    "risk_report",
    target_machine="alice-gpu-01",
    owner="alice",
    library="risk",
    kwargs={"customer_id": 42},
    output="report",
    offline_policy="queue",
)
print(run.id, run.status)

Download artifacts

result = client.call(
    "risk_report",
    kwargs={"customer_id": 42},
    output="report",
    artifacts_dir="artifacts/risk_report",
)
print(result.downloaded_artifacts)

Refresh an editable development checkout

# Contributor workflow: an editable install picks up source edits immediately
python -m pip install -e ".[test]"

# Restart the daemon so workers pick up the new code
systemctl --user restart spl-daemon     # or relaunch: spl-daemon serve

Troubleshooting

`SPLClient.call()` says the local daemon is not reachable

`SPLClient` is trying to connect to the local daemon URL, usually `http://127.0.0.1:8765` or the endpoint saved in `daemon-endpoint.json`. Start the daemon in the same machine, WSL, Docker container, or remote kernel where the notebook runs, or pass `daemon_port` or `base_url` explicitly.

`spl-daemon: command not found` after install

The `spl-daemon` command ships inside the `splime` package. Make sure the install's scripts directory is on your `PATH`, or install the CLI in its own environment with `pipx install "splime==0.4.9"` (or `uv tool install "splime==0.4.9"`).

Python version error during install

The packages require Python 3.13+. Create the venv with Python 3.13 or newer. Python 3.12 environments will fail the package metadata check.

Worker environment does not match SPL metadata

The object metadata pins exact package versions. The daemon creates a venv per unique dependency spec. If a run uses a different version, inspect `env-build-list`, review the exact package versions captured during publish, and rebuild the matching spec hash.

Server object requires local environments that are not registered

A mirrored server object references an environment name, for example `spl_core`. Register a base Python executable under the same name with `client.register_env("spl_core")` or `python -m spl.daemon env-add spl_core C:\Python313\python.exe`.

`NodeRemote` cannot resolve inputs and outputs

The constructor omitted explicit ports, so it asked the local daemon to resolve the remote signature. Connect the daemon to splime, confirm `client.current_server_connection()`, or pass explicit `url`, `inputs`, and `outputs`.

Reference

Important local daemon CLI commands

python -m spl.daemon serve
python -m spl.daemon health
python -m spl.daemon env-add <name> <python.exe>
python -m spl.daemon env-list
python -m spl.daemon env-build-list
python -m spl.daemon object-list --compact
python -m spl.daemon object-show <name-or-id>
python -m spl.daemon object-signature <name-or-id>
python -m spl.daemon object-versions <name-or-id>
python -m spl.daemon run <object> --kwargs "{\"seed\": 42}" --wait
python -m spl.daemon server-connect --machine-token <token> --user-token <token>
python -m spl.daemon pull <name>          # mirror one server object locally
python -m spl.daemon pull --all --dry-run  # plan a batch mirror of the visible catalog
python -m spl.daemon doctor                # setup checklist + enrolled identity line
python -m spl.daemon connections-list
python -m spl.daemon connections-prune --dry-run

Important server API surfaces

Endpoint Purpose
`GET /health` Server health check.
`GET /console/overview` Single console startup snapshot.
`POST /daemon-enrollment` Create machine credentials for daemon pairing.
`POST /connections/connect` and `POST /sync` Daemon lease, heartbeat, object sync, job polling, and run updates.
`GET /libraries`, `GET /objects`, `GET /owners/<owner>/libraries/<library>/objects` Library and object catalog reads.
`GET /objects/<name>/signature?function=<function>` Callable metadata for objects and Pipeline child functions.
`POST /remote-runs` Create a remote run.
`GET /remote-runs/<id>/detail` Run state, timeline, result, and artifacts.

Testing the workspace

cd spl-core
python -m pytest

cd ../spl-daemon
python -m pytest

cd ../spl-server
python -m pytest

cd ../spl-frontend
npm run check