Get to know Cadenya
We’re developers who love to build. We set out to create a yes-code platform that makes building agents feel like the best parts of building software.
To run agent-generated code in a Daytona sandbox from a Cadenya agent, give the agent a Bare tool called run_code and answer each call from your own backend. A small worker reads the objective’s events, runs the code in a Daytona sandbox with Daytona’s TypeScript SDK, and sends the output back through the API. The worker only makes outbound requests, so nothing on your side accepts traffic from the internet, and the Daytona key never leaves your servers.
So start with the bare tool if you don’t want to host an endpoint. If you already run an HTTP service, an HTTP tool set can call it directly, and that variant comes after the run. The same setup works with E2B and Blaxel, and Sandboxes for running agent-generated code compares the three on isolation, start time and price. What Cadenya adds to a Daytona sandbox is at the end.
Every SDK version and limit below comes from Daytona’s own docs and packages, checked on October 29, 2026.
How a bare tool runs code in your sandbox
A Bare tool set holds tools with no execution adapter. Cadenya stores the tool, shows it to the model, and waits for you to supply each result. Here’s one run_code call from start to finish:
- The agent calls
run_codewith a Python program as itscodeargument. - Cadenya records the call, writes a
toolCalledevent to the objective, and pauses the loop until the call gets content. - Your worker reads that event from the objective’s event stream, creates a sandbox on the first call, and runs the code.
- The worker sends stdout, stderr and any error back with
setToolCallContent, and the agent reads them on its next loop.
The Daytona key lives only in your worker’s environment. Cadenya never sees it, and the sandbox never needs to reach Cadenya.
The worker below keeps one sandbox per objective, so a file the agent writes in one call is still there in the next. It kills the sandbox when the agent’s turn ends.
The agent, the tool and the variation as config files
Cadenya’s CLI applies resources from YAML files in .cadenya/, with settings in cadenya.yaml. Each file is one resource, and a child directory takes its parent’s filename:
cadenya.yaml
.cadenya/
toolSets/
sandbox.yaml # the Bare tool set
sandbox/
run-code.yaml # the run_code tool
agents/
code-runner.yaml # the agent
code-runner/
default.yaml # its variation: prompt, model, tools
The bundle key in cadenya.yaml marks every resource the CLI writes, and it’s how the CLI knows which ones it owns:
bundleKey: code-runner
The tool set, in .cadenya/toolSets/sandbox.yaml:
metadata:
name: Sandbox
spec:
description: Runs Python in an isolated sandbox through your worker
adapter:
type: bare
bare: {}
The tool, in .cadenya/toolSets/sandbox/run-code.yaml. llmToolName is the name the model sees, and parameters is the JSON Schema of its arguments:
metadata:
name: Run code
spec:
llmToolName: run_code
description: >-
Run Python 3 code in an isolated sandbox and get back its stdout, stderr
and any error. Files you write stay in the sandbox for the rest of this
conversation. Print every value you need to see.
parameters:
type: object
properties:
code:
type: string
description: The complete Python 3 program to run.
required:
- code
additionalProperties: false
config:
type: bare
bare: {}
The agent, in .cadenya/agents/code-runner.yaml:
state: published
metadata:
name: Code runner
spec:
description: Answers questions about numbers and data by running Python with run_code
And its variation, in .cadenya/agents/code-runner/default.yaml. The model is a setting here, so switching models is a one-line change:
metadata:
name: Default
spec:
systemPromptTemplate: |
You answer questions about numbers and data. Never compute a result in
your head: write a short Python 3 program, run it with the run_code tool,
and read its output. If the code fails, fix it and run it again. Then
answer in one or two sentences with the exact numbers the code printed.
modelConfig:
modelId: getting-started-key.xingchen-xing4-0-29b-a4b-free
temperature: 0
constraints:
maxToolCalls: 8
assignments:
- type: toolId
toolId: external_id:sandbox/run-code
Any model that cadenya models list shows and that supports tool calls works in modelId. The CLI reads CADENYA_API_KEY and CADENYA_WORKSPACE_ID from the environment. Plan first, then apply:
cadenya config plan
cadenya config apply
Plan prints every create, update and delete before apply writes anything. The cadenya-config-loader GitHub Action runs the same plan or apply from a workflow.
A worker that answers run_code
The worker needs the Cadenya TypeScript SDK and Daytona’s TypeScript SDK:
npm install @cadenya/cadenya @daytona/sdk
The worker talks to the sandbox through one small interface, so it stays the same on E2B and Blaxel. In src/sandboxes/types.ts:
export interface RunResult {
stdout: string;
stderr: string;
// Set when the code raised an exception or exited with a non-zero code.
error?: string;
}
export interface CodeSandbox {
readonly provider: "e2b" | "daytona" | "blaxel";
readonly id: string;
runPython(code: string): Promise<RunResult>;
kill(): Promise<void>;
}
The worker streams one objective’s events and answers each run_code call. In src/worker.ts:
import type Cadenya from "@cadenya/cadenya";
import type { CodeSandbox, RunResult } from "./sandboxes/types.js";
// Watches one objective and answers every run_code call in a sandbox. The
// sandbox is created on the first call, reused for the next ones, and killed
// when the agent's turn ends. Returns the agent's last message.
export async function serveRunCode(
client: Cadenya,
objectiveId: string,
createSandbox: () => Promise<CodeSandbox>,
): Promise<string> {
let sandbox: CodeSandbox | undefined;
let answer = "";
try {
const stream = await client.objectives.streamEvents(objectiveId);
for await (const event of stream) {
const data = event.data;
if (data.type === "toolCalled" && data.toolCalled.llmToolName === "run_code") {
const call = data.toolCalled;
const code = call.arguments?.code;
let result: RunResult;
if (typeof code !== "string" || code.trim() === "") {
result = { stdout: "", stderr: "", error: "run_code needs a non-empty code string" };
} else {
try {
sandbox ??= await createSandbox();
result = await sandbox.runPython(code);
} catch (error) {
// The agent reads the failure and can try again.
result = { stdout: "", stderr: "", error: `sandbox error: ${(error as Error).message}` };
}
}
// Long output is cut so one noisy program can't fill the context window.
await client.objectives.setToolCallContent(objectiveId, call.toolCallId, {
content: [{ type: "text", text: { text: JSON.stringify(result).slice(0, 8_000) } }],
});
}
if (data.type === "assistantMessage" && data.assistantMessage.content) {
answer = data.assistantMessage.content;
}
// The turn is over when the objective waits for the user, or ends.
if (data.type === "stateChanged" && data.stateChanged.toState === "OBJECTIVE_STATE_WAITING") break;
if (data.type === "finalized") break;
if (data.type === "error" || data.type === "cancelled" || data.type === "timedOut") {
throw new Error(`objective stopped: ${JSON.stringify(data)}`);
}
}
} finally {
await sandbox?.kill();
}
return answer;
}
A failed sandbox call goes back to the agent as an error it can read, so one bad program doesn’t end the objective. And the SDK’s stream reconnects on its own after a dropped connection, starting from the last event it received.
Two other ways to get the same calls fit a worker that serves many objectives:
- Polling. List an objective’s tool calls with
executionStatusset toTOOL_CALL_EXECUTION_STATUS_WAITING_FOR_CONTENT, which returns the Bare calls still waiting for a result. - Webhooks. Cadenya sends
objective_event.tool_calledto the agent’s webhook URL for every call, with the tool, its config and the parsed arguments.
The Daytona sandbox
Daytona’s TypeScript SDK runs code in the language you pick when you create the sandbox. In src/sandboxes/daytona.ts:
import { Daytona } from "@daytona/sdk";
import type { CodeSandbox, SandboxLabels } from "./types.js";
// Reads DAYTONA_API_KEY (and DAYTONA_API_URL or DAYTONA_TARGET, if set)
// from the environment.
export async function createDaytonaSandbox(labels: SandboxLabels): Promise<CodeSandbox> {
const daytona = new Daytona();
// An ephemeral sandbox is deleted as soon as it stops, and it stops
// after 10 idle minutes, so a crashed worker never leaves one behind.
const sandbox = await daytona.create({
language: "python",
labels,
ephemeral: true,
autoStopInterval: 10,
});
return {
provider: "daytona",
id: sandbox.id,
async runPython(code) {
const response = await sandbox.process.codeRun(code, undefined, 60);
return {
stdout: response.result,
stderr: "",
error: response.exitCode === 0 ? undefined : `exit code ${response.exitCode}`,
};
},
async kill() {
await sandbox.delete();
},
};
}
SandboxLabels is a Record<string, string> in the same types.ts, and the worker passes labels such as the objective ID so a cleanup job can find the sandbox later. A few things to know about Daytona:
codeRunstarts each run from a clean interpreter, so variables don’t carry over between calls. Files do, because the sandbox stays the same.resultholds the program’s output, traceback included, and a non-zeroexitCodemarks a failure.autoStopIntervalsets how many idle minutes pass before the sandbox stops, andephemeral: truedeletes it when it stops.
Run it
The script that starts an objective picks the runner from SANDBOX_PROVIDER. In src/run.ts:
import Cadenya from "@cadenya/cadenya";
import { createSandbox, providerFromEnv } from "./sandboxes/index.js";
import { serveRunCode } from "./worker.js";
const client = new Cadenya(); // reads CADENYA_API_KEY and CADENYA_WORKSPACE_ID
const provider = providerFromEnv(); // e2b, daytona or blaxel
const objective = await client.objectives.create({
agentId: "external_id:code-runner",
firstUserMessage: "What is the 1,000th prime number, and what is the sum of the first 1,000 primes?",
});
const id = objective.metadata.id;
const answer = await serveRunCode(client, id, () =>
createSandbox(provider, { objective: id }, `run-code-${id.slice(-12).toLowerCase()}`),
);
console.log(answer);
createSandbox in src/sandboxes/index.ts calls createDaytonaSandbox when SANDBOX_PROVIDER is daytona. Set DAYTONA_API_KEY, then run it:
SANDBOX_PROVIDER=daytona npx tsx src/run.ts
You should see the agent’s answer. This one comes from a run on Daytona on October 29, 2026:
The 1,000th prime number is **7,919**, and the sum of the first 1,000 primes is **3,682,913**.
Behind that answer is one run_code call. Here’s the program the agent wrote:
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
primes = []
n = 2
while len(primes) < 1000:
if is_prime(n):
primes.append(n)
n += 1
print("1000th prime:", primes[999])
print("Sum of first 1000 primes:", sum(primes))
And here’s what the worker sent back:
{"stdout":"1000th prime: 7919\nSum of first 1000 primes: 3682913\n","stderr":""}
Daytona’s default sandbox ran Python 3.14.4.
Ask a person before the code runs
Set requiresApproval: true on the tool, and Cadenya parks the loop on every run_code call until someone approves or denies it:
spec:
llmToolName: run_code
requiresApproval: true
The objective then writes a toolApprovalRequested event with the model’s arguments, which here are the code itself. Approve the call, and the toolCalled event reaches your worker:
cadenya objectives approve-tool-call <objective-id> <tool-call-id>
Or deny it with a memo that steers the agent:
cadenya objectives deny-tool-call <objective-id> <tool-call-id> \
--memo "Don't open network connections. Read the CSV from /tmp instead."
The worker doesn’t change. It only ever sees calls that a person approved.
The HTTP tool set variant, for teams with an endpoint
If you already host an HTTP service, an HTTP tool set sends each call to it directly, and your service runs the code. Cadenya sends tool set requests only to hosts on the account’s trusted endpoints list, so add yours first:
cadenya trusted-endpoints create --name sandbox-runner --host sandbox-runner.example.com
The tool set sets the base URL and a header that reads a tool set secret, so the token never appears in the prompt or in your repository:
metadata:
name: Sandbox runner
spec:
description: Runs Python in an isolated sandbox behind your own run-code endpoint
adapter:
type: http
http:
baseUrl: https://sandbox-runner.example.com
headers:
Authorization: "Bearer {{ secrets.RUN_CODE_TOKEN }}"
The tool keeps the same llmToolName and parameters as the Bare one, with an HTTP config instead. requestBodyTemplate builds the POST body, and {{ arguments | json }} sends the tool’s arguments as JSON:
config:
type: http
http:
requestMethod: POST
path: /run-code
requestBodyContentType: application/json
requestBodyTemplate: "{{ arguments | json }}"
Apply both, then store the token as a tool set secret. Cadenya encrypts it at rest and decrypts it only when a call uses it:
cadenya config apply
cadenya tool-sets secrets create <tool-set-id> --name RUN_CODE_TOKEN --value "$RUN_CODE_TOKEN"
The endpoint checks the token, runs the code in a fresh Daytona sandbox with the same runner, and kills the sandbox before it replies. In http-variant/server.ts:
import { timingSafeEqual } from "node:crypto";
import { createServer, type ServerResponse } from "node:http";
import { createSandbox, providerFromEnv, type RunResult } from "../src/sandboxes/index.js";
const token = process.env.RUN_CODE_TOKEN;
if (!token) throw new Error("Set RUN_CODE_TOKEN");
const expected = Buffer.from(`Bearer ${token}`);
const provider = providerFromEnv();
const port = Number(process.env.PORT ?? 8787);
function authorized(header: string | undefined): boolean {
const given = Buffer.from(header ?? "");
return given.length === expected.length && timingSafeEqual(given, expected);
}
function reply(res: ServerResponse, status: number, body: RunResult | { error: string }) {
res.writeHead(status, { "content-type": "application/json" });
res.end(JSON.stringify(body));
}
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/run-code") return reply(res, 404, { error: "not found" });
if (!authorized(req.headers.authorization)) return reply(res, 401, { error: "unauthorized" });
let raw = "";
for await (const chunk of req) raw += chunk;
let code: unknown;
try {
code = JSON.parse(raw || "{}").code;
} catch {
return reply(res, 400, { error: "the body must be JSON" });
}
if (typeof code !== "string" || code.trim() === "") return reply(res, 400, { error: "code must be a non-empty string" });
try {
const sandbox = await createSandbox(provider, { variant: "http" }, `run-code-http-${Date.now().toString(36)}`);
try {
reply(res, 200, await sandbox.runPython(code));
} finally {
await sandbox.kill();
}
} catch (error) {
reply(res, 502, { error: `sandbox error: ${(error as Error).message}` });
}
}).listen(port);
A call from your own machine shows what the agent gets back:
curl -s -X POST http://127.0.0.1:8787/run-code \
-H "Authorization: Bearer $RUN_CODE_TOKEN" -H 'content-type: application/json' \
-d '{"code":"import sys\nprint(sum(range(101)), sys.version.split()[0])"}'
You should see:
{"stdout":"5050 3.14.4\n","stderr":""}
And here’s what Cadenya sent for one call, as an echo service at the base URL received it on October 29, 2026, trimmed to the parts your endpoint reads:
{
"method": "POST",
"headers": {
"Authorization": "Bearer dummy-token-for-echo-test",
"Content-Type": "application/json",
"User-Agent": "Cadenya-Agent/1.0",
"X-Cadenya-Objective-Id": "obj_01M64N8E2SATMRXZD1MMJ6SP2H",
"X-Cadenya-Tool-Call-Id": "toolcall_01M64N8GE7ZRQE6MS1ARFBKGA4"
},
"json": {
"code": "print(6 * 7)"
}
}
The token came from the tool set secret, and the body from the template. This endpoint starts a new sandbox for each call and kills it before it replies. To keep files between calls, key the sandbox on X-Cadenya-Objective-Id, which names the objective on every call, or use the bare tool, which keeps one sandbox per objective.
What does Cadenya add to a Daytona sandbox?
A Daytona sandbox runs the code it’s handed. Cadenya is the agent loop around it, hosted, and the sandbox joins it as one tool. Here’s what that covers:
- The agent loop, with nothing to write or host. Cadenya calls the model, records each tool call, waits for your worker’s result, and calls the model again until the turn is done.
- A durable record of every run. Each objective keeps an event log of its loop, with every tool call and its result. List it after a run or stream it while the agent works.
- A person approves the code before it runs.
requiresApprovalparks the loop ontoolApprovalRequested, and a denial carries a memo that steers the agent. - Conversations that continue. When an objective is waiting, a Continue message picks the loop back up with the whole conversation. The sandbox keeps the files, and Cadenya keeps what was said about them.
- Your own API next to the sandbox. Point a tool set at your OpenAPI 3 spec, and every operation becomes a tool, re-checked each hour. The agent reads your data through your API, then analyzes it in the sandbox.
- Three ways to plug in a sandbox. A Bare tool answered by your worker, an HTTP tool set on your own endpoint, or an OpenAPI or MCP tool set, and any of them can require approval.
- Secrets that stay out of the prompt. Tool set secrets use envelope encryption with a unique data key for each value, and Cadenya decrypts them only at the point of use.
- Any model, set per variation. The model is a setting on a variation, and the weighted selection mode uses feedback to favor the variations that score well.
- The whole setup as files. The agent, its variation and its tools live in
.cadenya/, andcadenya config applyor the GitHub Action applies them. - SDKs in four languages. TypeScript, Python, Ruby and Go, all at 1.9.0, plus a CLI and a plain HTTP API.
- A price in one line. Free for 1,000 loops a month, $49 a month for 50,000, then $0.0015 a loop. A loop is one LLM request.
The tool sets guide shows Bare and HTTP tool sets in the app, and the free plan needs no card.
No account yet? A coding agent can create a temporary workspace with cadenya workspaces create --temporary, run its first objectives there, and hand you a link to claim it, as auth.md explains. To create an account, sign up at app.cadenya.com/signup, then copy your API key from the API keys page at app.cadenya.com/account/apiKeys.
Grow wherever AI goes next.
Start shipping agents that are equipped to evolve.