# MXC TypeScript SDK: One-Shot vs State-Aware API Patterns

> Explore the MXC TypeScript SDK's one-shot vs state-aware API patterns. Understand how to choose the right model for your sandbox execution needs. Maximize efficiency with the right API choice.

- Repository: [Microsoft/mxc](https://github.com/microsoft/mxc)
- Tags: deep-dive
- Published: 2026-06-07

---

**The MXC TypeScript SDK provides two distinct programming models for sandbox execution: the One-Shot API for ephemeral, run-and-forget workloads via `spawnSandbox`, and the State-Aware API for long-running, reusable sandboxes with explicit lifecycle management through methods like `provisionSandbox` and `execInSandbox`.**

The microsoft/mxc repository ships a TypeScript SDK that abstracts container-style sandbox execution behind two fundamentally different architectural patterns. While both APIs ultimately invoke the same native `mxc` binary through `resolveBinaryAndCommonArgs` in [`helper.ts`](https://github.com/microsoft/mxc/blob/main/helper.ts), they differ significantly in resource management, error handling, and type safety guarantees.

## Understanding the One-Shot API Pattern

The One-Shot API, implemented in **[`sdk/src/sandbox.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/sandbox.ts)**, is designed for scenarios where sandbox provisioning time is acceptable for each individual invocation and lifecycle management is unnecessary.

### Core Implementation and Entry Points

The primary entry points are **`spawnSandbox`** and **`spawnSandboxFromConfig`**. These functions construct a single envelope describing the entire request—including the command line, filesystem policies, network configuration, and UI settings—and pass it directly to the native binary. The binary creates the sandbox, streams output via a `ChildProcess` or PTY interface, and automatically tears down the environment when the process exits.

Error handling in this pattern is process-level only. Failures surface as non-zero exit codes on the returned process object, without structured error deserialization or detailed exception metadata.

### When to Use the One-Shot Pattern

Choose this pattern for:

- **Quick "run-and-forget" scripts** where the overhead of sandbox creation per call is acceptable
- **Simple CI steps** where spin-up time dominates and persistence is unnecessary
- **Scenarios where you do not need to keep a sandbox alive** between distinct command executions

### One-Shot Implementation Example

```typescript
import { spawnSandbox, SandboxSpawnOptions } from "mxc/sdk";

// Run `echo hello` inside a temporary sandbox
const options: SandboxSpawnOptions = {
  // optional: limit resources, set env vars, etc.
};
const pty = spawnSandbox(
  {
    // Minimal sandbox config – filesystem/network/ UI omitted for brevity
    commandLine: "echo hello",
  },
  options,
);

// Pipe live output to the console
pty.onData((data) => process.stdout.write(data));
pty.onExit(({ exitCode }) => console.log(`Exited with ${exitCode}`));

```

*Source: `spawnSandbox` implementation lives in **[`sdk/src/sandbox.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/sandbox.ts)**.*

## Understanding the State-Aware API Pattern

The State-Aware API, located in **[`sdk/src/state-aware.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware.ts)**, exposes a granular, five-phase lifecycle for long-running sandboxes that require stable identity and repeated execution.

### Lifecycle Management Methods

Unlike the monolithic One-Shot approach, the State-Aware API breaks operations into discrete async helpers:

- **`provisionSandbox`** – Sends a `phase: "provision"` envelope and returns a `ProvisionResult` containing a branded `SandboxId` and backend-specific metadata
- **`startSandbox`** – Sends a `phase: "start"` envelope identified by the sandbox ID
- **`execInSandbox`** / **`execInSandboxAsync`** – Sends a `phase: "exec"` envelope with a `ProcessConfig`; the async variant buffers stdout/stderr and parses error envelopes
- **`stopSandbox`** – Sends a `phase: "stop"` envelope; the sandbox remains provisioned for later restarts
- **`deprovisionSandbox`** – Sends a `phase: "deprovision"` envelope; invalidates the sandbox ID permanently

All envelopes are constructed by **`buildStateAwareEnvelope`** in **[`sdk/src/state-aware-helper.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware-helper.ts)** and dispatched via `nonExecCall` (for non-exec phases) or `spawnAndCollect` / PTY spawning (for exec phases).

### Type Safety and Backend Branding

A critical advantage of the State-Aware API is its compile-time type safety. The SDK defines a branded type **`SandboxId<C>`** in **[`sdk/src/state-aware-types.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware-types.ts)**, where the generic `C` represents the specific backend (e.g., `"isolation_session"`). This branding ensures that configuration objects using conditional types like `ProvisionConfigFor<C>` or `StartConfigFor<C>` cannot be mismatched across different backends, preventing configuration errors at compile time rather than runtime.

### Structured Error Handling with MxcError

Unlike the One-Shot pattern's process-level failures, the State-Aware API deserializes errors into strongly-typed **`MxcError`** objects. The `execInSandboxAsync` method specifically captures error envelopes containing the original error code, message, and details, throwing typed exceptions that enable robust programmatic error handling for stateful services.

### State-Aware Implementation Example

```typescript
import {
  provisionSandbox,
  startSandbox,
  execInSandboxAsync,
  stopSandbox,
  deprovisionSandbox,
} from "mxc/sdk";
import type { SandboxId } from "mxc/sdk/src/state-aware-types";

// 1️⃣ Provision a sandbox (here we use the isolation‑session backend)
const prov = await provisionSandbox("isolation_session", {
  filesystem: { readonly: true },
});
const sandboxId: SandboxId<"isolation_session"> = prov.sandboxId;

// 2️⃣ Start the sandbox
await startSandbox(sandboxId);

// 3️⃣ Execute a command (buffered version)
const execResult = await execInSandboxAsync(sandboxId, {
  process: { commandLine: "echo state‑aware" },
});
console.log(execResult.stdout.trim()); // → “state-aware”

// 4️⃣ Stop (optional – allows later restart)
await stopSandbox(sandboxId);

// 5️⃣ De‑provision when finished
await deprovisionSandbox(sandboxId);

```

*Key source files:*
- **Lifecycle methods** – **[`sdk/src/state-aware.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware.ts)**
- **Type definitions** – **[`sdk/src/state-aware-types.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware-types.ts)**
- **Envelope construction** – **[`sdk/src/state-aware-helper.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware-helper.ts)**

## Architectural Comparison

Both APIs utilize the underlying **`mxc`** native binary, but differ in execution semantics:

- **Resource Model**: **One-Shot** creates and destroys a sandbox for every call, incurring provisioning overhead per execution. **State-Aware** provisions once, allowing multiple start/exec/stop cycles to amortize provisioning costs across many commands.

- **Return Types**: **One-Shot** returns streaming `ChildProcess` objects suitable for real-time I/O. **State-Aware** returns structured objects like `ProvisionResult`, `StartResult`, and `ExecResult` containing sandbox identifiers and metadata.

- **Error Semantics**: **One-Shot** relies on exit codes. **State-Aware** provides deserialized `MxcError` objects with full error context through `execInSandboxAsync`.

- **Type Safety**: Only the **State-Aware** API uses branded `SandboxId<C>` types and backend-specific configuration interfaces to prevent cross-backend contamination at compile time.

## Summary

- The **One-Shot API** in [`sdk/src/sandbox.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/sandbox.ts) provides ephemeral execution via `spawnSandbox` and `spawnSandboxFromConfig` with automatic cleanup, ideal for short-lived workloads.
- The **State-Aware API** in [`sdk/src/state-aware.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware.ts) offers granular lifecycle control through `provisionSandbox`, `startSandbox`, `execInSandbox*`, `stopSandbox`, and `deprovisionSandbox`.
- Both patterns ultimately invoke the native `mxc` binary through `resolveBinaryAndCommonArgs` as implemented in [`helper.ts`](https://github.com/microsoft/mxc/blob/main/helper.ts).
- State-Aware sandboxes use strongly-typed `SandboxId<C>` branding defined in [`state-aware-types.ts`](https://github.com/microsoft/mxc/blob/main/state-aware-types.ts) to enforce backend-specific configuration safety.
- Choose One-Shot for CI steps and isolated commands; choose State-Aware for persistent services, repeated executions, and scenarios requiring stable sandbox identity such as Entra-backed isolation sessions.

## Frequently Asked Questions

### When should I use the One-Shot API versus the State-Aware API?

Use the **One-Shot API** when you need to execute a single command without managing sandbox lifetime, such as in CI pipelines or simple automation scripts where provisioning overhead is acceptable per invocation. Choose the **State-Aware API** when you need to reuse a sandbox across multiple commands, maintain state between executions, or when working with backends like Entra-backed isolation sessions that require a stable sandbox identity across distinct lifecycle phases.

### How does error handling differ between the two patterns?

The **One-Shot API** surfaces failures only as process-level exit codes on the returned `ChildProcess` or PTY object. In contrast, the **State-Aware API**'s `execInSandboxAsync` method deserializes error envelopes into structured **`MxcError`** objects containing specific error codes, messages, and details, enabling programmatic error handling rather than parsing shell output or checking exit codes.

### What is the purpose of the branded SandboxId type?

The **`SandboxId<C>`** type in [`sdk/src/state-aware-types.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware-types.ts) uses TypeScript's type system to "brand" a sandbox ID with its specific backend configuration type (e.g., `"isolation_session"`). This compile-time guarantee ensures that you cannot accidentally pass configuration options meant for one backend to a sandbox provisioned for another, preventing subtle runtime configuration mismatches.

### Can I switch from One-Shot to State-Aware execution for an existing sandbox?

No, these patterns are mutually exclusive. One-Shot sandboxes created via `spawnSandbox` are transient by design and are immediately deprovisioned upon process exit. To utilize State-Aware lifecycle management, you must explicitly provision a new sandbox using `provisionSandbox` and manage its entire lifecycle—from provisioning through deprovisioning—using the discrete phase methods in [`sdk/src/state-aware.ts`](https://github.com/microsoft/mxc/blob/main/sdk/src/state-aware.ts).