Guides
Use with a sandbox
Use a group from E2B, Daytona, Modal or your own containers with a scoped API key.
A code sandbox runs commands but forgets everything when it shuts down. A Primitives group holds the state: the database, files, queues, memory, vault, keys and objects live outside the sandbox and survive it. Code inside the sandbox reaches the group over HTTPS or MCP with an API key scoped to that group.
How it fits together
- Your orchestrator, in a trusted runtime, creates the group and the primitives the run needs with its workspace API key.
- In the dashboard, you create an API key scoped to that group and to only the tools the sandboxed code needs.
- You pass the scoped key and URL into the sandbox as environment variables.
- Code in the sandbox uses the SDK or REST API with that key, or an MCP client connects with it. The key can't reach any other group, can't manage groups, and can't read secret values.
Workspace keys and root tokens never enter the sandbox.
In your orchestrator
import { Primitives } from '@primitives/sdk';
const p = new Primitives({ token: process.env.PRIMITIVES_API_KEY }); // workspace key, server only
const group = await p.groups.create({ name: 'agent-42' });
const workspace = await group.fs.create('workspace');
const results = await group.db.create('results');
await group.memory.create('notes');
await workspace.write('/task.md', taskDescription);
// Hand the sandbox the URL, the group's name, the primitives' ids and a key
// created in the dashboard, scoped to agent-42 and fs_*, db_*, memory_*. Nothing else.
const sandboxEnv = {
PRIMITIVES_URL: 'https://api.useprimitives.com',
PRIMITIVES_GROUP: group.name,
PRIMITIVES_API_KEY: process.env.AGENT_42_API_KEY,
WORKSPACE_ID: workspace.id,
RESULTS_ID: results.id,
};A key is scoped to a group by name, so create the key for the group name the sandbox will use. How you pass sandboxEnv depends on the sandbox: E2B, Daytona and Modal all accept environment variables when you create a sandbox or run a command, and so do docker run -e and Kubernetes pods.
Inside the sandbox
With the SDK, open the group by name. p.group(name) makes no request, so it works with a key that can't list groups, and group.fs('workspace') finds the primitive by name. p.fs(id) opens it by id instead:
import { Primitives } from '@primitives/sdk';
const p = new Primitives({ token: process.env.PRIMITIVES_API_KEY, baseUrl: process.env.PRIMITIVES_URL });
const group = p.group(process.env.PRIMITIVES_GROUP!);
const workspace = group.fs('workspace');
const task = await workspace.readText('/task.md');
await workspace.write('/out/report.md', await doTheWork(task));
await group.memory('notes').remember({ kind: 'episodic', text: `Finished ${process.env.PRIMITIVES_GROUP}` });import asyncio, os
from primitives import Primitives
async def main():
async with Primitives(os.environ["PRIMITIVES_API_KEY"], base_url=os.environ["PRIMITIVES_URL"]) as p:
group = p.group(os.environ["PRIMITIVES_GROUP"])
workspace = group.fs("workspace")
task = await workspace.read_text("/task.md")
await workspace.write("/out/report.md", task.upper())
asyncio.run(main())Without an SDK, plain HTTP works from any language. Every primitive endpoint takes the primitive's id; GET /v1/primitives lists the key's group if you don't have them:
curl -s "$PRIMITIVES_URL/v1/primitives/$WORKSPACE_ID/files/task.md" \
-H "Authorization: Bearer $PRIMITIVES_API_KEY"
curl -s -X POST "$PRIMITIVES_URL/v1/primitives/$RESULTS_ID/query" \
-H "Authorization: Bearer $PRIMITIVES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sql": "select count(*) as n from results"}'Calls outside the key's allowlist fail with 403 tool_not_allowed.
An agent in the sandbox
If the agent itself runs in the sandbox, connect its MCP client with the same key. Claude Code, for example:
claude mcp add --transport http primitives https://mcp.useprimitives.com/mcp \
--header "Authorization: Bearer $PRIMITIVES_API_KEY"The group comes from the key, so the URL is the same for every group. The model reads files, queries the database and remembers things through tools, and the sandbox runs the code. Both see the same group. The agent can call primitive_list to see the group's primitives, and tools take one as primitive, by id or name ("workspace", "results"). Since the group holds a single primitive of each of those types, the agent can leave it out.
Local development
With primitives dev on your machine, use http://localhost:7777 for the REST API and http://localhost:7777/mcp/<group> for MCP, with no key. primitives dev listens on 127.0.0.1 by default. To reach the REST API from a local container, start it with --host 0.0.0.0 and --token <secret> (so account calls need a root token), and use your Docker host alias, such as http://host.docker.internal:7777. Only do this on a trusted network. The local MCP endpoint accepts localhost and 127.0.0.1 as the host, so run MCP clients on the same machine as the engine. There are no scoped keys locally, so local code can reach every group: test the isolation in the cloud with a scoped key.
Coming soon: mounts
Mounting a filesystem primitive into a sandbox over FUSE or NFS, so ordinary tools like git and npm work on it directly, is coming soon, along with sandbox adapters for E2B, Daytona and Modal. Until then, copy the files a command needs into the sandbox with fs.read, and write results back with fs.write:
for (const path of await workspace.glob('/repo/**/*.ts')) {
await sandbox.files.write(path, await workspace.readText(path)); // your sandbox's file API
}Cleaning up
Revoke the scoped key in the dashboard when the agent no longer needs it, and destroy the group if you don't need its state:
await group.destroy({ force: true }); // the group and every primitive in it