Skip to content

Add plumbing for TS/PY snippets in Sandboxes - #1778

Merged
jpwilliams merged 6 commits into
mainfrom
jack/sandbox-runner-scripts
Oct 15, 2026
Merged

jpwilliams merged 6 commits into
mainfrom
jack/sandbox-runner-scripts

Conversation

@jpwilliams

Copy link
Copy Markdown
Member

Summary

Add the plumbing for TypeScript and Python snippets to unlock higher-level APIs for running code. These allow submitted code to be within an existing process with some controlled globals etc which allow us to influence the environment enough to produce some nice future sugar.

This is the foundation that will allow for functionality such as:

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

Checklist

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

Pure builders for the commands that run a snippet with input as globals and a
top-level return as the result, plus the parser for the result sentinel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jpwilliams jpwilliams self-assigned this Oct 13, 2026
Copilot AI balanced review requested due to automatic review settings October 13, 2026 23:27
@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4443b03

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Copilot AI left a comment

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.

🟡 Changes recommended

Result framing, cyclic validation, and TypeScript transformation have correctness gaps.

4 open findings
What changed in this PR

Adds internal sandbox plumbing to execute TypeScript and Python snippets with JSON inputs and parsed results.

Changes:

  • Builds inline or file-backed runner commands.
  • Validates JSON inputs and global names.
  • Adds execution and parsing tests for both languages.
File Description
runnerScripts.ts Implements snippet runners, validation, and result parsing.
runnerScripts.test.ts Tests execution, payload handling, validation, and parsing.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts
Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Adds sandbox execution for TypeScript and Python snippets.

Fix result parsing before merging; successful snippets can fail when they print after returning.

Findings

  1. P1 Later logs break results ▶
  2. P2 Cyclic inputs exhaust the stack ▶
  3. P2 Empty array slots change ▶
Fix with agent prompt
### Issue 1
packages/inngest/src/components/sandbox/runnerScripts.ts:224-228
`parseRunnerOutput` treats everything after the result marker as JSON. A valid TypeScript snippet such as `setTimeout(() => console.log('later'), 10); return 1` prints its result before the timer fires. The command succeeds, but parsing `1\nlater` throws and loses the result. Read only the result's line and preserve later output in `stdout`, or write results separately from logs.

### Issue 2
packages/inngest/src/components/sandbox/runnerScripts.ts:156-157
`assertJson` follows nested objects and arrays without checking for cycles. Passing a self-referencing object through `input` exhausts the call stack and throws `RangeError` instead of `SandboxValidationError`. Track objects on the current recursion path and reject cycles with the same error used for other invalid JSON values.

### Issue 3
packages/inngest/src/components/sandbox/runnerScripts.ts:146-148
`assertJson` uses `forEach`, which skips empty array slots. An input such as `[1, , 3]` therefore passes, although an explicit `undefined` element is rejected. `JSON.stringify` then replaces the empty slot with `null`, silently changing the value the snippet sees. Check every array index and reject empty slots too.

```suggestion
    for (let index = 0; index < value.length; index++) {
      assertJson(
        Object.hasOwn(value, index) ? value[index] : undefined,
        `${path}[${index}]`,
      );
    }
```

---

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

Summary

Adds internal helpers for running TypeScript and Python snippets in sandboxes.

  • TypeScript and Python snippets can return values from async code.
  • Snippet inputs must use valid names and JSON values.

Later logs can break result parsing. Input checks also miss cycles and empty array slots.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Snippet and input] --> B[Check input]
  B --> C{Payload size}
  C -->|Small| D[Command argument]
  C -->|Large| E[Upload file]
  D --> F[Run language wrapper]
  E --> F
  F --> G[Logs and marked result]
  G --> H[Split logs from result]
Loading

Reviews (1) · Last reviewed commit: "feat(sandbox): add runner script builder..." · Reviewed by Greptile

Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
Comment thread packages/inngest/src/components/sandbox/runnerScripts.ts Outdated
jpwilliams and others added 2 commits October 13, 2026 23:37
…cles and sparse arrays

- TypeScript wrapper emits the result frame on clean exit, so output
  scheduled after the snippet returned no longer breaks parsing
- parseRunnerOutput takes the exit code and ignores the sentinel on failure
- TypeScript is transformed (enums etc.), not only stripped
- input validation rejects circular references and empty array slots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jpwilliams
jpwilliams added this pull request to stack #1785 October 14, 2026 19:09
jpwilliams and others added 3 commits October 14, 2026 19:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…26 removed

Enums and other syntax that needs transforming are no longer supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jpwilliams
jpwilliams merged commit 4f08f13 into main Oct 15, 2026
46 checks passed
@jpwilliams
jpwilliams deleted the jack/sandbox-runner-scripts 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.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.

`input`s become variables accessible by the code.

```ts
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.

```ts
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() }),
  });
};
```

```ts
// later in a run
const { mean } = await stats("get-stats", [1, 2, 3, 4]);
```

### Trace

<img width="1084" height="224" alt="image"
src="https://github.com/user-attachments/assets/da75f15a-b33e-479b-8fae-0d9f5cf029dc"
/>
<img width="1082" height="327" alt="image"
src="https://github.com/user-attachments/assets/d7a43855-f6d4-4e8d-bc2e-8ac0f904ac43"
/>


## 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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

4 participants