Functions¶
Workpeg Functions are the backend logic of a Peg. They run in isolated containers, accept a JSON payload, and return a JSON result. Functions are packaged as Docker images and published to the Workpeg Registry.
Creating a project¶
A function on its own:
Generated structure:
hello/
├── app/
│ ├── __init__.py
│ └── main.py ← function entrypoint
├── wheels/ ← vendored wheels; builds have no network
├── Dockerfile ← FROM the function base at your SDK's version
├── requirements.txt
└── README.md
The Dockerfile builds on the function base image,
which already has the SDK, a non-root user and the runtime's entrypoint.
Workpeg builds it with no network, so extra packages are vendored into
wheels/ by running pip inside that image; that page has the command, and
explains how requirements.txt is installed.
Re-create if the directory already exists:
If you are building a whole peg, workpeg project create writes the same app/, Dockerfile and requirements.txt layout into the peg's function/ directory, alongside a ui/ half and a shared workpeg.json. Everything on this page works there — run the commands from inside function/, or pass --path.
Function contract¶
Every function implements:
| Parameter | Type | Description |
|---|---|---|
context |
dict |
Platform-supplied context (table, user, metadata) |
payload |
dict |
Caller-supplied input |
Return value is serialized to JSON and wrapped in the runtime envelope.
Minimal example:
Richer example:
def main(context, payload):
name = payload.get("name", "stranger")
return {
"greeting": f"Hello, {name}!",
"context_keys": list(context.keys()),
}
Input format¶
The runtime expects JSON on stdin or as an HTTP body:
{
"context": {
"table": { ... },
"staff": { ... },
"metadata": { ... }
},
"payload": {
"key": "value"
}
}
Response format¶
Success:
Error (unhandled exception):
Context object¶
The context dict carries structured platform information about the caller. Fields depend on what the Workpeg platform injects at invocation time.
Context types¶
table — Workpeg table context:
{
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "my_table",
"owner_id": 42,
"metadata": {...},
"columns": [...],
"namespace": "org_name",
"peg": {...}
}
staff — Staff/user context:
metadata — Arbitrary key/value metadata. Any dict is accepted.
actor — who wrote the row, when the platform knows:
Present on writes made by the assistant through a declarative manifest action. A string rather than a boolean, because "not the agent" is not one thing — a scheduled job, a webhook and another Peg's hook are all not-the-agent and nothing like a person at a keyboard. Absent means the platform did not say; treat it as unknown rather than as "a human".
You can only act on it in a pre-save hook, which is the only phase that can refuse a write. See Save hooks.
The context shape is not identical on both paths. An ordinary API write sends the nested objects above; the assistant's declarative path currently sends flat identifiers. Read defensively —
context.get("table")may be a dict or a uuid string depending on who wrote the row.
Accessing context in a function¶
def main(context, payload):
table = context.get("table", {})
staff = context.get("staff")
meta = context.get("metadata", {})
table_name = table.get("name")
user_email = staff.get("email") if staff else None
return {"table": table_name, "user": user_email}
ContextBuilder (SDK)¶
When building tools that invoke functions programmatically, use ContextBuilder to construct and validate a context object before sending it:
from workpeg.context import ContextBuilder
ctx = (
ContextBuilder()
.add("table", {
"uuid": "a1b2c3d4-e5f6-1234-8900-aabbccddeeff",
"name": "orders",
"owner_id": 1,
"metadata": {},
"columns": [],
"namespace": "my_org",
"peg": {},
})
.add("staff", {"email": "dev@example.com"})
.add("metadata", {"source": "test"})
.build()
)
ContextBuilder.add() validates each context entry and raises ValueError if required fields are missing or malformed. It returns self so calls can be chained.
Entrypoint¶
Default entrypoint:
This means: file app/main.py, function main.
Override via environment variable:
Override in workpeg.json:
Entrypoint format: module.path:function_name
Local runtime (no Docker)¶
Run a single invocation from stdin:
Response goes to stdout:
Runtime server¶
Start a warm HTTP server:
Default binding:
Invoke:
curl -X POST http://localhost:8000/invoke \
-H "Content-Type: application/json" \
-d '{"context": {}, "payload": {"name": "World"}}'
Health check:
The invoke key. With WORKPEG_INVOKE_KEY set, POST /invoke answers
only a request carrying that key in X-Workpeg-Invoke-Key, with
Content-Type: application/json; anything else is a 401 and the function
does not run. A dev install's runner sets it on the container it starts, so
that a web page open in your browser cannot run your function on loopback.
Unset, as on every deployed container, the runtime answers as above.
--request-file FILE and --answer-file FILE read the envelope from, and
write the answer to, a file instead of stdin and stdout.
Running it for a dev install¶
A function is called with three envelopes, a Tables API save, a row Workpeg
AI wrote and a manifest action. workpeg function test <peg> runs it once
with any of them, offline. On a dev install, workpeg project serve <peg>
--fake-cloud runs it on your computer for your own saves, in Docker by
default; Workpeg's engine, which will run functions apart from the API, is
not available yet. See Your function.
Building a Docker image¶
Uses Dockerfile in the project root, after checking it by the rules
Workpeg's install applies (The rules for every function build).
A Dockerfile Workpeg would refuse is refused here with the same message,
and Docker never runs. Default image tag is derived from the project directory name:
Custom tag:
Custom project path:
Build the way Workpeg does (no network for any RUN step, the base
pulled fresh, and linux/amd64, which Workpeg runs, also on an Apple
silicon Mac):
Without --like-workpeg the build uses your computer's network and
processor, so a step that downloads still builds here and fails on Workpeg.
See Check a build before you submit.
Tag resolution order: --tag flag → workpeg.json build.image → auto-generated slug.
build.image names the image on your computer only. Workpeg names every
function image itself and refuses a bundle whose workpeg.json sets
build.image, so workpeg function submit refuses it too, and
workpeg function build warns. Use --tag instead.
Running with Docker¶
This:
- Builds the image (unless
--no-build) - Starts a detached container
- Exposes the runtime HTTP server
Skip rebuild:
Custom project path:
Docker networking:
Attach the container to a Docker network so other containers can reach it:
Other containers address the runtime at:
This allows multiple Peg functions to run side-by-side without host port conflicts.
Disable port exposure to host:
Default endpoint (when exposed):
Getting a token (WORKPEG_PK)¶
function submit and cdn submit authenticate with a single JWT, WORKPEG_PK,
generated from the Workpeg developer dashboard:
- Create an account. Open https://developers.workpeg.com/ and fill in the sign-up form — it appears on your first visit (or whenever no account is detected).
- Create a Peg. From the dashboard choose New peg and complete the form. New Pegs go through a review that can take up to 3 working days.
- Generate the token. Once the Peg is approved, open it from the dashboard's Peg list → API & SDK → Generate token, and copy the JWT.
Then expose it to the SDK:
The token is scoped to that Peg — its payload carries the peg_namespace the SDK
reads to route submit calls to the right registry. Treat it like a password:
keep it out of source control (use an env var or a secrets manager), and
regenerate it from API & SDK if it ever leaks.
WORKPEG_PK is not a Tables API key
The registry signs this token, and only the registry can check it. It is
for workpeg function submit and workpeg cdn submit, and it stays on
your machine or in your CI. The Tables API refuses it with
401, so it does not belong in a function's environment. A running
function reaches tables with the token the platform puts in each call's
context; see
Backend access from a function.
Submitting a function¶
A function has branches, and two of them matter:
devis the default development branch. A bare version goes there.mainis the release channel. Workpeg reviews each main version, not just the peg once, and a main version is installed only once approved.
Upload a development version, which is the default:
That uploads dev:1.0.0, and the SDK says so in one line on stderr after
the upload:
dev:1.0.0 is uploaded for development only; not reviewed, installed or run on Workpeg; submit main:1.0.0 when it is ready.
When the version is ready for publish, submit it to main:
The SDK then says:
Publishing is Workpeg's step, not yours, and approval does not publish
anything by itself. --branch main with a bare version does the same
thing:
The ref is <version> or <branch>:<version>. --branch NAME names the
branch for a bare version. Giving both a colon ref and a different
--branch, as in main:1.0.0 --branch dev, is refused before anything is
uploaded, so neither one silently wins. A bare main or dev, in any
case, is refused too, because it is a branch and not a version:
workpeg function submit main says
'main' is a branch, not a version: give main:<version>.
Only main is ever installed
dev, and every other branch you name, is never reviewed, installed or
run on Workpeg.
The token is a JWT. The SDK decodes it (without verifying the signature) to extract the peg_namespace used for routing to the correct registry endpoint. The server validates the signature.
Before it uploads anything, submit checks the project the way Workpeg's
install will: a regular Dockerfile and app/main.py, a Dockerfile
that follows the rules for every function build,
and a workpeg.json without build.image. A project that fails is not
uploaded, and the error names the line and the rule, in the words
Workpeg's install would use.
Custom API endpoint:
Custom project path:
Request timeout:
The SDK bundles the project directory into a .tar.gz archive, excluding:
.git/__pycache__/.pytest_cache/venv/,.venv/dist/,build/.DS_Store
How a published function is stored¶
workpeg function submit uploads the bundle to the Peg registry
(repo.workpeg.com), which stores it as an object in the private repo
bucket, addressed by Peg, branch, and version:
- Versions are immutable. Re-submitting an existing
<branch>:<version>is rejected (409 Conflict) — submit a new version instead. The registry records a SHA-256 of every bundle at upload. mainis the release channel, and the only branch ever installed. When a business installs your Peg, the platform pulls the pinned version frommain.- Each main version is reviewed. Every version submitted to
maincarries its own review status, and a main version is installed only once approved. Approving one version does not approve the next. - Other branches are for development only.
devis the default, and you may name others. The registry stores their versions, but they are never reviewed, installed or run on Workpeg.
You can list a function's versions and fetch per-version metadata (size,
timestamps, checksum, review status, a short-lived download URL) from the
registry with the same WORKPEG_PK token used to submit.
How the platform calls a function¶
When a business installs your Peg, the platform downloads the pinned
version's bundle from the main branch (the release channel; no other
branch is installed or run on Workpeg), builds its Docker image, and starts
the container on the platform's internal network — functions are never
imported into the platform process; every call crosses HTTP into your
container.
Inside the container the runtime server listens on port
8000, and the platform invokes the function by POSTing the standard
envelope to /invoke:
{ "context": { "table": {…}, "staff": {…}, "metadata": {…},
"business": "<business_uuid>", "token": "<JWT for this call>" },
"payload": { "…": "…" } }
context["business"] and context["token"] are the function's only way back
into the Tables API: the token is minted by the API for this one call, on
behalf of the person whose save or request caused it, and expires after
15 minutes. A manifest action's context carries the same two keys.
Your main(context, payload) runs and the response comes back as:
A raised exception returns {"status": "error", "error_type": …,
"error": …} with a non-2xx status. The platform also probes
GET /healthz to check the installed function is alive.
The main way functions get called today is as table hooks: register
custom.<peg_namespace>:<version> as a table's pre- or post-save hook and
the platform invokes your container on every row save — see
Tables & rows for the hook semantics (what the
payload is per phase, what happens to the result, and error behaviour).
Calls also go the other way: while it handles a call, a function can
call into the Tables API with that call's context["token"], for the
business in context["business"]. The token is statelessly authenticated,
acts as the person who caused the call, and is jailed to the Peg's own
tables. The dashboard's WORKPEG_PK does not work there: the API cannot
verify it and answers 401. See
Backend access from a function.
Both tokens are backend secrets: server-to-server only, never in UI code or
anything a browser can see.
Runtime backends¶
| Backend | Flag | Status | Description |
|---|---|---|---|
| Docker | --with docker |
Available | Local container runtime |
| Firecracker | --with cracker |
Not available yet | Workpeg's engine, which runs functions apart from the API; it is not built yet, so --with cracker refuses and names --with docker |
Runtime resolution order:
--withCLI flagruntime.defaultinworkpeg.json- Fallback default:
cracker, which refuses. A folder with noworkpeg.jsonof its own reads its parent's, so aworkpeg project createpeg'sfunction/gets the peg's"default": "docker"
workpeg.json reference¶
{
"name": "hello",
"description": "Example function",
"namespace": "my-peg",
"function": {
"entrypoint": "app.main:main"
},
"runtime": {
"default": "docker",
"docker": {
"host": "127.0.0.1",
"port": 8000
}
},
"cdn": {
"assets_path": "assets"
}
}
All fields are optional. Missing fields fall back to SDK defaults.
namespace records the peg the function belongs to. workpeg project create writes it; workpeg function new does not.
Docker requirements¶
A function image starts FROM the function base image
and installs extra packages only from its wheels/ folder, with no index.
Workpeg builds it with no network, and refuses any other base than python
or that image at an exact version: FROM alpine is refused, FROM
python:3.12-alpine is accepted. See
which FROM lines are accepted.
Required for workpeg function build and workpeg function run --with docker:
- Docker installed and on
PATH - Docker daemon running
When running the SDK inside a container (e.g., in CI), mount the Docker socket:
Development lifecycle¶
# 1. Create project (or `workpeg project create my-peg` for a whole peg)
workpeg function new my-function
# 2. Iterate locally (fast, no Docker)
echo '{"context": {}, "payload": {}}' \
| workpeg function runtime
# 3. Test with Docker
workpeg function build
workpeg function run --with docker
# 4. Upload to dev, then submit the release for review
export WORKPEG_PK=<your-token>
workpeg function submit 1.0.0
workpeg function submit main:1.0.0