---
title: "Run agent-generated code in a Blaxel sandbox (2026): a bare run_code tool, a TypeScript worker, Blaxel's SDK and an HTTP tool set with Cadenya"
url: "https://cadenya.com/other-content/running-agent-code-in-blaxel"
description: "How to give an AI agent a run_code tool backed by a Blaxel sandbox: a Cadenya bare tool answered by your own worker through the event stream, with tested TypeScript for Blaxel's SDK, config files applied with the Cadenya CLI, a human approval before the code runs, and an HTTP tool set for teams that already host an endpoint."
---

To run agent-generated code in a Blaxel 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 Blaxel sandbox with Blaxel’s 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 Blaxel 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](/other-content/running-agent-code-in-e2b) and [Daytona](/other-content/running-agent-code-in-daytona), and [Sandboxes for running agent-generated code](/other-content/running-agent-generated-code-safely) compares the three on isolation, start time and price. [What Cadenya adds to a Blaxel sandbox](#what-does-cadenya-add-to-a-blaxel-sandbox) is at the end.

Every SDK version and limit below comes from Blaxel’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:

1.  The agent calls `run_code` with a Python program as its `code` argument.
2.  Cadenya records the call, writes a `toolCalled` event to the objective, and pauses the loop until the call gets content.
3.  Your worker reads that event from the objective’s event stream, creates a sandbox on the first call, and runs the code.
4.  The worker sends stdout, stderr and any error back with `setToolCallContent`, and the agent reads them on its next loop.

**The Blaxel 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](https://github.com/cadenya/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 Blaxel’s SDK:

```
npm install @cadenya/cadenya @blaxel/core
```

The worker talks to the sandbox through one small interface, so it stays the same on [E2B](/other-content/running-agent-code-in-e2b) and [Daytona](/other-content/running-agent-code-in-daytona). 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:

1.  **Polling.** List an objective’s tool calls with `executionStatus` set to `TOOL_CALL_EXECUTION_STATUS_WAITING_FOR_CONTENT`, which returns the Bare calls still waiting for a result.
2.  **Webhooks.** Cadenya sends `objective_event.tool_called` to the agent’s webhook URL for every call, with the tool, its config and the parsed arguments.

## The Blaxel sandbox

Blaxel’s SDK reads its credentials only from the environment, a `.env` file or a CLI login. In `src/sandboxes/blaxel.ts`:

```
import { SandboxInstance } from "@blaxel/core";
import type { CodeSandbox, SandboxLabels } from "./types.js";

// Reads BL_API_KEY and BL_WORKSPACE (and BL_REGION, if set) from the environment.
export async function createBlaxelSandbox(labels: SandboxLabels, name: string): Promise<CodeSandbox> {
  // Blaxel deletes the sandbox when its TTL ends, so a crashed worker
  // never leaves one behind for long.
  const sandbox = await SandboxInstance.create({
    name,
    image: "blaxel/base-image:latest",
    memory: 2048,
    region: process.env.BL_REGION ?? "us-pdx-1",
    labels,
    ttl: "30m",
  });

  return {
    provider: "blaxel",
    id: name,
    async runPython(code) {
      await sandbox.fs.write("/tmp/run_code.py", code);
      const run = await sandbox.process.exec({
        command: "python3 /tmp/run_code.py",
        waitForCompletion: true,
        timeout: 55,
      });
      return {
        stdout: run.stdout ?? "",
        stderr: run.stderr ?? "",
        error: run.exitCode === 0 ? undefined : `exit code ${run.exitCode}`,
      };
    },
    async kill() {
      await SandboxInstance.delete(name);
    },
  };
}
```

`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 Blaxel:

1.  Blaxel addresses a sandbox by its name, so the worker names it after the objective.
2.  `blaxel/base-image:latest` has Node.js 22 and Python 3.12.
3.  A call with `waitForCompletion` waits 60 seconds at most. For longer programs, start the process and wait on it separately.
4.  The shortest `ttl` Blaxel takes is `5m`, and deleting a sandbox erases its data at once.

## 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 `createBlaxelSandbox` when `SANDBOX_PROVIDER` is `blaxel`. Set `BL_API_KEY` and `BL_WORKSPACE`, then run it:

```
SANDBOX_PROVIDER=blaxel npx tsx src/run.ts
```

You should see the agent’s answer. This one comes from a run on Blaxel 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

count = 0
total = 0
n = 2
while count < 1000:
    if is_prime(n):
        count += 1
        total += n
        if count == 1000:
            print("1000th prime:", n)
            print("Sum of first 1000 primes:", total)
    n += 1
```

And here’s what the worker sent back:

```
{"stdout":"1000th prime: 7919\nSum of first 1000 primes: 3682913\n","stderr":""}
```

Blaxel’s base image ran Python 3.12.15.

## 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 Blaxel 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])"}'
```

It answers with the same JSON the worker sends back, with `stdout`, `stderr` and any `error`.

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 Blaxel sandbox?

A Blaxel sandbox runs the code it’s handed. **Cadenya runs the agent loop around it, so there’s no loop to write, and the sandbox joins it as one tool.** Here’s what that covers:

1.  **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.
2.  **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.
3.  **A person approves the code before it runs.** `requiresApproval` parks the loop on `toolApprovalRequested`, and a denial carries a memo that steers the agent.
4.  **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.
5.  **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.
6.  **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.
7.  **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.
8.  **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.
9.  **The whole setup as files.** The agent, its variation and its tools live in `.cadenya/`, and `cadenya config apply` or the GitHub Action applies them.
10.  **SDKs in four languages.** TypeScript, Python, Ruby and Go, all at 1.9.0, plus a CLI and a plain HTTP API.
11.  **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](/docs/guides/the-basics/tool-sets#bare-tool-sets) 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](https://cadenya.com/auth.md) explains. To create an account, sign up at [app.cadenya.com/signup](https://app.cadenya.com/signup?utm_source=cadenya.com&utm_medium=article&utm_campaign=running-agent-code-in-blaxel), then copy your API key from the API keys page at `app.cadenya.com/account/apiKeys`.