Framework docs

SPLime Python framework, daemon, and server documentation.

This page documents the current SPLime developer surface: how Python functions become versioned 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.

Overview

SPLime is a private Python control plane. 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-framework`

Python object model, Pipeline builder, SPL/YAML import/export, `SPLClient`, and direct server clients.

02

`spl-daemon`

Local registry, object versions, worker subprocesses, environment cache, server sync, and local run history.

03

`spl-server`

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

Installation

All Python packages currently require Python 3.13 or newer. Install the framework into the notebook or script environment, and install the daemon into the environment that will run the local daemon service.

Editable development install

cd spl-core
python -m pip install -e ".[test]"

cd ../spl-daemon
python -m pip install -e ".[test]"

Build and install wheels

cd spl-core
.\tools\build-package.ps1 -OutDir ..\dist-packages

cd ..\spl-daemon
.\tools\build-package.ps1 -OutDir ..\dist-packages

cd ..
python -m pip install --find-links .\dist-packages --force-reinstall spl-framework spl-daemon

Use `--find-links` when installing `spl-daemon` from local wheels. The daemon depends on `spl-framework`, and pip must be able to find the locally built framework wheel.

Package names

Project Package Console command Role
`spl-core` `spl-framework` None Python framework and SDK clients.
`spl-daemon` `spl-daemon` `spl-daemon` Local daemon runtime and worker execution.
`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.client 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.value)       # function value

Expected result shape

Object kind Selector How to read
Function No `output` needed `result.value`
Pipeline with alias `output="alias"` `result.value["default"]` for a single-output node
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.

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.client 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="local" | "server" | "all")` Lists local daemon objects, server-visible objects, or both.
`signature()`, `inputs()`, `outputs()`, `describe()` Reads callable metadata for objects and Pipeline child functions.
`start()`, `queue()`, `call()` Starts local or server-side execution.

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.value["default"])

Calling Objects

Local call

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

Server-side 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.value)

Call a Function inside a Pipeline

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

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)                         # 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)
result_node = pipeline.get_node_by_alias("feeling")
run = deployment.run(a=300)
print(run[result_node]["default"])
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.

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.value)

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.

Examples Cookbook

These examples show the practical composition patterns SPLime supports today. Local `NodeFunction` steps run in the current Pipeline process. `NodeRemote` steps call the local daemon, which creates a server-side remote run and waits for the selected target daemon machine to return the value.

Mixed local and remote Pipeline

In this pattern the preparation and final formatting functions run locally, while `demo_traktorist_pipeline::happiness` runs on the machine selected by the remote object's library execution settings.

from spl.core.common import Deployment, lift
from spl.core.entities.node_remote import NodeRemote

def prepare_score(a: int) -> int:
    return a

def format_feeling(feeling: str) -> dict:
    return {
        "feeling": feeling,
        "source": "local formatter",
    }

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

mixed_pipeline = (
    lift(format_feeling)
    .bind(feeling=lift(remote_happiness).bind(a=lift(prepare_score)))
    .alias("result")
    .render("mixed_local_remote_pipeline")
)

deployment = Deployment(client, mixed_pipeline)
result_node = mixed_pipeline.get_node_by_alias("result")
run = deployment.run(a=300)
print(run[result_node]["default"])
Target machine selection For a persisted Pipeline, prefer configuring `default_machine_id` on the remote object's server library. The current execution bridge can also read `target_machine` from an in-memory `NodeRemote`, but that attribute is not serialized to SPL/YAML yet.

In-memory remote node pinned to a machine

Use this only for local notebook experiments with `Deployment(client, pipeline)`. It works because `SPLClient.run_node()` forwards a `target_machine` attribute when one exists on the live node object. It is not yet a stable publishable Pipeline feature.

def pin_remote_node(node, target_machine: str):
    # Current workaround: works for this live Python object only.
    object.__setattr__(node, "target_machine", target_machine)
    return node

remote_happiness = pin_remote_node(
    NodeRemote(pipeline="demo_traktorist_pipeline", function="happiness"),
    "machine-y",
)

pipeline = (
    lift(format_feeling)
    .bind(feeling=lift(remote_happiness).bind(a=300))
    .alias("result")
    .render("mixed_local_remote_pinned")
)

Use another user's Function and Pipeline on different machines

This pattern composes remote objects from two owner/library namespaces. The risk Function belongs to Alice's `risk` library and runs on that library's default machine. The report Pipeline belongs to Bob's `reports` library and runs on that library's default machine. The final summary function runs locally on the Pipeline owner machine.

from spl.core.common import Deployment, lift
from spl.core.entities.node_remote import NodeRemote

def library_url(owner: str, library: str) -> str:
    return f"https://splime.io/api/owners/{owner}/libraries/{library}"

def normalize_customer(customer_id: int) -> dict:
    return {"customer_id": customer_id}

def merge_outputs(score: dict, report: dict) -> dict:
    return {
        "score": score,
        "report": report,
        "summary_machine": "local pipeline machine",
    }

# Requires execute/read access to alice/risk.
# alice/risk should define default_machine_id, for example alice-gpu-01.
risk_score = NodeRemote(
    url=library_url("alice", "risk"),
    name="score_customer",
)

# Requires execute/read access to bob/reports.
# bob/reports should define default_machine_id, for example bob-report-runner.
report_pipeline = NodeRemote(
    url=library_url("bob", "reports"),
    name="build_customer_report",
)

customer = lift(normalize_customer)
score = lift(risk_score).bind(customer=customer)
report = lift(report_pipeline).bind(score=score, customer=customer)

cross_library_pipeline = (
    lift(merge_outputs)
    .bind(score=score, report=report)
    .alias("summary")
    .render("cross_library_mixed_pipeline")
)

deployment = Deployment(client, cross_library_pipeline)
result_node = cross_library_pipeline.get_node_by_alias("summary")
run = deployment.run(customer_id=42)
print(run[result_node]["default"])
Step Object Runs on
Normalize input Local Python Function Current Pipeline machine.
Risk score Alice's remote Function in `risk` `alice/risk.default_machine_id`.
Report Bob's remote Pipeline in `reports` `bob/reports.default_machine_id`.
Merge result Local Python Function Current Pipeline machine.
Access requirements The caller needs metadata/read access to resolve signatures and execute access to launch each remote object. Bind names such as `customer`, `score`, and `report` must match the real input names returned by `client.inputs(..., owner=..., library=...)`.

See the server library instead of only local objects

server_objects = client.objects(scope="server", 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.queue(
    "risk_report",
    target_machine="alice-gpu-01",
    owner="alice",
    library="risk",
    kwargs={"customer_id": 42},
    output="report",
)
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)

Rebuild packages after code changes

cd spl-core
.\tools\build-package.ps1 -OutDir ..\dist-packages

cd ..\spl-daemon
.\tools\build-package.ps1 -OutDir ..\dist-packages

cd ..
.\.venv-spl\Scripts\python.exe -m pip install --find-links .\dist-packages --force-reinstall spl-framework spl-daemon

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` install cannot find `spl-framework`

Install with `--find-links` pointing to the directory that contains both locally built wheels: `python -m pip install --find-links .\dist-packages spl-daemon`.

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>

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