Skip to content

Add step.sandbox.python() - #1783

Merged
jpwilliams merged 47 commits into
jack/sandbox-runner-scriptsfrom
jack/sandbox-python-runner
Oct 15, 2026
Merged

jpwilliams merged 47 commits into
jack/sandbox-runner-scriptsfrom
jack/sandbox-python-runner

Conversation

@jpwilliams

@jpwilliams jpwilliams commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds step.sandbox.python(), a managed-lifecycle Sandbox that runs Python code and can take input.

Direct step usage

Use Python from TS, with the sandbox's lifecycle managed for you.

inputs become variables accessible by the code.

const stats = await step.sandbox.python("stats", `
  import statistics
   
  return {"mean": statistics.mean(values)}
`, {
  input: { values: [1, 2, 3, 4] },
});

Reusable functions

Create a reusable function usable during any run.

Output can also be typed and validated using any StandardSchema-compliant schema library.

import { step } from "inngest";
import { z } from "zod";

export const stats = async (id: string, values: number[]) => {
  return step.sandbox.python(id, `
    import statistics

    return {"mean": statistics.mean(values), "stdev": statistics.stdev(values)}
  `, {
    input: { values },
    schema: { output: z.object({ mean: z.number(), stdev: z.number() }),
  });
};
// later in a run
const { mean } = await stats("get-stats", [1, 2, 3, 4]);

Trace

image image

Checklist

  • Added a docs PR that references this PR N/A Later bump
  • Added unit/integration tests
  • Added changesets if applicable

Related

jpwilliams and others added 30 commits October 13, 2026 20:42
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Takes the span kind and title, so other scoped calls can reuse the
create, run and destroy lifecycle. No behaviour change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Runs a snippet with input as globals and a top-level return as the result.
step.sandbox.python is one create, run and destroy group of kind python; the
same method is on a created sandbox. A non-zero exit fails with stderr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A destroy step that fails for good no longer changes the run's outcome
or surfaces as a SuppressedError. The failed destroy attempt records an
inngest.warnings entry on its step instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jpwilliams jpwilliams self-assigned this Oct 14, 2026
@changeset-bot

changeset-bot Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e815586

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
inngest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@inngest-release-bot inngest-release-bot added the 📦 inngest Affects the `inngest` package label Oct 14, 2026
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] Adds Python code execution in sandboxes.

Fix silent loss of large Python results and the missing advertised schema support before merging.

Findings

  1. P1 Large Python results disappear ▶
  2. P1 Schema example does not work ▶
  3. P2 Invalid calls keep sandboxes alive ▶
Fix with agent prompt
### Issue 1
packages/inngest/src/components/sandbox/runners.ts:87-91
`runCode` ignores `output` from `commands.run` and parses potentially shortened `stdout`. For example, `return 'x' * 3000000` exceeds the 2 MiB limit. The saved output keeps only the tail, removing `__INNGEST_RESULT__`, so `parseRunnerOutput` returns `undefined`. The one-shot runner then destroys the sandbox and reports success with the wrong result.

Detect a missing result caused by truncation and fail clearly instead of treating it as no return value.

### Issue 2
packages/inngest/src/components/sandbox/runners.ts:24-29
`RunnerOptions` has no `schema` option, and `runCode` returns parsed JSON without checking an output schema. The PR's advertised `schema: { output: z.object(...) }` example therefore fails TypeScript's option check and cannot infer or validate its result.

Add the promised schema support and a test covering inference and invalid output.

### Issue 3
packages/inngest/src/components/sandbox/runners.ts:136-141
`createOneShotRunner` creates the sandbox before `buildRunner` checks the input and payload size. Invalid input or a payload over 16 KiB throws `SandboxValidationError`, which is retryable and does not trigger cleanup while attempts remain. The same invalid payload then fails on every retry while keeping the sandbox alive.

Build and validate the command before creating the sandbox to avoid unnecessary allocation and retries.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds step.sandbox.python() with managed sandbox creation and cleanup, plus python() on existing durable sandbox handles.

  • Sandbox callers can run Python and get its returned value.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["step.sandbox.python()"] --> B["Create sandbox"]
  B --> C["Build and validate command"]
  C --> D["Run durable command"]
  D --> E["Save output, keeping only its tail if too large"]
  E --> F["Check exit code and parse result"]
  F --> G["Destroy sandbox on success or final failure"]
  G --> H["Return result or throw"]
