Run your own code
Add code your pod can run when no capability does what you need (a code skill, a script on your own computer, or a card in the apps), how each is sandboxed and approved, and how agents, apps and scripts call it.
When no capability does what you need, you or your agent can add code. There are three places code runs. Each is sandboxed, and by default code an agent adds waits for your approval before it runs.
Once approved, your code can be run by your agents (over MCP), by the CLI, by your automations, and by any script or website server that holds an API key for your pod. A page in a visitor's browser, or an app a person connects to their pod, cannot run it: see Call your code from elsewhere.
| You want | Add | Where it runs |
|---|---|---|
| Logic over your data or an API: compute, filter, call a service, write records back | A code skill (JavaScript) | In your pod's sandbox |
| A program with packages, files or Python: convert a file, chart a CSV, scrape a page | A box script (Python or Node.js) | On a computer of yours, in a fresh box per run |
| Records shown your way: a card, a chart, a board | A cell (a card in the apps) | In the Synap apps, in a sandboxed frame |
For a single HTTP call on a service you already connected, you need no code: add a declarative verb instead (see Make your own capability).
Code skills
A code skill is a verb whose body is JavaScript. Once approved, it shows up next to your other verbs, and everything that can run a verb can run it.
What the code is
Your code is the body of an async function. The pod runs it as:
(async function (args, context) {
/* your code */
})(args, context);argsholds the inputs of the run, shaped by the skill'sparameters(for example{ "days": "number?" }).contextholdsuserId,workspaceIdandagentUserId.- What you
return(JSON) is the run's result.awaitworks at the top level.
The code has the JavaScript built-ins plus URL, URLSearchParams, TextEncoder, TextDecoder, btoa, atob and
structuredClone. There is no fetch, require, process or Buffer: code that uses a name the sandbox does not
provide is refused when it is saved. It reaches the outside only through what the pod hands it:
| In the code | Does |
|---|---|
hubProtocol.getEntities({ profileSlug, limit }), hubProtocol.search(query, opts), hubProtocol.getDocument(id) | Read your pod, as you |
propose.entity({ profileSlug, title, properties, … }) | Write a record back, through the same governance as any agent write |
callProvider(tool, method, path, body) | Call a connected service's API with its stored credential. The code never sees the key. |
secrets.get("vault://…") | Read a secret you granted it |
host.fetch(url, opts) | Reach a host on the skill's approved allowedHosts list (declared in a capability definition); every other host is refused |
Example
This skill lists your overdue tasks. It reads only your pod:
const tasks = await hubProtocol.getEntities({ profileSlug: "task", limit: 100 });
const today = new Date().toISOString().slice(0, 10);
const overdue = tasks.filter(
(t) => t.properties?.dueDate && String(t.properties.dueDate).slice(0, 10) < today,
);
return { count: overdue.length, titles: overdue.map((t) => t.title) };Add one
- Ask your agent. It calls
synap_create_skillwith aname, adescription, thecode, abodyexplaining the skill, and itsparameters. When the code calls a connected service, it lists that tool inrequiredTools, so you approve the tool together with the skill.timeoutSecondsis 30 by default, at most 300. - The API:
POST /api/hub/skills. See Skills in the Hub REST API. - Inside a capability, as a verb of
kind: "code", so it installs with the connection it needs. See Capabilities.
To fix a skill, change it in place (synap_update_skill), never create a second one under the same name.
Approval
A code skill runs only while it is turned on:
- An agent's new skill comes back as a proposal (a Request in the apps), whatever its write mode: creating a skill adds something that can act, so it is never one of the undoable writes an agent may make directly. The review shows its whole code, the services and hosts it can reach; approving it also turns it on.
- If you add a rule that lets an agent create skills without asking, a skill whose code reaches nothing outside your
pod (no hosts, no connected tools, no
callProvider) starts turned on. Any other skill starts off, and the pod asks you once whether to turn it on. - A code skill created through the API by a person starts turned off. Turn it on with
POST /api/hub/skills/{id}/approveand{ "approved": true }, with a person's key: an agent's key is refused. - Changing what a skill runs (its code, parameters, timeout, the hosts it may reach, or a tool added to it) turns it
off until a person approves it again.
synap_list_capabilitiesthen shows it asblocked: "not_enabled", with a link to turn it on.
How it is isolated
Each run happens in a V8 isolate (isolated-vm) with 64 MB of memory and the skill's timeout, inside a separate
worker process the pod supervises, started with Node.js's permission model and read-only file access. A code skill
never runs inside the pod's API process; a pod in production refuses to.
Box scripts (advanced)
A box script is a whole program in Python or Node.js, with packages, run on a computer of yours, never on the pod. Each run gets a fresh box with no access to your pod and no secrets; its network reaches only the package registries (while installing) and the hosts the script declared.
Your agent writes one with synap_create_skill and executionMode: "box", the program in code, and
box: { language: "python" | "node", packages: [{ name, version }], hosts: [...] }. Packages are pinned to an exact
version. The script reads its inputs from /in/parameters.json and writes files to /out; they come back with the
run, with its log and exit code. The reviewer approves the code, the packages and the hosts together.
A run is queued (status: "queued" with a boxRunId) until a computer picks it up. For that, one of your computers
runs synap box serve, and you turn on Runs scripts for it in the pod admin console, under Computers.
Otherwise the run is refused and says why.
A Proxmox host is needed today
synap box serve creates each box as a fresh unprivileged LXC container on a Proxmox host, and destroys it after the
run. Preparing that host is not covered in these docs yet.
Cells
A cell is UI code that renders records: a card for one kind, a chart, a board. It runs in the Synap apps, not on the pod.
# one module, packages included
synap cell build ./deal-card.tsx --out ./dist/deal-card.js --bundle-deps
# send it to the pod
synap cell define --name "Deal card" --file ./dist/deal-card.jssynap cell build --define does both in one step. Bundle every dependency: a cell that asks for packages at run time
fails to load. Your agent defines one with
synap_create_cell (always a proposal), then binds it as how a kind renders (list, detail, card or
dashboard) with synap_promote_cell_to_renderer.
The cell runs in a sandboxed frame with no access to the app's origin, cookies or storage, and no pod key. It runs only its own bundled code, can contact only hosts that a package you installed declared, and reads and writes your pod by messages to the app, which allows a fixed set of operations.
Call your code from elsewhere
Once approved, a code skill or a box script runs like any verb:
| From | How |
|---|---|
| Your agent (MCP) | synap_list_capabilities lists it; synap_run_capability runs it, by its name (verbId) or skillId, with parameters |
| The CLI | synap capability run <name> --<input> <value> |
| An automation | A skill or capability step |
| A script, or your website's server, with an API key | POST /api/hub/capabilities/execute with skillId or verbId and parameters (scope hub-protocol.write), or POST /api/external/skills/{id}/invoke with { "input": { … } } (scope skills.invoke) |
When an agent runs it, the same rules as any verb apply: it can come back as a proposal you approve. See Capabilities.
What is not possible today:
- An app a person connects to their pod (built on
@synap-core/sdk/connect, see Build a website on your pod) cannot run your own skills. It reaches only the actions the pod offers for an intent, such as "publish a post", and those come from Synap's official capabilities. - A public website with no key cannot run code on your pod.
- An automation step cannot hold code. Its
condition,transformandcomputesteps take expressions; put code in a skill and call it from askillstep.