Loading

Reviews (1) · Last reviewed commit: "chore: drop changesets already released ..." · Reviewed by Greptile

Comment thread packages/inngest/src/components/sandbox/runners.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runners.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runners.ts Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jpwilliams jpwilliams mentioned this pull request Oct 14, 2026
2 of 3 tasks
@jpwilliams
jpwilliams added this pull request to stack #1785 October 14, 2026 19:09
jpwilliams and others added 14 commits October 14, 2026 19:17
…ail on truncated results

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntax test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n its step

The group is named for the user's step ID. Create, run, result and destroy
steps get display names and the SDK origin so a trace dims them, and the
result step parses and validates the file so its output is the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runners parse and validate the result file inside its step through an
internal helper, so the public files.read is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only step.sandbox.python remains. A user-built sandbox can't be assumed to
have the language installed, and a handle runner is stateless, so it offers
nothing over a one-shot plus commands.run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
options: Omit<OneShotRunnerOptions, "schema"> & {
schema: { output: TSchema };
},
): Promise<StandardSchemaV1.InferOutput<TSchema>>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we account for serialization in the return type here? with schema.output: z.coerce.date(), TS says we get a Date, but after replay it’s a string

i repro’d value.getUTCFullYear() compiling and then throwing at runtime with both step.sandbox.python() and step.sandbox.typescript(). should we apply the same output typing as step.run?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nice, aye we'll Jsonify here atm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jpwilliams
jpwilliams merged commit 167709c into main Oct 15, 2026
46 checks passed
@jpwilliams
jpwilliams deleted the jack/sandbox-python-runner branch October 15, 2026 18:13
jpwilliams added a commit that referenced this pull request Oct 15, 2026
## Summary
<!-- Succinctly describe your change, providing context, what you've
changed, and why. -->

Adds `step.sandbox.typescript()`, a managed-lifecycle Sandbox that runs
TypeScript code and can take `input`.

### Direct step usage

Run TS, with the sandbox's lifecycle managed for you.

`input`s become variables accessible by the code.

```ts
const stats = await step.sandbox.typescript("sum", `
  return numbers.reduce((a: number, b: number) => a + b, 0)
`, {
  input: { numbers: [1, 2, 3] },
});
```

### Reusable functions

Create a reusable function usable during any run.

Output can also be typed and validated using any
`StandardSchema`-compliant schema library.

```ts
import { step } from "inngest";
import { z } from "zod";

export const stats = async (id: string, values: number[]) => {
  return step.sandbox.typescript(id, `
    ...
  `, {
    input: { values },
    schema: { output: z.object({ words: z.number() }),
  });
};
```

```ts
// later in a run
const { words } = await stats("get-stats", event.data.body);
```

### Trace

<img width="1082" height="227" alt="image"
src="https://github.com/user-attachments/assets/3d1c8ad2-7f82-4756-b088-24844543b5fd"
/>
<img width="1086" height="323" alt="image"
src="https://github.com/user-attachments/assets/0524e5ab-c794-47a5-9f5d-a05fd6cfc58f"
/>


## Checklist
<!-- Tick these items off as you progress. -->
<!-- If an item isn't applicable, ideally please strikeout the item by
wrapping it in "~~"" and suffix it with "N/A My reason for skipping
this." -->
<!-- e.g. "- [ ] ~~Added tests~~ N/A Only touches docs" -->

- [ ] ~Added a [docs PR](https://github.com/inngest/website) that
references this PR~ N/A Later bump
- [x] Added unit/integration tests
- [x] Added changesets if applicable

## Related
<!-- A space for any related links, issues, or PRs. -->
<!-- Linear issues are autolinked. -->
<!-- e.g. - INN-123 -->
<!-- GitHub issues/PRs can be linked using shorthand. -->
<!-- e.g. "- inngest/inngest#123" -->
<!-- Feel free to remove this section if there are no applicable related
links.-->
- Based on #1778 and #1783

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📦 inngest Affects the `inngest` package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants