diff --git a/preview-features/on-demand-sandboxes/README.md b/preview-features/on-demand-sandboxes/README.md
new file mode 100644
index 00000000..1fe1630a
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/README.md
@@ -0,0 +1,245 @@
+# On-demand Sandboxes for Azure Durable Task Scheduler
+
+> **Status:** Private preview
+
+## Get private preview access
+
+To gain access to the private preview, email [dts-team@microsoft.com](mailto:dts-team@microsoft.com).
+
+## Overview
+
+A *sandbox* is an isolated, microVM-backed container that runs a single piece of your
+workflow with its own runtime, dependencies, and security boundary—separate from your
+orchestrator's process.
+
+On-demand Sandboxes let you move individual workflow steps (activities) out of your
+orchestrator process and into managed, isolated compute, while your orchestrator stays
+exactly where it is. You tell Durable Task Scheduler (DTS) which activities should run
+in isolation and provide a container image with that activity code; DTS handles
+provisioning, scaling, and teardown.
+
+Most activities belong in-process: they're fast, simple, and co-located with your
+orchestrator. But some steps don't fit that model—they need a native binary, a
+different language runtime, per-invocation isolation, or bursty compute you don't want
+to keep warm. On-demand Sandboxes handle those exceptions without dedicated
+infrastructure or custom scaling policies.
+
+## Why it's valuable
+
+- **Activity-level granularity.** Move individual steps to managed compute, not your
+ whole app.
+- **Per-activity or per-invocation isolation.** Each execution runs in a clean,
+ microVM-backed sandbox—ideal for untrusted code, customer plugins, or LLM-generated
+ logic.
+- **Cross-runtime flexibility.** Run a Python inference step from a .NET orchestrator,
+ with no compromise on either side.
+- **Scale-to-zero.** Pay for CPU and memory per second of execution, not for
+ infrastructure that sits idle.
+- **No orchestrator changes.** Your orchestration code and hosting model don't change
+ at all.
+
+## Prerequisites
+
+Before you begin, make sure you have:
+
+- **Private preview access.** On-demand Sandboxes is in private preview.
+ [Sign up here](https://techcommunity.microsoft.com/blog/AppsonAzureBlog/introducing-on-demand-sandboxes-for-azure-durable-task-scheduler-private-preview/4522333)
+ to have the feature enabled on your scheduler.
+- **An app using a supported standalone Durable Task SDK.** On-demand Sandboxes target
+ the standalone Durable Task SDKs used *outside* the Azure Functions host—apps running
+ on Azure Container Apps, Azure Kubernetes Service, App Service, or anywhere else you
+ self-host. The private preview supports the **.NET** and **Python** SDKs; additional
+ language SDKs and Azure Functions support are coming soon.
+- **A provisioned Durable Task Scheduler** configured as the durable backend for your app,
+ in one of the supported preview regions.
+- **A container registry** (for example, Azure Container Registry) where you can push
+ the worker image that contains your sandboxed activity code.
+- **User-assigned managed identities** that DTS uses to pull your worker image from your
+ registry and start the sandbox. The scheduler must have the identity attached, and the
+ image-pull identity needs the **AcrPull** role on your registry. You provide the client
+ IDs on the worker profile (the image-pull identity via `Image.ManagedIdentityClientId` /
+ `image.managed_identity_client_id`, and the worker/scheduler identity via
+ `SchedulerManagedIdentityClientId` / `scheduler_managed_identity_client_id`). See
+ [Configure the scheduler identity for image pull](#configure-the-scheduler-identity-for-image-pull).
+
+## How it works
+
+On-demand Sandboxes use a two-part model:
+
+1. A **sandbox worker profile** in your orchestrator app that tells DTS which activities
+ to offload.
+2. A **worker image** that contains those activity implementations.
+
+Your orchestrator still calls activities the same way it always has. The decision to run
+an activity in a sandbox lives entirely in the profile configuration.
+
+### A simple example
+
+Imagine an orchestrator that does two things: format some text in-process, then run a
+piece of customer-supplied Python in isolation. Only the second activity is declared in a
+sandbox worker profile, so DTS runs it in a managed sandbox started from your worker
+image—while the first activity stays in-process. The result flows back to the
+orchestrator as if nothing special happened.
+
+```mermaid
+flowchart LR
+ subgraph YourApp["Your orchestrator app"]
+ Orch["Orchestrator"]
+ InProc["FormatText activity
(runs in-process)"]
+ end
+
+ DTS["Durable Task Scheduler
(durable backend)"]
+
+ subgraph Sandbox["Managed sandbox (started by DTS)"]
+ Worker["RunPython activity
(your worker image)"]
+ end
+
+ Orch -->|"in-process call"| InProc
+ Orch -->|"call RunPython"| DTS
+ DTS -->|"start sandbox &
dispatch activity"| Worker
+ Worker -->|"result"| DTS
+ DTS -->|"result"| Orch
+```
+
+1. The orchestrator runs `FormatText` in-process, like any normal activity.
+2. When it calls `RunPython`—an activity declared in a sandbox worker profile—DTS starts a
+ sandbox from your worker image and dispatches the activity to it.
+3. The activity runs in the isolated sandbox, and its result flows back through DTS to the
+ orchestrator. When the work is done, DTS tears the sandbox down.
+
+## Configure the scheduler identity for image pull
+
+To start a sandbox, DTS pulls your worker image from your container registry on your
+behalf. It does this using a **user-assigned managed identity** attached to the scheduler.
+That identity must be granted the **AcrPull** role on the Azure Container Registry that
+hosts your worker image, and the scheduler must have the identity attached.
+
+> [!IMPORTANT]
+> Only **user-assigned** managed identities are supported. System-assigned managed
+> identities are not supported at this time.
+
+The worker profile distinguishes two identities, and you can use the same identity for
+both or split them:
+
+- **Image-pull identity** (`Image.ManagedIdentityClientId` /
+ `image.managed_identity_client_id`) — the identity DTS uses to **pull the worker image**
+ from your registry. This identity needs the **AcrPull** role on the registry.
+- **Worker/scheduler identity** (`SchedulerManagedIdentityClientId` /
+ `scheduler_managed_identity_client_id`) — the identity the **sandbox worker uses to
+ connect back to Durable Task Scheduler**, and the identity your activity code runs as
+ when it calls other services (for example, Storage, Key Vault, or a database). Grant
+ this identity whatever roles your activity code needs on those downstream services.
+
+Both identities must be attached to the scheduler. Using two separate identities lets you
+scope image-pull permissions narrowly while granting your activity code only the
+downstream permissions it needs.
+
+### 1. Grant the identity the AcrPull role on your registry
+
+Assign the **AcrPull** role to the **image-pull** user-assigned managed identity, scoped
+to your registry:
+
+```bash
+az role assignment create \
+ --assignee "" \
+ --role "AcrPull" \
+ --scope "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/"
+```
+
+Without this role assignment, DTS cannot pull the worker image and the sandbox will fail
+to start. If your activity code calls other Azure services, grant the **worker/scheduler**
+identity the roles it needs on those services as well.
+
+### 2. Attach the identity to the scheduler
+
+The scheduler must have the user-assigned identity attached. You can attach it when you
+create the scheduler, or update an existing scheduler.
+
+> [!IMPORTANT]
+> Managing scheduler identities requires API version **2026-05-01-preview** or later. See
+> the [Schedulers - Create Or Update](https://learn.microsoft.com/rest/api/durabletask/schedulers/create-or-update?view=rest-durabletask-2026-05-01-preview&tabs=HTTP#managedserviceidentity)
+> REST API reference.
+
+**For an existing scheduler**, send a PATCH to the scheduler resource URI. You can attach
+multiple identities:
+
+```bash
+az rest --method patch \
+ --uri "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.DurableTask/schedulers/?api-version=2026-05-01-preview" \
+ --body '{
+ "identity": {
+ "type": "UserAssigned",
+ "userAssignedIdentities": {
+ "/subscriptions//resourceGroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/": {}
+ }
+ }
+ }'
+```
+
+You can also include the same `identity` block directly in the body when **creating** a
+scheduler.
+
+Once the identities are attached to the scheduler—the image-pull identity with the
+**AcrPull** role on your registry—reference their client IDs on the worker profile
+(`Image.ManagedIdentityClientId` / `image.managed_identity_client_id` and
+`SchedulerManagedIdentityClientId` / `scheduler_managed_identity_client_id`) so DTS uses
+the image-pull identity to pull the image and the worker/scheduler identity for the
+sandbox worker to connect back to DTS and call downstream services.
+
+## Choose your language
+
+Follow the step-by-step guide for your SDK:
+
+- **[.NET guide](./docs/dotnet.md)** — declare a sandbox worker profile and build the worker
+ image with the .NET Durable Task SDK.
+- **[Python guide](./docs/python.md)** — declare a sandbox worker profile and build the worker
+ image with the Python Durable Task SDK.
+
+Both guides follow the same shape: declare a sandbox worker profile in your orchestrator
+app, build and push a worker image, then view execution logs in the DTS dashboard.
+
+## Worker profile configuration reference
+
+Both languages configure the same worker profile settings. The table below lists each
+setting, what it controls, its accepted values, and its default. The setting names differ
+slightly between .NET (`PascalCase`) and Python (`snake_case`) but map one to one.
+
+| Setting (.NET / Python) | What it controls | Accepted values | Default |
+| --- | --- | --- | --- |
+| `Image.ImageRef` / `image.image_ref` | The container image that holds your activity implementations. | A full OCI image reference, by tag (`myregistry.azurecr.io/workers/hello:1.0`) or digest (`myregistry.azurecr.io/workers/hello@sha256:...`). | *Required* |
+| `Image.ManagedIdentityClientId` / `image.managed_identity_client_id` | The client ID of the user-assigned managed identity DTS uses to **pull the worker image** from your registry. This identity needs the **AcrPull** role on the registry. | A user-assigned managed identity client ID (GUID). Must be attached to the scheduler. | *Required* |
+| `SchedulerManagedIdentityClientId` / `scheduler_managed_identity_client_id` | The client ID of the user-assigned managed identity the **sandbox worker uses to connect back to DTS**, and that the activity code runs as when calling other services. | A user-assigned managed identity client ID (GUID). Must be attached to the scheduler. Can be the same identity as the image-pull identity or a different one. | *Required* |
+| `Cpu` / `cpu` | CPU quantity declared for each sandbox. | A positive CPU quantity, expressed in millicores (`500m`, `1000m`) or whole/fractional cores (`2`, `0.5`). | `1000m` (1 vCPU) |
+| `Memory` / `memory` | Memory quantity declared for each sandbox. | A positive memory quantity, such as `256Mi`, `1Gi`, or a bare number interpreted as MiB (`2048`). | `2048Mi` |
+| `MaxConcurrentActivities` / `max_concurrent_activities` | How many activities a single sandbox worker instance processes concurrently. | An integer greater than `0`. There is no enforced upper bound; size it to what your activity and resource shape can handle. | `100` |
+| `EnvironmentVariables` / `environment_variables` | Customer environment variables injected into the sandbox at runtime. | A map of string keys to string values. | Empty |
+| *(profile id)* | Friendly profile id that groups the image, resources, and activities for monitoring and reuse. | A non-empty string, unique across your declared profiles. | `default` |
+| `AddActivity` / `add_activity` | The activity names this profile offloads to the sandbox. | One or more activity names. At least one is required; an activity can belong to only one profile. | *Required* |
+
+> [!NOTE]
+> CPU and memory must be positive resource quantities. The platform may apply additional
+> per-preview ceilings on the total CPU and memory a sandbox can request—check your
+> private preview onboarding details for the current limits.
+
+## View logs in the DTS dashboard
+
+Once your sandbox activities are running, you can view their execution logs directly in
+the Durable Task Scheduler dashboard. The dashboard shows real-time output from your
+managed workers, including stdout, stderr, and activity lifecycle events—giving you full
+visibility into what's happening inside the sandbox without configuring external log
+sinks or building your own observability pipeline.
+
+## Get started
+
+On-demand Sandboxes is in private preview. To get access,
+[sign up here](https://techcommunity.microsoft.com/blog/AppsonAzureBlog/introducing-on-demand-sandboxes-for-azure-durable-task-scheduler-private-preview/4522333).
+Once you're in, the workflow is straightforward: declare a sandbox worker profile in
+your orchestrator app, build and push a worker image, and DTS takes care of the rest.
+
+## Related resources
+
+- **Documentation:** [Durable Task Scheduler overview](https://learn.microsoft.com/azure/durable-task/)
+- **Samples:** [.NET sample](./samples/dotnet) · [Python sample](./samples/python)
+- **Pricing:** [Azure Durable Task Scheduler pricing](https://azure.microsoft.com/pricing/)
+- **Feedback:** Open an issue in the
+ [Durable-Task-Scheduler GitHub repo](https://github.com/Azure-Samples/Durable-Task-Scheduler).
diff --git a/preview-features/on-demand-sandboxes/docs/dotnet.md b/preview-features/on-demand-sandboxes/docs/dotnet.md
new file mode 100644
index 00000000..76e3229c
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/docs/dotnet.md
@@ -0,0 +1,182 @@
+# On-demand Sandboxes — .NET guide
+
+> **Status:** Private preview · [Back to overview](./README.md)
+
+This guide walks through using On-demand Sandboxes with the **.NET** Durable Task SDK.
+Make sure you've reviewed the [prerequisites](./README.md#prerequisites) first.
+
+On-demand Sandboxes use a two-part model: a **sandbox worker profile** in your
+orchestrator app that tells DTS which activities to offload, and a **worker image** that
+contains those activity implementations. Your orchestrator still calls activities the
+same way it always has—the decision to run one in a sandbox lives entirely in the profile
+configuration.
+
+## Install the preview packages
+
+The on-demand sandbox APIs ship in two opt-in preview packages that layer on top of the
+Azure-managed client and worker packages:
+
+- `Microsoft.DurableTask.Client.AzureManaged.Sandboxes` — declarer-app side
+ (`[SandboxWorkerProfile]`, `SandboxWorkerProfileOptions`, `SandboxActivitiesClient`).
+- `Microsoft.DurableTask.Worker.AzureManaged.Sandboxes` — sandbox-worker side
+ (`UseSandboxWorker()`).
+
+Add the client and worker packages to your orchestrator app, and the worker package to
+the sandbox worker image project:
+
+```bash
+# Orchestrator / declarer app
+dotnet add package Microsoft.DurableTask.Client.AzureManaged.Sandboxes --version 1.25.0-preview.2
+dotnet add package Microsoft.DurableTask.Worker.AzureManaged.Sandboxes --version 1.25.0-preview.2
+
+# Sandbox worker image project
+dotnet add package Microsoft.DurableTask.Worker.AzureManaged.Sandboxes --version 1.25.0-preview.2
+```
+
+## Step 1 — Declare a sandbox worker profile
+
+In the app that hosts your orchestrator, define a sandbox worker profile. The profile
+gives DTS the container image of your activity code, the managed identities DTS uses to
+pull the image and start the sandbox, the resource shape, concurrency setting, and the
+activity names that should run in a sandbox.
+
+```csharp
+using Microsoft.DurableTask.Client.AzureManaged;
+
+[SandboxWorkerProfile("")]
+internal sealed class CodeSandboxWorkerProfile : ISandboxWorkerProfile
+{
+ public void Configure(SandboxWorkerProfileOptions options)
+ {
+ options.Image.ImageRef = Environment.GetEnvironmentVariable("DTS_SANDBOX_CONTAINER_IMAGE")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_CONTAINER_IMAGE is required.");
+ options.Image.ManagedIdentityClientId = Environment.GetEnvironmentVariable("DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID is required.");
+ options.SchedulerManagedIdentityClientId = Environment.GetEnvironmentVariable("DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID is required.");
+ options.Cpu = "1000m";
+ options.Memory = "2048Mi";
+ options.MaxConcurrentActivities = 1;
+ options.AddActivity(TaskNames.ExecuteCode, version: "");
+ }
+}
+```
+
+> [!IMPORTANT]
+> The managed identities referenced by `options.Image.ManagedIdentityClientId` and
+> `options.SchedulerManagedIdentityClientId` must both be attached to the scheduler. The
+> image-pull identity must have the **AcrPull** role on your container registry, and the
+> worker/scheduler identity must have whatever roles your activity code needs on the
+> downstream services it calls. You can use the same identity for both or split them. See
+> [Configure the scheduler identity for image pull](./README.md#configure-the-scheduler-identity-for-image-pull).
+
+Then, in the main app, enable work-item filters, register the sandbox activities client,
+and declare the profiles with DTS:
+
+```csharp
+builder.Services.AddDurableTaskWorker(workerBuilder =>
+{
+ workerBuilder.AddTasks(tasks => tasks.AddAllGeneratedTasks());
+ workerBuilder.UseWorkItemFilters();
+ workerBuilder.UseDurableTaskScheduler(options =>
+ {
+ options.EndpointAddress = endpoint;
+ options.TaskHubName = taskHub;
+ options.Credential = credential;
+ });
+});
+
+// Profiles are declared via [SandboxWorkerProfile]. This registers the client that
+// publishes them to DTS.
+builder.Services.AddDurableTaskSchedulerSandboxActivitiesClient();
+```
+
+`UseWorkItemFilters()` is required: without it, DTS can dispatch a sandbox activity to
+your in-process worker—which doesn't implement it—and the orchestration gets stuck
+retrying the wrong worker.
+
+Once the host is running, publish the declared profiles to DTS so it can route their
+activities to a sandbox:
+
+```csharp
+SandboxActivitiesClient sandboxActivitiesClient =
+ host.Services.GetRequiredService();
+await sandboxActivitiesClient.EnableSandboxActivitiesAsync();
+```
+
+`EnableSandboxActivitiesAsync()` is the key call—it registers your sandbox worker profiles
+with DTS so it picks them up and routes their declared activities to managed compute.
+Without it, those activities won't be offloaded.
+
+For the meaning, accepted values, and defaults of each `SandboxWorkerProfileOptions`
+setting, see the
+[worker profile configuration reference](./README.md#worker-profile-configuration-reference).
+In short: `Image.ImageRef` is the image with your activity implementations;
+`Image.ManagedIdentityClientId` is the managed identity DTS uses to **pull the worker
+image** from your registry (needs **AcrPull**), while `SchedulerManagedIdentityClientId`
+is the managed identity the **sandbox worker uses to connect back to DTS** and that the
+activity code runs as when it calls other services; `Cpu` / `Memory` set the per-sandbox
+resource shape; `MaxConcurrentActivities` sets concurrency; and `AddActivity` selects the
+activities to offload (only added activities run in DTS-managed isolated compute;
+everything else stays in-process).
+
+The orchestrator call site doesn't change:
+
+```csharp
+ExecuteCodeOutput execution = await context.CallActivityAsync(
+ TaskNames.ExecuteCode,
+ new ExecuteCodeInput(pythonCode, input.CsvData));
+```
+
+Because `ExecuteCode` is not registered in the main app's in-process activity list, DTS
+uses the profile to route the work to the sandbox image when the orchestrator calls it.
+
+## Step 2 — Build the worker image
+
+The worker image is a container you own. In most apps, this worker lives in a separate
+project from the orchestrator host so it can have its own entry point, dependencies, and
+container image. It registers the activity implementations it can run and opts in to
+managed execution with `UseSandboxWorker()`:
+
+```csharp
+builder.Services.AddDurableTaskWorker(workerBuilder =>
+{
+ workerBuilder.AddTasks(tasks =>
+ {
+ tasks.AddActivity();
+ });
+
+ workerBuilder.UseSandboxWorker();
+});
+```
+
+`UseSandboxWorker()` is the key line—it signals that this worker runs in DTS-managed
+compute. The sandbox worker does **not** need to configure the DTS endpoint, task hub,
+profile id, or credentials; DTS injects the runtime settings when it starts the
+container.
+
+The activity implementations themselves are standard Durable Task activities. There's
+nothing special about the activity code—it can call a runtime with different
+dependencies (for example, Python and pandas) while running in an isolated container
+instead of in your main app's process.
+
+Package the image like any containerized service, including whatever runtimes and native
+tools the activity needs. Push it to your container registry (for example, Azure
+Container Registry) and reference the image in the worker profile's `Image.ImageRef`
+option. The image-pull identity you set in `Image.ManagedIdentityClientId` must have the
+**AcrPull** role on that registry.
+
+## Step 3 — View logs in the DTS dashboard
+
+Once your sandbox activities are running, you can view their execution logs directly in
+the Durable Task Scheduler dashboard. See
+[View logs in the DTS dashboard](./README.md#view-logs-in-the-dts-dashboard) in the
+overview for details.
+
+## Next steps
+
+- [Worker profile configuration reference](./README.md#worker-profile-configuration-reference)
+- [Configure the scheduler identity for image pull](./README.md#configure-the-scheduler-identity-for-image-pull)
+- [End-to-end .NET sample](../samples/dotnet)
+- [Python guide](./python.md)
+- [Back to overview](./README.md)
diff --git a/preview-features/on-demand-sandboxes/docs/python.md b/preview-features/on-demand-sandboxes/docs/python.md
new file mode 100644
index 00000000..ae168d84
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/docs/python.md
@@ -0,0 +1,218 @@
+# On-demand Sandboxes — Python guide
+
+> **Status:** Private preview · [Back to overview](./README.md)
+
+This guide walks through using On-demand Sandboxes with the **Python** Durable Task SDK.
+Make sure you've reviewed the [prerequisites](./README.md#prerequisites) first.
+
+On-demand Sandboxes use a two-part model: a **sandbox worker profile** (the *declarer
+app*) that tells DTS which activities to offload, and a **worker image** that contains
+those activity implementations. Your orchestrator still calls activities the same way it
+always has—the decision to run one in a sandbox lives entirely in the profile
+configuration.
+
+## Install the SDK
+
+The on-demand sandbox APIs ship under the `durabletask.azuremanaged.preview.sandboxes`
+namespace. Install the Durable Task packages:
+
+```bash
+pip install durabletask==1.6.0 durabletask-azuremanaged==1.6.0
+```
+
+## Step 1 — Declare a sandbox worker profile
+
+The declarer app uses a decorated profile class to declare the remote worker image and
+activity ownership, then enables sandbox activities on the DTS client. The profile sets
+the image, the managed identities DTS needs to pull the image and start the sandbox, the
+resource shape, concurrency, any customer environment variables, and the activity names
+to offload with `options.add_activity(...)`.
+
+```python
+import os
+
+from azure.identity import DefaultAzureCredential
+
+from durabletask import client, task
+from durabletask.azuremanaged.client import DurableTaskSchedulerClient
+from durabletask.azuremanaged.preview.sandboxes import (
+ SandboxActivitiesClient,
+ SandboxWorkerProfile,
+ sandbox_worker_profile,
+)
+from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
+
+REMOTE_HELLO = "remote_hello"
+
+endpoint = os.environ["DTS_ENDPOINT"]
+taskhub_name = os.environ["DTS_TASK_HUB"]
+worker_profile_id = os.getenv("DTS_WORKER_PROFILE_ID", "default")
+container_image = os.environ["DTS_SANDBOX_CONTAINER_IMAGE"]
+
+
+def hello_orchestrator(ctx: task.OrchestrationContext, name: str):
+ """Orchestrator that calls an activity executed by the remote sandbox worker."""
+ return (yield ctx.call_activity(REMOTE_HELLO, input=name))
+
+
+@sandbox_worker_profile(worker_profile_id)
+class RemoteWorkerProfile(SandboxWorkerProfile):
+ def configure(self, options) -> None:
+ options.image.image_ref = container_image
+ options.image.managed_identity_client_id = os.environ[
+ "DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID"]
+ options.scheduler_managed_identity_client_id = os.environ[
+ "DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID"]
+ options.cpu = "1000m"
+ options.memory = "2048Mi"
+ options.max_concurrent_activities = 1
+ options.environment_variables["SAMPLE_MARKER"] = "python-sample-marker"
+ options.add_activity(REMOTE_HELLO)
+
+
+credential = DefaultAzureCredential()
+
+# Declare the sandbox worker profile with DTS so it can route the activity to a sandbox.
+sandbox_client = SandboxActivitiesClient(
+ host_address=endpoint,
+ secure_channel=True,
+ taskhub=taskhub_name,
+ token_credential=credential)
+sandbox_client.enable_sandbox_activities()
+
+with DurableTaskSchedulerWorker(
+ host_address=endpoint,
+ secure_channel=True,
+ taskhub=taskhub_name,
+ token_credential=credential) as worker:
+ worker.add_orchestrator(hello_orchestrator)
+ worker.use_work_item_filters()
+ worker.start()
+
+ durable_client = DurableTaskSchedulerClient(
+ host_address=endpoint,
+ secure_channel=True,
+ taskhub=taskhub_name,
+ token_credential=credential)
+ instance_id = durable_client.schedule_new_orchestration(
+ hello_orchestrator, input="on-demand sandbox Python")
+ state = durable_client.wait_for_orchestration_completion(instance_id, timeout=300)
+ print(state.serialized_output if state else "no result")
+```
+
+`enable_sandbox_activities()` is the key call—it registers the declared profiles with DTS
+so it can route those activities to the sandbox image. `use_work_item_filters()` keeps
+sandbox activities from being dispatched to this in-process worker.
+
+> [!IMPORTANT]
+> The managed identities referenced by `options.image.managed_identity_client_id` and
+> `options.scheduler_managed_identity_client_id` must both be attached to the scheduler.
+> The image-pull identity must have the **AcrPull** role on your container registry, and
+> the worker/scheduler identity must have whatever roles your activity code needs on the
+> downstream services it calls (you can use the same identity for both or split them). See
+> [Configure the scheduler identity for image pull](./README.md#configure-the-scheduler-identity-for-image-pull).
+
+For the meaning, accepted values, and defaults of each profile option, see the
+[worker profile configuration reference](./README.md#worker-profile-configuration-reference).
+In short: `image.image_ref` is the image with your activity implementations;
+`image.managed_identity_client_id` is the managed identity DTS uses to **pull the worker
+image** from your registry (needs **AcrPull**), while
+`scheduler_managed_identity_client_id` is the managed identity the **sandbox worker uses
+to connect back to DTS** and that the activity code runs as when it calls other services;
+`cpu` / `memory` set the per-sandbox resource shape; `max_concurrent_activities` sets
+concurrency; `environment_variables` injects customer environment variables; and
+`add_activity(...)` selects the activities to offload (only added activities run in
+DTS-managed isolated compute; everything else stays in-process).
+
+The orchestrator call site doesn't change—it calls `REMOTE_HELLO` the same way it would
+call any activity, and DTS routes it to the sandbox.
+
+## Step 2 — Build the worker image
+
+The worker image runs `SandboxWorker()`, registers the activity implementations it owns,
+and starts. The sandbox worker does **not** configure the DTS endpoint, task hub, profile
+id, or credentials—`SandboxWorker()` reads the runtime settings (such as `DTS_ENDPOINT`,
+`DTS_TASK_HUB`, `DTS_WORKER_PROFILE_ID`, and `DTS_SANDBOX_ID`) from environment variables
+that DTS injects when it starts the container.
+
+```python
+import os
+import threading
+
+from durabletask import task
+from durabletask.azuremanaged.preview.sandboxes import SandboxWorker
+
+REMOTE_HELLO = "remote_hello"
+
+
+def _remote_hello(ctx: task.ActivityContext, name: str) -> str:
+ """Activity function that runs inside the on-demand sandbox worker container."""
+ sandbox_id = os.getenv("DTS_SANDBOX_ID", "unknown-sandbox")
+ return f"Hello {name} from Python on-demand sandbox worker {sandbox_id}!"
+
+
+# The registered activity name must match the name declared in the worker profile.
+_remote_hello.__name__ = REMOTE_HELLO
+
+with SandboxWorker() as worker:
+ worker.add_activity(_remote_hello)
+ worker.start()
+ print("Python on-demand sandbox remote worker is running.")
+ try:
+ threading.Event().wait()
+ except KeyboardInterrupt:
+ pass
+```
+
+Keep the activity name constant (here, `REMOTE_HELLO`) in a small shared module so the
+declarer app and the remote worker stay in sync. When the worker connects, it reports its
+registered activity names, and DTS validates they match the declaration before
+advertising worker capacity.
+
+Build and push the image with a `Containerfile`/`Dockerfile` that installs the SDK and
+your activity's dependencies, then copies in the worker entry point:
+
+```dockerfile
+# syntax=docker/dockerfile:1.7
+FROM python:3.12-slim AS runtime
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+ENV GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/etc/ssl/certs/ca-certificates.crt
+
+# Install the Durable Task SDK (with the sandboxes extension), plus your
+# activity dependencies.
+RUN pip install --no-cache-dir durabletask==1.6.0 durabletask-azuremanaged==1.6.0
+
+COPY remote_worker.py /app/remote_worker.py
+COPY activities.py /app/activities.py
+
+ENTRYPOINT ["python", "/app/remote_worker.py"]
+```
+
+```bash
+docker build -f Containerfile -t .
+docker push
+```
+
+Then set the image reference on the declarer profile (for example, via the
+`DTS_SANDBOX_CONTAINER_IMAGE` environment variable). DTS pulls the image using the
+image-pull managed identity you configured on the profile, which must have the
+**AcrPull** role on your registry.
+
+## Step 3 — View logs in the DTS dashboard
+
+Once your sandbox activities are running, you can view their execution logs directly in
+the Durable Task Scheduler dashboard. See
+[View logs in the DTS dashboard](./README.md#view-logs-in-the-dts-dashboard) in the
+overview for details.
+
+## Next steps
+
+- [Worker profile configuration reference](./README.md#worker-profile-configuration-reference)
+- [Configure the scheduler identity for image pull](./README.md#configure-the-scheduler-identity-for-image-pull)
+- [End-to-end Python sample](../samples/python)
+- [.NET guide](./dotnet.md)
+- [Back to overview](./README.md)
diff --git a/preview-features/on-demand-sandboxes/samples/README.md b/preview-features/on-demand-sandboxes/samples/README.md
new file mode 100644
index 00000000..089651ef
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/README.md
@@ -0,0 +1,19 @@
+# On-demand Sandboxes: LLM-generated code interpreter demo
+
+A Durable Task Scheduler (DTS) demo of the **On-demand Sandboxes** preview, built
+in two languages. A three-step workflow asks a natural-language question over a
+CSV: an LLM generates a pandas script, the **untrusted** script runs in a
+DTS-managed on-demand sandbox (fanned out per region), and an in-process step
+aggregates the answer.
+
+| Directory | Implementation | SDK |
+| --- | --- | --- |
+| [`dotnet/`](dotnet/README.md) | .NET 10 | `Microsoft.DurableTask.*.AzureManaged.Sandboxes` 1.25.0-preview.2 |
+| [`python/`](python/README.md) | Python 3.12 | `durabletask-azuremanaged` 1.6.0 |
+
+Both implementations follow the same shape: a main/declarer app hosts the
+orchestrator and in-process activities and declares the sandbox worker profile,
+while a separate sandbox worker image runs the offloaded `ExecuteCode` /
+`execute_code` activity in DTS-provisioned compute.
+
+See each directory's README for prerequisites, build, and run instructions.
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/Directory.Build.props b/preview-features/on-demand-sandboxes/samples/dotnet/Directory.Build.props
new file mode 100644
index 00000000..68c0dd96
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/Directory.Build.props
@@ -0,0 +1,13 @@
+
+
+
+
+ 1.25.0-preview.2
+
+ 2.1.0-preview.2
+
+
+
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/README.md b/preview-features/on-demand-sandboxes/samples/dotnet/README.md
new file mode 100644
index 00000000..58dfaf0f
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/README.md
@@ -0,0 +1,234 @@
+# On-demand Sandboxes demo: LLM-generated code interpreter
+
+A three-step Durable Task workflow that demonstrates the **On-demand Sandboxes** preview
+of Azure Durable Task Scheduler (DTS).
+
+```
+ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
+ │ GenerateCode │ │ ExecuteCode │ │ FormatAnswer │
+ │ (in-process .NET) │ -> │ (on-demand sandbox) │ -> │ (in-process .NET) │
+ │ Azure OpenAI -> Python │ │ python3 + pandas │ │ Pretty-print answer │
+ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘
+```
+
+The orchestrator asks a natural-language question over `data/sales_q1.csv`. The LLM
+returns a self-contained pandas script. That script is **untrusted** code, so it runs in
+a DTS-managed on-demand sandbox - not in the orchestrator's process. The first and last
+activities stay in-process; only `ExecuteCode` is offloaded.
+
+## Why this is a fit for On-demand Sandboxes
+
+- The generated Python is arbitrary code. It should not run in the orchestrator host.
+- The sandbox needs a different runtime (Python + pandas) than the orchestrator (.NET).
+- Each invocation gets a fresh container. No cross-request state to worry about.
+- Bursty by nature - a question every few minutes, but each one is short-lived.
+
+## Architecture
+
+```mermaid
+flowchart LR
+ subgraph MainAppHost["main-app process (.NET, always running)"]
+ direction TB
+ Orch["AnalyzeSales
Orchestrator"]
+ GenCode["GenerateCode
(in-process activity)"]
+ FormatAns["FormatAnswer
(in-process activity)"]
+ end
+
+ subgraph DTS["Azure Durable Task Scheduler"]
+ direction TB
+ TaskHub[("Task Hub")]
+ Sandboxes["On-demand sandbox runtime"]
+ end
+
+ subgraph Sandbox["DTS-managed sandbox container
(code-executor profile, on-demand)"]
+ ExecCode["ExecuteCode
(on-demand sandbox activity)"]
+ end
+
+ Orch <--> TaskHub
+ GenCode <--> TaskHub
+ FormatAns <--> TaskHub
+
+ TaskHub -.->|"ExecuteCode declared on-demand"| Sandboxes
+ Sandboxes ==>|"provision on demand /
scale to zero"| Sandbox
+ ExecCode <--> TaskHub
+```
+
+**How it works:**
+
+- The orchestrator and its in-process activities (`GenerateCode`, `FormatAnswer`) run in the always-on `main-app` process and exchange work items with the DTS task hub.
+- `ExecuteCode` is declared as an on-demand sandbox activity by the `code-executor` worker profile (see `main-app/WorkerProfiles.cs`). The activity is never registered in the main app.
+- When the orchestrator calls `ExecuteCode`, the DTS on-demand sandbox runtime provisions a sandbox container from the profile's image. The sandbox picks up the work item, runs it, returns the result, and is scaled back to zero when idle.
+- The orchestrator's call site (`CallActivityAsync(TaskNames.ExecuteCode, ...)`) is identical to any other activity call — the "this runs in a sandbox" decision lives entirely in the worker profile declaration.
+
+## Layout
+
+```
+dts-ondemand-sandbox-codegen-demo/
+├── Directory.Build.props # Pins the DTS preview package version
+├── data/sales_q1.csv # Sample dataset (~35 rows)
+├── azure.yaml # azd service + hooks (Deploy to Azure)
+├── infra/ # Bicep: AKS, ACR, identity, Azure OpenAI, scheduler wiring
+├── scripts/ # acr-build.sh + attach-scheduler-identity.sh (azd hooks)
+├── main-app/ # Orchestrator host (.NET 10), deployed to AKS
+│ ├── Program.cs
+│ ├── AnalyzeSalesOrchestrator.cs
+│ ├── Activities.cs # GenerateCode + FormatAnswer (in-process)
+│ ├── Contracts.cs
+│ ├── TaskNames.cs
+│ ├── Containerfile # main-app image
+│ └── manifests/ # K8s deployment template
+└── sandbox-worker/ # Built into the sandbox container image
+ ├── Program.cs # UseSandboxWorker()
+ ├── ExecuteCodeActivity.cs # Shells out to python3
+ ├── Contracts.cs
+ └── Containerfile
+```
+
+## Prerequisites
+
+- .NET 10 SDK
+- Docker (for building the sandbox image)
+- A DTS scheduler + task hub you can hit
+- An Azure Container Registry with anonymous pull enabled (so DTS can fetch the sandbox image)
+- An Azure OpenAI deployment of a chat model (GPT-4o, GPT-4.1, etc.)
+- The Durable Task on-demand sandbox preview packages (`1.25.0-preview.2`) available on
+ a NuGet feed you can restore from
+
+## Build the sandbox image
+
+From the demo root:
+
+```bash
+ACR=
+IMAGE=$ACR.azurecr.io/dts-codegen-sandbox:v1
+
+docker build \
+ --platform linux/amd64 \
+ -f sandbox-worker/Containerfile \
+ -t $IMAGE \
+ .
+
+# Enable anonymous pull so DTS can fetch the sandbox image without credentials
+az acr update --name $ACR --anonymous-pull-enabled true
+
+az acr login --name $ACR
+docker push $IMAGE
+```
+
+> **Note on `--platform linux/amd64`:** Required on Apple Silicon. The `Grpc.Tools`
+> 2.78.0 linux_arm64 `protoc` binary segfaults under Docker's arm64 emulation.
+> amd64 builds work fine under Rosetta and match what DTS sandboxes run anyway.
+
+## Run the orchestrator
+
+```bash
+export DTS_ENDPOINT="https://"
+export DTS_TASK_HUB=""
+export DTS_SANDBOX_CONTAINER_IMAGE=".azurecr.io/dts-codegen-sandbox:v1"
+export DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID=""
+export DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID=""
+
+export AOAI_ENDPOINT="https://.openai.azure.com"
+export AOAI_DEPLOYMENT=""
+
+# Sign in so DefaultAzureCredential can reach DTS and Azure OpenAI
+az login
+
+dotnet run --project main-app/main-app.csproj -- \
+ "Which region had the highest total revenue in March 2025?"
+```
+
+The orchestrator prints the question, the orchestration id, and the final answer.
+The main-app console shows the AOAI-generated Python (prefixed `[generate]`) before
+it's handed off to the sandbox. The sandbox container logs (prefixed `[sandbox]`)
+stream through the DTS dashboard's **On-demand Sandboxes** tab while `ExecuteCode`
+runs — that's where you see the code, dataset load, execution timing, and script output.
+
+## Deploy to Azure (AKS) with `azd`
+
+The `infra/` folder and `azure.yaml` deploy the **main-app** orchestrator to **Azure
+Kubernetes Service** with [`azd`](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd).
+The sandbox worker image is built and pushed to ACR; DTS starts it on demand, so it is
+never deployed to the cluster.
+
+> The Durable Task Scheduler is **not created** by this template — you pass in an
+> existing one. On-demand Sandboxes is a private-preview feature that must be enabled on
+> the scheduler out of band, so the scheduler is patched separately and supplied here by
+> name.
+
+### What gets provisioned
+
+| Resource | Purpose |
+|----------|---------|
+| **AKS cluster** | Hosts the `main-app` orchestrator pod (workload identity enabled) |
+| **Azure Container Registry** | Stores the main-app and sandbox-worker images (built server-side via ACR Tasks) |
+| **User-assigned managed identity** + federated credential | Pod auth to DTS/Azure OpenAI, ACR pull for the sandbox, and the sandbox's connection back to DTS |
+| **Azure OpenAI** + `gpt-4o` deployment | Backs the in-process `GenerateCode` activity |
+| **VNet** | Network isolation for AKS |
+
+The deployment also **ensures the task hub** exists, grants the identity the roles it
+needs (AcrPull, Durable Task data access, Cognitive Services OpenAI User), and a
+`postprovision` hook **attaches the identity to your scheduler** (a merge-safe PATCH).
+
+### Prerequisites
+
+- An existing **DTS scheduler** with the On-demand Sandboxes preview enabled, and its
+ resource group name.
+- [Azure Developer CLI (`azd`)](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd), [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), and [kubectl](https://kubernetes.io/docs/tasks/tools/).
+- Azure OpenAI quota for `gpt-4o` in your target region.
+
+### Deploy
+
+```bash
+azd auth login && az login
+
+# Point the template at your existing (preview-enabled) scheduler.
+azd env set DTS_SCHEDULER_NAME ""
+azd env set DTS_SCHEDULER_RESOURCE_GROUP ""
+# Optional overrides: DTS_TASK_HUB (default: default), AZURE_OPENAI_LOCATION
+
+azd up
+```
+
+`azd` provisions the resources, builds both images via ACR Tasks, attaches the identity
+to your scheduler, and deploys the `main-app` pod. If you don't set `DTS_SCHEDULER_NAME`
+/ `DTS_SCHEDULER_RESOURCE_GROUP` first, `azd` prompts for them.
+
+### Verify
+
+```bash
+az aks get-credentials --resource-group --name # from `azd env get-values`
+kubectl get pods
+kubectl logs -l app=mainapp --tail=50
+```
+
+The `main-app` pod runs the orchestration; `[sandbox]` logs from `ExecuteCode` stream in
+the DTS dashboard's **On-demand Sandboxes** tab.
+
+### Clean up
+
+```bash
+azd down
+```
+
+This removes the resources the template created. Your scheduler is left untouched (it was
+not created here); detach the identity manually if you no longer need it.
+
+## Sample questions to try
+
+- `Which region had the highest total revenue in March 2025?`
+- `What was the best-selling product in Q1?`
+- `Average revenue per transaction in February?`
+- `Total units sold in the East region across the quarter?`
+
+## What's in-process vs on-demand sandbox
+
+| Activity | Runs where | Why |
+| --------------- | ------------------ | ------------------------------------------------------ |
+| GenerateCode | In-process | Plain Azure OpenAI HTTP call. No reason to split out. |
+| ExecuteCode | **Sandbox** | Untrusted LLM-generated code + different runtime. |
+| FormatAnswer | In-process | Trivial string formatting. |
+
+Only `ExecuteCode` is declared on the `code-executor` sandbox worker profile via
+`options.AddActivity(...)` (see `main-app/WorkerProfiles.cs`). Everything
+else runs wherever the orchestrator runs.
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/azure.yaml b/preview-features/on-demand-sandboxes/samples/dotnet/azure.yaml
new file mode 100644
index 00000000..c2200786
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/azure.yaml
@@ -0,0 +1,24 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+# On-demand Sandboxes (.NET) on Azure Kubernetes Service.
+#
+# Only the orchestrator (main-app) is deployed to AKS. The sandbox worker image is
+# built and pushed to ACR by the predeploy hook; DTS starts it on demand, so it is
+# not an azd service. Images are built server-side with ACR Tasks (az acr build) to
+# avoid local Docker builds (the Grpc.Tools arm64 protoc segfaults on Apple Silicon).
+
+metadata:
+ template: dts-ondemand-sandboxes-dotnet-aks
+name: dts-ondemand-sandboxes-dotnet
+hooks:
+ predeploy:
+ shell: bash
+ run: ./scripts/acr-build.sh
+ postprovision:
+ shell: bash
+ run: ./scripts/attach-scheduler-identity.sh
+services:
+ mainapp:
+ project: ./main-app
+ language: csharp
+ host: aks
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/data/sales_q1.csv b/preview-features/on-demand-sandboxes/samples/dotnet/data/sales_q1.csv
new file mode 100644
index 00000000..aa6d0ee8
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/data/sales_q1.csv
@@ -0,0 +1,301 @@
+date,region,product,units,revenue
+2025-01-01,West,Gadget,18,9000
+2025-01-01,East,Widget,25,5000
+2025-01-01,Central,Gizmo,32,9600
+2025-01-01,South,Gadget,39,19500
+2025-01-02,West,Widget,46,9200
+2025-01-02,East,Gizmo,53,15900
+2025-01-02,Central,Gadget,60,30000
+2025-01-03,South,Widget,24,4800
+2025-01-03,West,Gizmo,31,9300
+2025-01-03,East,Gadget,38,19000
+2025-01-03,Central,Widget,45,9000
+2025-01-04,South,Gizmo,52,15600
+2025-01-04,West,Gadget,59,29500
+2025-01-04,East,Widget,23,4600
+2025-01-05,Central,Gizmo,30,9000
+2025-01-05,South,Gadget,37,18500
+2025-01-05,West,Widget,44,8800
+2025-01-06,East,Gizmo,51,15300
+2025-01-06,Central,Gadget,58,29000
+2025-01-06,South,Widget,22,4400
+2025-01-06,West,Gizmo,29,8700
+2025-01-07,East,Gadget,36,18000
+2025-01-07,Central,Widget,43,8600
+2025-01-07,South,Gizmo,50,15000
+2025-01-08,West,Gadget,57,28500
+2025-01-08,East,Widget,21,4200
+2025-01-08,Central,Gizmo,28,8400
+2025-01-09,South,Gadget,35,17500
+2025-01-09,West,Widget,42,8400
+2025-01-09,East,Gizmo,49,14700
+2025-01-09,Central,Gadget,56,28000
+2025-01-10,South,Widget,20,4000
+2025-01-10,West,Gizmo,27,8100
+2025-01-10,East,Gadget,34,17000
+2025-01-11,Central,Widget,41,8200
+2025-01-11,South,Gizmo,48,14400
+2025-01-11,West,Gadget,55,27500
+2025-01-12,East,Widget,19,3800
+2025-01-12,Central,Gizmo,26,7800
+2025-01-12,South,Gadget,33,16500
+2025-01-12,West,Widget,40,8000
+2025-01-13,East,Gizmo,47,14100
+2025-01-13,Central,Gadget,54,27000
+2025-01-13,South,Widget,18,3600
+2025-01-14,West,Gizmo,25,7500
+2025-01-14,East,Gadget,32,16000
+2025-01-14,Central,Widget,39,7800
+2025-01-14,South,Gizmo,46,13800
+2025-01-15,West,Gadget,53,26500
+2025-01-15,East,Widget,60,12000
+2025-01-15,Central,Gizmo,24,7200
+2025-01-16,South,Gadget,31,15500
+2025-01-16,West,Widget,38,7600
+2025-01-16,East,Gizmo,45,13500
+2025-01-17,Central,Gadget,52,26000
+2025-01-17,South,Widget,59,11800
+2025-01-17,West,Gizmo,23,6900
+2025-01-17,East,Gadget,30,15000
+2025-01-18,Central,Widget,37,7400
+2025-01-18,South,Gizmo,44,13200
+2025-01-18,West,Gadget,51,25500
+2025-01-19,East,Widget,58,11600
+2025-01-19,Central,Gizmo,22,6600
+2025-01-19,South,Gadget,29,14500
+2025-01-20,West,Widget,36,7200
+2025-01-20,East,Gizmo,43,12900
+2025-01-20,Central,Gadget,50,25000
+2025-01-20,South,Widget,57,11400
+2025-01-21,West,Gizmo,21,6300
+2025-01-21,East,Gadget,28,14000
+2025-01-21,Central,Widget,35,7000
+2025-01-22,South,Gizmo,42,12600
+2025-01-22,West,Gadget,49,24500
+2025-01-22,East,Widget,56,11200
+2025-01-23,Central,Gizmo,20,6000
+2025-01-23,South,Gadget,27,13500
+2025-01-23,West,Widget,34,6800
+2025-01-23,East,Gizmo,41,12300
+2025-01-24,Central,Gadget,48,24000
+2025-01-24,South,Widget,55,11000
+2025-01-24,West,Gizmo,19,5700
+2025-01-25,East,Gadget,26,13000
+2025-01-25,Central,Widget,33,6600
+2025-01-25,South,Gizmo,40,12000
+2025-01-26,West,Gadget,47,23500
+2025-01-26,East,Widget,54,10800
+2025-01-26,Central,Gizmo,18,5400
+2025-01-26,South,Gadget,25,12500
+2025-01-27,West,Widget,32,6400
+2025-01-27,East,Gizmo,39,11700
+2025-01-27,Central,Gadget,46,23000
+2025-01-28,South,Widget,53,10600
+2025-01-28,West,Gizmo,60,18000
+2025-01-28,East,Gadget,24,12000
+2025-01-28,Central,Widget,31,6200
+2025-01-29,South,Gizmo,38,11400
+2025-01-29,West,Gadget,45,22500
+2025-01-29,East,Widget,52,10400
+2025-01-30,Central,Gizmo,59,17700
+2025-01-30,South,Gadget,23,11500
+2025-01-30,West,Widget,30,6000
+2025-01-31,East,Gizmo,37,11100
+2025-01-31,Central,Gadget,44,22000
+2025-01-31,South,Widget,51,10200
+2025-01-31,West,Gizmo,58,17400
+2025-02-01,East,Gadget,22,11000
+2025-02-01,Central,Widget,29,5800
+2025-02-01,South,Gizmo,36,10800
+2025-02-02,West,Gadget,43,21500
+2025-02-02,East,Widget,50,10000
+2025-02-02,Central,Gizmo,57,17100
+2025-02-03,South,Gadget,21,10500
+2025-02-03,West,Widget,28,5600
+2025-02-03,East,Gizmo,35,10500
+2025-02-03,Central,Gadget,42,21000
+2025-02-04,South,Widget,49,9800
+2025-02-04,West,Gizmo,56,16800
+2025-02-04,East,Gadget,20,10000
+2025-02-05,Central,Widget,27,5400
+2025-02-05,South,Gizmo,34,10200
+2025-02-05,West,Gadget,41,20500
+2025-02-06,East,Widget,48,9600
+2025-02-06,Central,Gizmo,55,16500
+2025-02-06,South,Gadget,19,9500
+2025-02-06,West,Widget,26,5200
+2025-02-07,East,Gizmo,33,9900
+2025-02-07,Central,Gadget,40,20000
+2025-02-07,South,Widget,47,9400
+2025-02-08,West,Gizmo,54,16200
+2025-02-08,East,Gadget,18,9000
+2025-02-08,Central,Widget,25,5000
+2025-02-08,South,Gizmo,32,9600
+2025-02-09,West,Gadget,39,19500
+2025-02-09,East,Widget,46,9200
+2025-02-09,Central,Gizmo,53,15900
+2025-02-10,South,Gadget,60,30000
+2025-02-10,West,Widget,24,4800
+2025-02-10,East,Gizmo,31,9300
+2025-02-11,Central,Gadget,38,19000
+2025-02-11,South,Widget,45,9000
+2025-02-11,West,Gizmo,52,15600
+2025-02-11,East,Gadget,59,29500
+2025-02-12,Central,Widget,23,4600
+2025-02-12,South,Gizmo,30,9000
+2025-02-12,West,Gadget,37,18500
+2025-02-13,East,Widget,44,8800
+2025-02-13,Central,Gizmo,51,15300
+2025-02-13,South,Gadget,58,29000
+2025-02-14,West,Widget,22,4400
+2025-02-14,East,Gizmo,29,8700
+2025-02-14,Central,Gadget,36,18000
+2025-02-14,South,Widget,43,8600
+2025-02-15,West,Gizmo,50,15000
+2025-02-15,East,Gadget,57,28500
+2025-02-15,Central,Widget,21,4200
+2025-02-16,South,Gizmo,28,8400
+2025-02-16,West,Gadget,35,17500
+2025-02-16,East,Widget,42,8400
+2025-02-17,Central,Gizmo,49,14700
+2025-02-17,South,Gadget,56,28000
+2025-02-17,West,Widget,20,4000
+2025-02-17,East,Gizmo,27,8100
+2025-02-18,Central,Gadget,34,17000
+2025-02-18,South,Widget,41,8200
+2025-02-18,West,Gizmo,48,14400
+2025-02-19,East,Gadget,55,27500
+2025-02-19,Central,Widget,19,3800
+2025-02-19,South,Gizmo,26,7800
+2025-02-20,West,Gadget,33,16500
+2025-02-20,East,Widget,40,8000
+2025-02-20,Central,Gizmo,47,14100
+2025-02-20,South,Gadget,54,27000
+2025-02-21,West,Widget,18,3600
+2025-02-21,East,Gizmo,25,7500
+2025-02-21,Central,Gadget,32,16000
+2025-02-22,South,Widget,39,7800
+2025-02-22,West,Gizmo,46,13800
+2025-02-22,East,Gadget,53,26500
+2025-02-22,Central,Widget,60,12000
+2025-02-23,South,Gizmo,24,7200
+2025-02-23,West,Gadget,31,15500
+2025-02-23,East,Widget,38,7600
+2025-02-24,Central,Gizmo,45,13500
+2025-02-24,South,Gadget,52,26000
+2025-02-24,West,Widget,59,11800
+2025-02-25,East,Gizmo,23,6900
+2025-02-25,Central,Gadget,30,15000
+2025-02-25,South,Widget,37,7400
+2025-02-25,West,Gizmo,44,13200
+2025-02-26,East,Gadget,51,25500
+2025-02-26,Central,Widget,58,11600
+2025-02-26,South,Gizmo,22,6600
+2025-02-27,West,Gadget,29,14500
+2025-02-27,East,Widget,36,7200
+2025-02-27,Central,Gizmo,43,12900
+2025-02-28,South,Gadget,50,25000
+2025-02-28,West,Widget,57,11400
+2025-02-28,East,Gizmo,21,6300
+2025-02-28,Central,Gadget,28,14000
+2025-03-01,South,Widget,35,7000
+2025-03-01,West,Gizmo,60,18000
+2025-03-01,East,Gadget,53,26500
+2025-03-02,Central,Widget,58,11600
+2025-03-02,South,Gizmo,20,6000
+2025-03-02,West,Gadget,45,22500
+2025-03-03,East,Widget,38,7600
+2025-03-03,Central,Gizmo,43,12900
+2025-03-03,South,Gadget,48,24000
+2025-03-03,West,Widget,73,14600
+2025-03-04,East,Gizmo,23,6900
+2025-03-04,Central,Gadget,28,14000
+2025-03-04,South,Widget,33,6600
+2025-03-05,West,Gizmo,58,17400
+2025-03-05,East,Gadget,51,25500
+2025-03-05,Central,Widget,56,11200
+2025-03-05,South,Gizmo,18,5400
+2025-03-06,West,Gadget,43,21500
+2025-03-06,East,Widget,36,7200
+2025-03-06,Central,Gizmo,41,12300
+2025-03-07,South,Gadget,46,23000
+2025-03-07,West,Widget,71,14200
+2025-03-07,East,Gizmo,64,19200
+2025-03-08,Central,Gadget,26,13000
+2025-03-08,South,Widget,31,6200
+2025-03-08,West,Gizmo,56,16800
+2025-03-08,East,Gadget,49,24500
+2025-03-09,Central,Widget,54,10800
+2025-03-09,South,Gizmo,59,17700
+2025-03-09,West,Gadget,41,20500
+2025-03-10,East,Widget,34,6800
+2025-03-10,Central,Gizmo,39,11700
+2025-03-10,South,Gadget,44,22000
+2025-03-11,West,Widget,69,13800
+2025-03-11,East,Gizmo,62,18600
+2025-03-11,Central,Gadget,24,12000
+2025-03-11,South,Widget,29,5800
+2025-03-12,West,Gizmo,54,16200
+2025-03-12,East,Gadget,47,23500
+2025-03-12,Central,Widget,52,10400
+2025-03-13,South,Gizmo,57,17100
+2025-03-13,West,Gadget,39,19500
+2025-03-13,East,Widget,32,6400
+2025-03-14,Central,Gizmo,37,11100
+2025-03-14,South,Gadget,42,21000
+2025-03-14,West,Widget,67,13400
+2025-03-14,East,Gizmo,60,18000
+2025-03-15,Central,Gadget,22,11000
+2025-03-15,South,Widget,27,5400
+2025-03-15,West,Gizmo,52,15600
+2025-03-16,East,Gadget,45,22500
+2025-03-16,Central,Widget,50,10000
+2025-03-16,South,Gizmo,55,16500
+2025-03-17,West,Gadget,37,18500
+2025-03-17,East,Widget,30,6000
+2025-03-17,Central,Gizmo,35,10500
+2025-03-17,South,Gadget,40,20000
+2025-03-18,West,Widget,65,13000
+2025-03-18,East,Gizmo,58,17400
+2025-03-18,Central,Gadget,20,10000
+2025-03-19,South,Widget,25,5000
+2025-03-19,West,Gizmo,50,15000
+2025-03-19,East,Gadget,43,21500
+2025-03-19,Central,Widget,48,9600
+2025-03-20,South,Gizmo,53,15900
+2025-03-20,West,Gadget,78,39000
+2025-03-20,East,Widget,28,5600
+2025-03-21,Central,Gizmo,33,9900
+2025-03-21,South,Gadget,38,19000
+2025-03-21,West,Widget,63,12600
+2025-03-22,East,Gizmo,56,16800
+2025-03-22,Central,Gadget,61,30500
+2025-03-22,South,Widget,23,4600
+2025-03-22,West,Gizmo,48,14400
+2025-03-23,East,Gadget,41,20500
+2025-03-23,Central,Widget,46,9200
+2025-03-23,South,Gizmo,51,15300
+2025-03-24,West,Gadget,76,38000
+2025-03-24,East,Widget,26,5200
+2025-03-24,Central,Gizmo,31,9300
+2025-03-25,South,Gadget,36,18000
+2025-03-25,West,Widget,61,12200
+2025-03-25,East,Gizmo,54,16200
+2025-03-25,Central,Gadget,59,29500
+2025-03-26,South,Widget,21,4200
+2025-03-26,West,Gizmo,46,13800
+2025-03-26,East,Gadget,39,19500
+2025-03-27,Central,Widget,44,8800
+2025-03-27,South,Gizmo,49,14700
+2025-03-27,West,Gadget,74,37000
+2025-03-28,East,Widget,24,4800
+2025-03-28,Central,Gizmo,29,8700
+2025-03-28,South,Gadget,34,17000
+2025-03-28,West,Widget,59,11800
+2025-03-29,East,Gizmo,52,15600
+2025-03-29,Central,Gadget,57,28500
+2025-03-29,South,Widget,19,3800
+2025-03-30,West,Gizmo,44,13200
+2025-03-30,East,Gadget,37,18500
+2025-03-30,Central,Widget,42,8400
+2025-03-31,South,Gizmo,47,14100
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/abbreviations.json b/preview-features/on-demand-sandboxes/samples/dotnet/infra/abbreviations.json
new file mode 100644
index 00000000..10424d3d
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/abbreviations.json
@@ -0,0 +1,17 @@
+{
+ "analysisServicesServers": "as",
+ "apiManagementService": "apim-",
+ "appConfigurationStores": "appcs-",
+ "appManagedEnvironments": "cae-",
+ "appContainerApps": "ca-",
+ "authorizationPolicyDefinitions": "policy-",
+ "automationAutomationAccounts": "aa-",
+ "containerRegistryRegistries": "cr",
+ "containerServiceManagedClusters": "aks-",
+ "networkVirtualNetworks": "vnet-",
+ "networkNetworkSecurityGroups": "nsg-",
+ "managedIdentityUserAssignedIdentities": "id-",
+ "resourcesResourceGroups": "rg-",
+ "dts": "dts-",
+ "taskhub": "taskhub-"
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/federated-identity.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/federated-identity.bicep
new file mode 100644
index 00000000..18cadeaa
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/federated-identity.bicep
@@ -0,0 +1,34 @@
+metadata description = 'Creates a federated identity credential for AKS workload identity.'
+
+@description('The name of the user-assigned managed identity')
+param identityName string
+
+@description('The name of the federated credential')
+param federatedCredentialName string
+
+@description('The OIDC issuer URL from the AKS cluster')
+param oidcIssuerUrl string
+
+@description('The Kubernetes namespace for the service account')
+param serviceAccountNamespace string = 'default'
+
+@description('The Kubernetes service account name')
+param serviceAccountName string
+
+resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
+ name: identityName
+}
+
+// Federated identity credential binds a Kubernetes service account to the
+// user-assigned managed identity, enabling pods to authenticate as that identity.
+resource federatedCredential 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2024-11-30' = {
+ parent: identity
+ name: federatedCredentialName
+ properties: {
+ issuer: oidcIssuerUrl
+ subject: 'system:serviceaccount:${serviceAccountNamespace}:${serviceAccountName}'
+ audiences: [
+ 'api://AzureADTokenExchange'
+ ]
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/openai.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/openai.bicep
new file mode 100644
index 00000000..bf724f67
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/openai.bicep
@@ -0,0 +1,92 @@
+metadata description = 'Creates an Azure OpenAI account with a chat model deployment for the GenerateCode activity.'
+
+@description('Name of the Azure OpenAI (Cognitive Services) account')
+param name string
+
+@description('Azure region for the Azure OpenAI account')
+param location string = resourceGroup().location
+
+@description('Tags to apply to the account')
+param tags object = {}
+
+@description('Custom subdomain used to build the account endpoint')
+param customSubDomainName string = name
+
+@description('Name of the chat model deployment the app calls')
+param chatDeploymentName string = 'gpt-5.1'
+
+@description('Chat model name')
+param chatModelName string = 'gpt-5.1'
+
+@description('Chat model version')
+param chatModelVersion string = '2025-11-13'
+
+@description('Deployment SKU name for the chat model')
+param chatDeploymentSkuName string = 'GlobalStandard'
+
+@description('Tokens-per-minute capacity (in thousands) for the chat deployment')
+param chatDeploymentCapacity int = 30
+
+@description('Principal id of the workload identity that calls Azure OpenAI')
+param workloadPrincipalId string
+
+@description('Principal id of the deploying user (optional)')
+param userPrincipalId string = ''
+
+resource account 'Microsoft.CognitiveServices/accounts@2024-10-01' = {
+ name: name
+ location: location
+ tags: tags
+ kind: 'OpenAI'
+ sku: {
+ name: 'S0'
+ }
+ properties: {
+ customSubDomainName: customSubDomainName
+ publicNetworkAccess: 'Enabled'
+ disableLocalAuth: true
+ }
+}
+
+resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
+ parent: account
+ name: chatDeploymentName
+ sku: {
+ name: chatDeploymentSkuName
+ capacity: chatDeploymentCapacity
+ }
+ properties: {
+ model: {
+ format: 'OpenAI'
+ name: chatModelName
+ version: chatModelVersion
+ }
+ }
+}
+
+// Cognitive Services OpenAI User role (5e0bd9bd-7b93-4f28-af87-19fc36ad61bd)
+var openAiUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')
+
+resource workloadOpenAiAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: account
+ name: guid(account.id, workloadPrincipalId, openAiUserRole)
+ properties: {
+ roleDefinitionId: openAiUserRole
+ principalId: workloadPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource userOpenAiAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(userPrincipalId)) {
+ scope: account
+ name: guid(account.id, userPrincipalId, openAiUserRole)
+ properties: {
+ roleDefinitionId: openAiUserRole
+ principalId: userPrincipalId
+ principalType: 'User'
+ }
+}
+
+output endpoint string = account.properties.endpoint
+output name string = account.name
+output chatDeploymentName string = chatDeployment.name
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/scheduler-access.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/scheduler-access.bicep
new file mode 100644
index 00000000..aea4ebf5
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/scheduler-access.bicep
@@ -0,0 +1,53 @@
+metadata description = 'Wires up an existing Durable Task Scheduler: ensures the task hub, grants data-plane access, and surfaces the endpoint. Deployed into the scheduler\'s resource group.'
+
+@description('Name of the existing Durable Task Scheduler')
+param schedulerName string
+
+@description('Name of the task hub to use (created if it does not already exist)')
+param taskHubName string = 'default'
+
+@description('Principal id of the workload identity that connects to DTS')
+param workloadPrincipalId string
+
+@description('Principal id of the deploying user, for dashboard access (optional)')
+param userPrincipalId string = ''
+
+// The scheduler is created and patched out of band (preview feature enablement +
+// managed-identity attach), so it is referenced as an existing resource here.
+resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' existing = {
+ name: schedulerName
+}
+
+// Ensure the task hub the app uses exists on the scheduler.
+resource taskHub 'Microsoft.DurableTask/schedulers/taskhubs@2025-11-01' = {
+ parent: scheduler
+ name: taskHubName
+}
+
+// Durable Task Data Contributor (0ad04412-c4d5-4796-b79c-f76d14c8d402) — data-plane
+// access used by the orchestrator app and the sandbox worker to connect to DTS.
+var dtsDataRole = '0ad04412-c4d5-4796-b79c-f76d14c8d402'
+
+resource workloadDtsAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: scheduler
+ name: guid(scheduler.id, workloadPrincipalId, dtsDataRole)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', dtsDataRole)
+ principalId: workloadPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource userDtsAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(userPrincipalId)) {
+ scope: scheduler
+ name: guid(scheduler.id, userPrincipalId, dtsDataRole)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', dtsDataRole)
+ principalId: userPrincipalId
+ principalType: 'User'
+ }
+}
+
+output endpoint string = scheduler.properties.endpoint
+output taskHubName string = taskHub.name
+output schedulerName string = scheduler.name
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/user-assigned-identity.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/user-assigned-identity.bicep
new file mode 100644
index 00000000..0583ab8d
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/app/user-assigned-identity.bicep
@@ -0,0 +1,17 @@
+metadata description = 'Creates a Microsoft Entra user-assigned identity.'
+
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: name
+ location: location
+ tags: tags
+}
+
+output name string = identity.name
+output resourceId string = identity.id
+output principalId string = identity.properties.principalId
+output clientId string = identity.properties.clientId
+output tenantId string = identity.properties.tenantId
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/aks-cluster.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/aks-cluster.bicep
new file mode 100644
index 00000000..d2a767e3
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/aks-cluster.bicep
@@ -0,0 +1,94 @@
+metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster.'
+
+@description('The name of the AKS cluster')
+param name string
+
+@description('The Azure region for the AKS cluster')
+param location string = resourceGroup().location
+
+@description('Tags to apply to the AKS cluster')
+param tags object = {}
+
+@description('The Kubernetes version for the AKS cluster')
+param kubernetesVersion string = '1.32'
+
+@description('The VM size for the default node pool')
+param agentVMSize string = 'standard_d4s_v5'
+
+@description('The number of nodes in the default node pool')
+param agentCount int = 2
+
+@description('The minimum number of nodes for autoscaling')
+param agentMinCount int = 1
+
+@description('The maximum number of nodes for autoscaling')
+param agentMaxCount int = 5
+
+@description('The subnet resource ID for the AKS nodes')
+param subnetId string = ''
+
+@description('The name of the container registry to attach')
+param containerRegistryName string = ''
+
+@description('Enable OIDC issuer for workload identity')
+param enableOidcIssuer bool = true
+
+@description('Enable workload identity')
+param enableWorkloadIdentity bool = true
+
+// AKS cluster with workload identity and OIDC issuer enabled
+resource aksCluster 'Microsoft.ContainerService/managedClusters@2024-09-01' = {
+ name: name
+ location: location
+ tags: tags
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ kubernetesVersion: kubernetesVersion
+ dnsPrefix: name
+ enableRBAC: true
+ agentPoolProfiles: [
+ {
+ name: 'system'
+ count: agentCount
+ vmSize: agentVMSize
+ mode: 'System'
+ osType: 'Linux'
+ osSKU: 'AzureLinux'
+ enableAutoScaling: true
+ minCount: agentMinCount
+ maxCount: agentMaxCount
+ vnetSubnetID: !empty(subnetId) ? subnetId : null
+ }
+ ]
+ networkProfile: {
+ networkPlugin: 'azure'
+ networkPolicy: 'azure'
+ serviceCidr: '10.1.0.0/16'
+ dnsServiceIP: '10.1.0.10'
+ }
+ oidcIssuerProfile: {
+ enabled: enableOidcIssuer
+ }
+ securityProfile: {
+ workloadIdentity: {
+ enabled: enableWorkloadIdentity
+ }
+ }
+ }
+}
+
+// Grant AKS kubelet identity AcrPull access to the container registry
+module registryAccess '../security/registry-access.bicep' = if (!empty(containerRegistryName)) {
+ name: 'aks-registry-access'
+ params: {
+ containerRegistryName: containerRegistryName
+ principalId: aksCluster.properties.identityProfile.kubeletidentity.objectId
+ }
+}
+
+output clusterName string = aksCluster.name
+output clusterId string = aksCluster.id
+output oidcIssuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL
+output kubeletIdentityObjectId string = aksCluster.properties.identityProfile.kubeletidentity.objectId
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/container-registry.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/container-registry.bicep
new file mode 100644
index 00000000..4cd24453
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/host/container-registry.bicep
@@ -0,0 +1,59 @@
+metadata description = 'Creates an Azure Container Registry.'
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+@description('Indicates whether admin user is enabled')
+param adminUserEnabled bool = false
+
+@description('Indicates whether anonymous pull is enabled')
+param anonymousPullEnabled bool = false
+
+@description('SKU settings')
+param sku object = {
+ name: 'Standard'
+}
+
+@description('The log analytics workspace ID used for logging and monitoring')
+param workspaceId string = ''
+
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
+ name: name
+ location: location
+ tags: tags
+ sku: sku
+ properties: {
+ adminUserEnabled: adminUserEnabled
+ anonymousPullEnabled: anonymousPullEnabled
+ publicNetworkAccess: 'Enabled'
+ }
+}
+
+resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {
+ name: 'registry-diagnostics'
+ scope: containerRegistry
+ properties: {
+ workspaceId: workspaceId
+ logs: [
+ {
+ category: 'ContainerRegistryRepositoryEvents'
+ enabled: true
+ }
+ {
+ category: 'ContainerRegistryLoginEvents'
+ enabled: true
+ }
+ ]
+ metrics: [
+ {
+ category: 'AllMetrics'
+ enabled: true
+ timeGrain: 'PT1M'
+ }
+ ]
+ }
+}
+
+output id string = containerRegistry.id
+output loginServer string = containerRegistry.properties.loginServer
+output name string = containerRegistry.name
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/networking/vnet.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/networking/vnet.bicep
new file mode 100644
index 00000000..46764a8e
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/networking/vnet.bicep
@@ -0,0 +1,40 @@
+@description('The name of the Virtual Network')
+param name string
+
+@description('The Azure region where the Virtual Network should exist')
+param location string = resourceGroup().location
+
+@description('Optional tags for the resources')
+param tags object = {}
+
+@description('The address prefixes of the Virtual Network')
+param addressPrefixes array = ['10.0.0.0/16']
+
+@description('The subnets to create in the Virtual Network')
+param subnets array = [
+ {
+ name: 'aks-subnet'
+ properties: {
+ addressPrefix: '10.0.0.0/21'
+ delegations: []
+ privateEndpointNetworkPolicies: 'Disabled'
+ privateLinkServiceNetworkPolicies: 'Enabled'
+ }
+ }
+]
+
+resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
+ name: name
+ location: location
+ tags: tags
+ properties: {
+ addressSpace: {
+ addressPrefixes: addressPrefixes
+ }
+ subnets: subnets
+ }
+}
+
+output id string = vnet.id
+output name string = vnet.name
+output aksSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'aks-subnet')
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/registry-access.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/registry-access.bicep
new file mode 100644
index 00000000..f977b9f1
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/registry-access.bicep
@@ -0,0 +1,19 @@
+metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.'
+param containerRegistryName string
+param principalId string
+
+var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+
+resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: containerRegistry // Use when specifying a scope that is different than the deployment scope
+ name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole)
+ properties: {
+ roleDefinitionId: acrPullRole
+ principalType: 'ServicePrincipal'
+ principalId: principalId
+ }
+}
+
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
+ name: containerRegistryName
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/role.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/role.bicep
new file mode 100644
index 00000000..0b30cfd3
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/core/security/role.bicep
@@ -0,0 +1,21 @@
+metadata description = 'Creates a role assignment for a service principal.'
+param principalId string
+
+@allowed([
+ 'Device'
+ 'ForeignGroup'
+ 'Group'
+ 'ServicePrincipal'
+ 'User'
+])
+param principalType string = 'ServicePrincipal'
+param roleDefinitionId string
+
+resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId)
+ properties: {
+ principalId: principalId
+ principalType: principalType
+ roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.bicep b/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.bicep
new file mode 100644
index 00000000..66ee57f2
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.bicep
@@ -0,0 +1,222 @@
+targetScope = 'subscription'
+
+// Provisions the Azure resources to run the On-demand Sandboxes code-interpreter demo
+// in the cloud: the orchestrator (main-app) runs on AKS, and DTS starts the sandbox
+// worker image on demand. The Durable Task Scheduler is NOT created here — it is passed
+// in as an existing resource (schedulerName + schedulerResourceGroupName) because it is
+// patched out of band to enable the On-demand Sandboxes preview feature.
+
+@minLength(1)
+@maxLength(64)
+@description('Name of the environment which is used to generate a short unique hash used in all resources.')
+param environmentName string
+
+@minLength(1)
+@description('Primary location for all resources')
+param location string
+
+@description('Id of the user or app to assign application roles')
+param principalId string = ''
+
+// AKS parameters
+param aksClusterName string = ''
+param kubernetesVersion string = '1.32'
+param aksVmSize string = 'standard_d4s_v5'
+param aksNodeCount int = 2
+
+// Container registry parameters
+param containerRegistryName string = ''
+
+// Existing Durable Task Scheduler (created + patched out of band for the preview).
+@description('Name of the existing Durable Task Scheduler to use as the durable backend.')
+param schedulerName string
+
+@description('Resource group that contains the existing Durable Task Scheduler.')
+param schedulerResourceGroupName string
+
+@description('Task hub to use on the scheduler (created if it does not exist).')
+param taskHubName string = 'default'
+
+// Azure OpenAI parameters (used by the in-process GenerateCode activity).
+param openAiServiceName string = ''
+param openAiLocation string = 'eastus'
+param chatDeploymentName string = 'gpt-5.1'
+param chatModelName string = 'gpt-5.1'
+param chatModelVersion string = '2025-11-13'
+param chatDeploymentSkuName string = 'GlobalStandard'
+param chatDeploymentCapacity int = 30
+
+// Service name (must match the service name in azure.yaml).
+param mainAppServiceName string = 'mainapp'
+
+// Optional resource group name override
+param resourceGroupName string = ''
+
+var abbrs = loadJsonContent('./abbreviations.json')
+
+var tags = {
+ 'azd-env-name': environmentName
+}
+
+var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
+
+resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
+ name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
+ location: location
+ tags: tags
+}
+
+// ============================
+// Identity
+// ============================
+
+// A single user-assigned managed identity is used for everything in this sample:
+// - AKS workload identity (the app authenticates to DTS and Azure OpenAI)
+// - DTS image pull for the sandbox (AcrPull on the registry)
+// - the sandbox worker connecting back to DTS
+module identity './app/user-assigned-identity.bicep' = {
+ scope: rg
+ params: {
+ name: '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}'
+ location: location
+ tags: tags
+ }
+}
+
+// ============================
+// Networking
+// ============================
+
+module vnet './core/networking/vnet.bicep' = {
+ scope: rg
+ params: {
+ name: '${abbrs.networkVirtualNetworks}${resourceToken}'
+ location: location
+ tags: tags
+ }
+}
+
+// ============================
+// Container Registry
+// ============================
+
+module containerRegistry './core/host/container-registry.bicep' = {
+ name: 'container-registry'
+ scope: rg
+ params: {
+ name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}'
+ location: location
+ tags: tags
+ sku: {
+ name: 'Standard'
+ }
+ anonymousPullEnabled: false
+ }
+}
+
+// Grant the managed identity AcrPull so DTS can pull the sandbox worker image.
+module identityAcrPull './core/security/registry-access.bicep' = {
+ name: 'identity-acr-pull'
+ scope: rg
+ params: {
+ containerRegistryName: containerRegistry.outputs.name
+ principalId: identity.outputs.principalId
+ }
+}
+
+// ============================
+// AKS Cluster
+// ============================
+
+module aksCluster './core/host/aks-cluster.bicep' = {
+ name: 'aks-cluster'
+ scope: rg
+ params: {
+ name: !empty(aksClusterName) ? aksClusterName : '${abbrs.containerServiceManagedClusters}${resourceToken}'
+ location: location
+ tags: tags
+ kubernetesVersion: kubernetesVersion
+ agentVMSize: aksVmSize
+ agentCount: aksNodeCount
+ subnetId: vnet.outputs.aksSubnetId
+ containerRegistryName: containerRegistry.outputs.name
+ }
+}
+
+// ============================
+// Workload Identity Federation
+// ============================
+
+module federatedIdentityMainApp './app/federated-identity.bicep' = {
+ name: 'federated-identity-mainapp'
+ scope: rg
+ params: {
+ identityName: identity.outputs.name
+ federatedCredentialName: 'fed-${mainAppServiceName}'
+ oidcIssuerUrl: aksCluster.outputs.oidcIssuerUrl
+ serviceAccountNamespace: 'default'
+ serviceAccountName: mainAppServiceName
+ }
+}
+
+// ============================
+// Azure OpenAI
+// ============================
+
+module openAi './app/openai.bicep' = {
+ name: 'openai'
+ scope: rg
+ params: {
+ name: !empty(openAiServiceName) ? openAiServiceName : 'aoai-${resourceToken}'
+ location: openAiLocation
+ tags: tags
+ chatDeploymentName: chatDeploymentName
+ chatModelName: chatModelName
+ chatModelVersion: chatModelVersion
+ chatDeploymentSkuName: chatDeploymentSkuName
+ chatDeploymentCapacity: chatDeploymentCapacity
+ workloadPrincipalId: identity.outputs.principalId
+ userPrincipalId: principalId
+ }
+}
+
+// ============================
+// Existing Durable Task Scheduler
+// ============================
+
+module schedulerAccess './app/scheduler-access.bicep' = {
+ name: 'scheduler-access'
+ scope: resourceGroup(schedulerResourceGroupName)
+ params: {
+ schedulerName: schedulerName
+ taskHubName: taskHubName
+ workloadPrincipalId: identity.outputs.principalId
+ userPrincipalId: principalId
+ }
+}
+
+// ============================
+// Outputs
+// ============================
+
+output AZURE_LOCATION string = location
+output AZURE_TENANT_ID string = tenant().tenantId
+
+output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
+output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
+
+output AZURE_AKS_CLUSTER_NAME string = aksCluster.outputs.clusterName
+
+output AZURE_USER_ASSIGNED_IDENTITY_NAME string = identity.outputs.name
+output AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID string = identity.outputs.clientId
+output AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID string = identity.outputs.resourceId
+
+// Scheduler details (DTS_ENDPOINT/DTS_TASK_HUB feed the app; name/RG feed the
+// postprovision identity-attach hook).
+output DTS_ENDPOINT string = schedulerAccess.outputs.endpoint
+output DTS_TASK_HUB string = schedulerAccess.outputs.taskHubName
+output DTS_SCHEDULER_NAME string = schedulerName
+output DTS_SCHEDULER_RESOURCE_GROUP string = schedulerResourceGroupName
+
+output AOAI_ENDPOINT string = openAi.outputs.endpoint
+output AOAI_DEPLOYMENT string = openAi.outputs.chatDeploymentName
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.parameters.json b/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.parameters.json
new file mode 100644
index 00000000..f98d2108
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/infra/main.parameters.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "environmentName": {
+ "value": "${AZURE_ENV_NAME}"
+ },
+ "location": {
+ "value": "${AZURE_LOCATION}"
+ },
+ "principalId": {
+ "value": "${AZURE_PRINCIPAL_ID}"
+ },
+ "schedulerName": {
+ "value": "${DTS_SCHEDULER_NAME}"
+ },
+ "schedulerResourceGroupName": {
+ "value": "${DTS_SCHEDULER_RESOURCE_GROUP}"
+ },
+ "taskHubName": {
+ "value": "${DTS_TASK_HUB=default}"
+ }
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Activities.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Activities.cs
new file mode 100644
index 00000000..dc83a0c9
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Activities.cs
@@ -0,0 +1,128 @@
+using System.ClientModel;
+using System.Globalization;
+using Azure.Identity;
+using Microsoft.DurableTask;
+using OpenAI.Chat;
+
+namespace Demo.Codegen.MainApp;
+
+///
+/// In-process activity. Calls Azure OpenAI to translate a natural-language question
+/// into a self-contained pandas script that reads /tmp/data.csv and prints the answer.
+///
+[DurableTask(TaskNames.GenerateCode)]
+internal sealed class GenerateCodeActivity : TaskActivity
+{
+ const string SystemPrompt = """
+ You are a Python code generator. Given a question about a sales dataset,
+ produce a single self-contained Python script that:
+
+ 1. Reads /tmp/data.csv with pandas. The columns are: date, region, product, units, revenue.
+ 2. Assumes the CSV contains rows for exactly one region.
+ 3. Computes the total revenue for March 2025 in this subset.
+ 4. Prints ONLY the numeric revenue total to stdout. No code fences, no explanation, no commentary.
+
+ Constraints:
+ - Use only the Python standard library and pandas.
+ - Do not access the network or filesystem outside /tmp.
+ - If there is no March 2025 data in this subset, print 0.
+ - Output must be plain text containing only the number.
+
+ Respond with the Python script only. No markdown, no backticks.
+ """;
+
+ public override async Task RunAsync(TaskActivityContext context, string question)
+ {
+ string endpoint = GetRequired("AOAI_ENDPOINT");
+ string deployment = GetRequired("AOAI_DEPLOYMENT");
+
+ var client = new Azure.AI.OpenAI.AzureOpenAIClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential());
+
+ ChatClient chat = client.GetChatClient(deployment);
+
+ ChatCompletion completion = await chat.CompleteChatAsync(
+ new SystemChatMessage(SystemPrompt),
+ new UserChatMessage(question));
+
+ string code = StripCodeFences(completion.Content[0].Text ?? string.Empty);
+ int lineCount = code.Split('\n').Length;
+ Console.WriteLine($"[generate] AOAI returned {lineCount} lines of Python:");
+ Console.WriteLine("---");
+ Console.WriteLine(code);
+ Console.WriteLine("---");
+ return code;
+ }
+
+ static string GetRequired(string name)
+ => Environment.GetEnvironmentVariable(name)
+ ?? throw new InvalidOperationException($"Environment variable '{name}' is required.");
+
+ static string StripCodeFences(string code)
+ {
+ string trimmed = code.Trim();
+ if (trimmed.StartsWith("```", StringComparison.Ordinal))
+ {
+ int firstNewline = trimmed.IndexOf('\n');
+ if (firstNewline > 0)
+ {
+ trimmed = trimmed[(firstNewline + 1)..];
+ }
+
+ if (trimmed.EndsWith("```", StringComparison.Ordinal))
+ {
+ trimmed = trimmed[..^3];
+ }
+ }
+
+ return trimmed.Trim();
+ }
+}
+
+///
+/// In-process activity. Wraps the sandboxed Python output in a friendly answer.
+/// Kept deliberately simple - in a real app this might call the LLM again to
+/// turn raw output into a sentence.
+///
+[DurableTask(TaskNames.FormatAnswer)]
+internal sealed class FormatAnswerActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, FormatAnswerInput input)
+ {
+ foreach (RegionExecutionResult result in input.Results)
+ {
+ if (result.Execution.ExitCode != 0)
+ {
+ return Task.FromResult(
+ $"Sandbox execution failed for region '{result.Region}' (exit code {result.Execution.ExitCode}): {result.Execution.Stderr}");
+ }
+ }
+
+ var totals = new List<(string Region, decimal Revenue)>();
+ foreach (RegionExecutionResult result in input.Results)
+ {
+ string stdout = result.Execution.Stdout.Trim();
+ if (!decimal.TryParse(stdout, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal revenue))
+ {
+ return Task.FromResult(
+ $"Sandbox execution returned a non-numeric result for region '{result.Region}': {stdout}");
+ }
+
+ totals.Add((result.Region, revenue));
+ }
+
+ foreach ((string region, decimal revenue) in totals.OrderByDescending(total => total.Revenue))
+ {
+ Console.WriteLine($"[fan-out] Region {region}: {revenue.ToString(CultureInfo.InvariantCulture)}");
+ }
+
+ string topRegion = totals
+ .OrderByDescending(total => total.Revenue)
+ .ThenBy(total => total.Region, StringComparer.Ordinal)
+ .First()
+ .Region;
+
+ return Task.FromResult($"Q: {input.Question}\nA: {topRegion}");
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/AnalyzeSalesOrchestrator.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/AnalyzeSalesOrchestrator.cs
new file mode 100644
index 00000000..c3725e9a
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/AnalyzeSalesOrchestrator.cs
@@ -0,0 +1,80 @@
+using Microsoft.DurableTask;
+
+namespace Demo.Codegen.MainApp;
+
+///
+/// 3-step workflow that answers a question over a CSV using LLM-generated Python.
+///
+[DurableTask(nameof(AnalyzeSalesOrchestrator))]
+internal sealed class AnalyzeSalesOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, AnalyzeSalesInput input)
+ {
+ // Generate one chunk-friendly Python script up front and reuse it for every region.
+ string pythonCode = await context.CallActivityAsync(
+ TaskNames.GenerateCode,
+ input.Question);
+
+ // Fan out: one sandbox execution per region-specific CSV partition.
+ RegionChunk[] chunks = SplitCsvByRegion(input.CsvData);
+ Task[] executions = chunks
+ .Select(chunk => context.CallActivityAsync(
+ TaskNames.ExecuteCode,
+ new ExecuteCodeInput(pythonCode, chunk.CsvData)))
+ .ToArray();
+
+ // Fan in: wait for every sandbox result, then hand the set to the formatter.
+ ExecuteCodeOutput[] results = await Task.WhenAll(executions);
+ RegionExecutionResult[] regionResults = chunks
+ .Zip(results, (chunk, execution) => new RegionExecutionResult(chunk.Region, execution))
+ .ToArray();
+
+ return await context.CallActivityAsync(
+ TaskNames.FormatAnswer,
+ new FormatAnswerInput(input.Question, regionResults));
+ }
+
+
+
+
+
+
+
+
+ static RegionChunk[] SplitCsvByRegion(string csvData)
+ {
+ // Partition the dataset deterministically so the same generated script can run once per region.
+ string normalized = csvData.Replace("\r\n", "\n", StringComparison.Ordinal);
+ string[] lines = normalized.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ if (lines.Length < 2)
+ {
+ return [];
+ }
+
+ string header = lines[0];
+ Dictionary> rowsByRegion = new(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string row in lines.Skip(1))
+ {
+ string[] cells = row.Split(',');
+ if (cells.Length < 2)
+ {
+ continue;
+ }
+
+ string region = cells[1].Trim();
+ if (!rowsByRegion.TryGetValue(region, out List? rows))
+ {
+ rows = [];
+ rowsByRegion.Add(region, rows);
+ }
+
+ rows.Add(row);
+ }
+
+ return rowsByRegion
+ .OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
+ .Select(pair => new RegionChunk(pair.Key, string.Join('\n', new[] { header }.Concat(pair.Value))))
+ .ToArray();
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Containerfile b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Containerfile
new file mode 100644
index 00000000..c0a2daf7
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Containerfile
@@ -0,0 +1,41 @@
+# syntax=docker/dockerfile:1.7
+#
+# Builds the orchestrator (main-app) image deployed to AKS. Build from the .NET sample
+# root so Directory.Build.props and the data/ folder are in the build context:
+#
+# docker build --platform linux/amd64 -f main-app/Containerfile -t /main-app: .
+#
+# --platform linux/amd64 is required: the Grpc.Tools linux_arm64 protoc binary segfaults
+# under Docker's arm64 emulation on Apple Silicon. amd64 works under Rosetta and matches
+# what the cluster runs.
+
+FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG TARGETARCH
+
+WORKDIR /src/dts-ondemand-sandbox-codegen-demo
+COPY . .
+
+WORKDIR /src/dts-ondemand-sandbox-codegen-demo/main-app
+RUN case "$TARGETARCH" in \
+ amd64) runtime_identifier=linux-x64 ;; \
+ arm64) runtime_identifier=linux-arm64 ;; \
+ *) echo "Unsupported target architecture: $TARGETARCH" >&2; exit 1 ;; \
+ esac \
+ && dotnet publish main-app.csproj \
+ -c Release \
+ -r "$runtime_identifier" \
+ --self-contained false \
+ -o /app/publish \
+ /p:DebugSymbols=false \
+ /p:DebugType=None \
+ && find /app/publish -type f \( -name '*.xml' -o -name '*.pdb' \) -delete
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
+WORKDIR /app
+
+COPY --from=build /app/publish ./
+# Ship the sample dataset and point the app at it.
+COPY data ./data
+ENV DEMO_CSV_PATH=/app/data/sales_q1.csv
+
+ENTRYPOINT ["dotnet", "CodegenMainApp.dll"]
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Contracts.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Contracts.cs
new file mode 100644
index 00000000..2520df72
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Contracts.cs
@@ -0,0 +1,32 @@
+namespace Demo.Codegen.MainApp;
+
+///
+/// Input passed from the orchestrator to the on-demand sandbox ExecuteCode activity.
+/// The activity writes the CSV to disk and runs the Python script against it.
+///
+public sealed record ExecuteCodeInput(string PythonCode, string CsvData);
+
+///
+/// Output of the sandboxed Python execution.
+///
+public sealed record ExecuteCodeOutput(string Stdout, string Stderr, int ExitCode);
+
+///
+/// A deterministic CSV partition used to fan out on-demand sandbox executions.
+///
+public sealed record RegionChunk(string Region, string CsvData);
+
+///
+/// Captures the sandbox execution result for a single region partition.
+///
+public sealed record RegionExecutionResult(string Region, ExecuteCodeOutput Execution);
+
+///
+/// Input to the orchestrator: a natural-language question and the CSV to answer it over.
+///
+public sealed record AnalyzeSalesInput(string Question, string CsvData);
+
+///
+/// Input to the final formatting activity after fan-out/fan-in completes.
+///
+public sealed record FormatAnswerInput(string Question, RegionExecutionResult[] Results);
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Program.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Program.cs
new file mode 100644
index 00000000..1f36d793
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/Program.cs
@@ -0,0 +1,120 @@
+using Azure.Core;
+using Azure.Identity;
+using Demo.Codegen.MainApp;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+string endpoint = Environment.GetEnvironmentVariable("DTS_ENDPOINT")
+ ?? throw new InvalidOperationException("DTS_ENDPOINT is required.");
+string taskHub = Environment.GetEnvironmentVariable("DTS_TASK_HUB") ?? "default";
+string aoaiEndpoint = Environment.GetEnvironmentVariable("AOAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AOAI_ENDPOINT is required.");
+string csvPath = Environment.GetEnvironmentVariable("DEMO_CSV_PATH")
+ ?? Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "data", "sales_q1.csv");
+string question = args.Length > 0
+ ? string.Join(' ', args)
+ : "Which region had the highest total revenue in March 2025?";
+
+if (!File.Exists(csvPath))
+{
+ Console.Error.WriteLine($"CSV file not found at: {csvPath}");
+ return 1;
+}
+
+string csvData = await File.ReadAllTextAsync(csvPath);
+TokenCredential credential = new DefaultAzureCredential();
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+builder.Logging.AddSimpleConsole(options =>
+{
+ options.SingleLine = true;
+ options.UseUtcTimestamp = true;
+ options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ ";
+});
+
+builder.Services.AddDurableTaskWorker(workerBuilder =>
+{
+ workerBuilder.AddTasks(tasks => tasks.AddAllGeneratedTasks());
+ workerBuilder.UseWorkItemFilters();
+ workerBuilder.UseDurableTaskScheduler(options =>
+ {
+ options.EndpointAddress = endpoint;
+ options.TaskHubName = taskHub;
+ options.Credential = credential;
+ });
+});
+
+builder.Services.AddDurableTaskClient(clientBuilder =>
+{
+ clientBuilder.UseDurableTaskScheduler(options =>
+ {
+ options.EndpointAddress = endpoint;
+ options.TaskHubName = taskHub;
+ options.Credential = credential;
+ });
+});
+
+// Profiles are declared in WorkerProfiles.cs via [SandboxWorkerProfile].
+builder.Services.AddDurableTaskSchedulerSandboxActivitiesClient();
+
+using IHost host = builder.Build();
+await host.StartAsync();
+
+// Declare the sandbox worker profiles with DTS so it can route ExecuteCode to a sandbox.
+SandboxActivitiesClient sandboxActivitiesClient = host.Services.GetRequiredService();
+await sandboxActivitiesClient.EnableSandboxActivitiesAsync();
+
+DurableTaskClient client = host.Services.GetRequiredService();
+
+// Print demo context so the audience understands the dataset before orchestration starts.
+string[] allLines = File.ReadAllLines(csvPath);
+string[] headers = allLines[0].Split(',');
+int rowCount = allLines.Length - 1;
+string columnList = string.Join(", ", headers);
+Console.WriteLine($"[demo] Dataset: {Path.GetFullPath(csvPath)}");
+Console.WriteLine($"[demo] {rowCount} rows × {headers.Length} columns: [{columnList}]");
+Console.WriteLine($"[demo] Preview (first 3 rows):");
+int previewCount = Math.Min(3, rowCount);
+for (int i = 1; i <= previewCount; i++)
+{
+ string[] cells = allLines[i].Split(',');
+ Console.WriteLine(" " + string.Join(" ", cells));
+}
+Console.WriteLine($"[demo] Question: {question}");
+Console.WriteLine();
+
+string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
+ TaskNames.AnalyzeSalesOrchestrator,
+ input: new AnalyzeSalesInput(question, csvData));
+
+Console.WriteLine($"Started orchestration: {instanceId}");
+
+OrchestrationMetadata? result = await client.WaitForInstanceCompletionAsync(
+ instanceId,
+ getInputsAndOutputs: true);
+
+Console.WriteLine($"Status: {result?.RuntimeStatus}");
+Console.WriteLine();
+
+if (result?.FailureDetails is { } failure)
+{
+ Console.WriteLine($"[failure] {failure.ErrorType}: {failure.ErrorMessage}");
+ if (!string.IsNullOrWhiteSpace(failure.StackTrace))
+ {
+ Console.WriteLine(failure.StackTrace);
+ }
+}
+else
+{
+ Console.WriteLine(result?.ReadOutputAs() ?? "");
+}
+
+await host.StopAsync();
+return 0;
+
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/TaskNames.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/TaskNames.cs
new file mode 100644
index 00000000..570e3757
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/TaskNames.cs
@@ -0,0 +1,13 @@
+namespace Demo.Codegen.MainApp;
+
+///
+/// Activity and orchestrator names shared with the sandbox worker.
+/// The sandbox worker registers with the same string.
+///
+internal static class TaskNames
+{
+ public const string AnalyzeSalesOrchestrator = nameof(AnalyzeSalesOrchestrator);
+ public const string GenerateCode = nameof(GenerateCode);
+ public const string ExecuteCode = nameof(ExecuteCode);
+ public const string FormatAnswer = nameof(FormatAnswer);
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/WorkerProfiles.cs b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/WorkerProfiles.cs
new file mode 100644
index 00000000..ffd52574
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/WorkerProfiles.cs
@@ -0,0 +1,26 @@
+using Microsoft.DurableTask.Client.AzureManaged;
+
+namespace Demo.Codegen.MainApp;
+
+///
+/// Declares the on-demand sandbox worker profile that hosts the
+/// activity in an isolated sandbox container. The profile id ("code-executor") surfaces in
+/// the DTS dashboard under the On-demand Sandboxes tab.
+///
+[SandboxWorkerProfile("code-executor")]
+internal sealed class CodeSandboxWorkerProfile : ISandboxWorkerProfile
+{
+ public void Configure(SandboxWorkerProfileOptions options)
+ {
+ options.Image.ImageRef = Environment.GetEnvironmentVariable("DTS_SANDBOX_CONTAINER_IMAGE")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_CONTAINER_IMAGE is required.");
+ options.Image.ManagedIdentityClientId = Environment.GetEnvironmentVariable("DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID is required.");
+ options.SchedulerManagedIdentityClientId = Environment.GetEnvironmentVariable("DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID")
+ ?? throw new InvalidOperationException("DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID is required.");
+ options.Cpu = "1000m";
+ options.Memory = "2048Mi";
+ options.MaxConcurrentActivities = 1;
+ options.AddActivity(TaskNames.ExecuteCode, version: "");
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/main-app.csproj b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/main-app.csproj
new file mode 100644
index 00000000..103b60dd
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/main-app.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ CodegenMainApp
+ Demo.Codegen.MainApp
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/main-app/manifests/deployment.tmpl.yaml b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/manifests/deployment.tmpl.yaml
new file mode 100644
index 00000000..da3bc458
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/main-app/manifests/deployment.tmpl.yaml
@@ -0,0 +1,57 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: mainapp
+ namespace: default
+ annotations:
+ azure.workload.identity/client-id: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ labels:
+ azure.workload.identity/use: "true"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mainapp
+ namespace: default
+ labels:
+ app: mainapp
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mainapp
+ template:
+ metadata:
+ labels:
+ app: mainapp
+ azure.workload.identity/use: "true"
+ spec:
+ serviceAccountName: mainapp
+ containers:
+ - name: mainapp
+ image: {{ .Env.SERVICE_MAINAPP_IMAGE_NAME }}
+ env:
+ - name: DTS_ENDPOINT
+ value: {{ .Env.DTS_ENDPOINT }}
+ - name: DTS_TASK_HUB
+ value: {{ .Env.DTS_TASK_HUB }}
+ - name: AOAI_ENDPOINT
+ value: {{ .Env.AOAI_ENDPOINT }}
+ - name: AOAI_DEPLOYMENT
+ value: {{ .Env.AOAI_DEPLOYMENT }}
+ - name: AZURE_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ # The sandbox worker profile (WorkerProfiles.cs) reads these:
+ - name: DTS_SANDBOX_CONTAINER_IMAGE
+ value: {{ .Env.DTS_SANDBOX_CONTAINER_IMAGE }}
+ - name: DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ - name: DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ resources:
+ requests:
+ cpu: 250m
+ memory: 256Mi
+ limits:
+ cpu: "1"
+ memory: 512Mi
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile
new file mode 100644
index 00000000..65ab938e
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile
@@ -0,0 +1,54 @@
+# syntax=docker/dockerfile:1.7
+#
+# Build from the demo root:
+#
+# docker build \
+# --platform linux/amd64 \
+# -f sandbox-worker/Containerfile \
+# -t .azurecr.io/dts-codegen-sandbox: \
+# .
+#
+# ⚠️ --platform linux/amd64 is required. The Grpc.Tools 2.78.0 linux_arm64 protoc
+# binary segfaults under Docker's arm64 emulation on Apple Silicon. amd64 protoc
+# works under Rosetta and matches what DTS sandboxes run anyway.
+#
+# The sandbox worker is a .NET app that shells out to python3, so we install
+# pandas in the runtime stage.
+
+FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG TARGETARCH
+
+WORKDIR /src/dts-ondemand-sandbox-codegen-demo
+COPY . .
+
+WORKDIR /src/dts-ondemand-sandbox-codegen-demo/sandbox-worker
+RUN case "$TARGETARCH" in \
+ amd64) runtime_identifier=linux-x64 ;; \
+ arm64) runtime_identifier=linux-arm64 ;; \
+ *) echo "Unsupported target architecture: $TARGETARCH" >&2; exit 1 ;; \
+ esac \
+ && dotnet publish sandbox-worker.csproj \
+ -c Release \
+ -r "$runtime_identifier" \
+ --self-contained false \
+ -o /app/publish \
+ /p:DebugSymbols=false \
+ /p:DebugType=None \
+ && find /app/publish -type f \( -name '*.xml' -o -name '*.pdb' \) -delete
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
+WORKDIR /app
+
+# Install python3 + pandas. These are the only tools the LLM-generated scripts can use.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends python3 python3-pip \
+ && pip3 install --no-cache-dir --break-system-packages pandas==2.2.* \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV ASPNETCORE_URLS=http://+:8080
+EXPOSE 8080
+
+COPY --from=build /app/publish ./
+
+ENTRYPOINT ["dotnet", "CodegenSandboxWorker.dll"]
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile.dockerignore b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile.dockerignore
new file mode 100644
index 00000000..fdf3b455
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Containerfile.dockerignore
@@ -0,0 +1,6 @@
+bin/
+obj/
+out/
+.vs/
+.idea/
+.DS_Store
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Contracts.cs b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Contracts.cs
new file mode 100644
index 00000000..eae31c76
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Contracts.cs
@@ -0,0 +1,9 @@
+namespace Demo.Codegen.SandboxWorker;
+
+///
+/// Input contract matching the main app's ExecuteCodeInput record.
+/// Defined separately here so the worker has no dependency on the main app.
+///
+public sealed record ExecuteCodeInput(string PythonCode, string CsvData);
+
+public sealed record ExecuteCodeOutput(string Stdout, string Stderr, int ExitCode);
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/ExecuteCodeActivity.cs b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/ExecuteCodeActivity.cs
new file mode 100644
index 00000000..9b23ad9f
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/ExecuteCodeActivity.cs
@@ -0,0 +1,133 @@
+using System.Diagnostics;
+using Microsoft.DurableTask;
+
+namespace Demo.Codegen.SandboxWorker;
+
+///
+/// Runs LLM-generated Python in the on-demand sandbox.
+///
+/// Each invocation gets a fresh container instance. The CSV is written to /tmp/data.csv
+/// and the generated script is executed against it. We capture stdout/stderr and the
+/// exit code so the orchestrator can surface failures cleanly.
+///
+[DurableTask("ExecuteCode")]
+internal sealed class ExecuteCodeActivity : TaskActivity
+{
+ public override async Task RunAsync(TaskActivityContext context, ExecuteCodeInput input)
+ {
+ string sandboxName = Environment.GetEnvironmentVariable("DTS_SANDBOX_ID")
+ ?? Environment.MachineName;
+
+ Console.WriteLine($"[sandbox] Starting ExecuteCode in sandbox '{sandboxName}' (pid={Environment.ProcessId})");
+ Console.WriteLine("[sandbox] This is isolated on-demand sandbox compute managed by DTS.");
+
+ // Show the audience exactly what untrusted code landed in this sandbox.
+ string[] codeLines = input.PythonCode.Split('\n');
+ int byteCount = System.Text.Encoding.UTF8.GetByteCount(input.PythonCode);
+ Console.WriteLine($"[sandbox] Received generated Python ({codeLines.Length} lines, {byteCount} bytes)");
+ Console.WriteLine("[sandbox] --- generated script ---");
+ const int maxDisplayLines = 30;
+ int displayCount = Math.Min(codeLines.Length, maxDisplayLines);
+ for (int i = 0; i < displayCount; i++)
+ {
+ Console.WriteLine(codeLines[i]);
+ }
+
+ if (codeLines.Length > maxDisplayLines)
+ {
+ Console.WriteLine($"... (truncated, {codeLines.Length - maxDisplayLines} more lines)");
+ }
+
+ Console.WriteLine("[sandbox] --- end script ---");
+
+ string workDir = Path.Combine("/tmp", $"run-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(workDir);
+ Console.WriteLine($"[sandbox] Created isolated work directory: {workDir}");
+
+ string csvPath = Path.Combine(workDir, "data.csv");
+ string scriptPath = Path.Combine(workDir, "script.py");
+
+ await File.WriteAllTextAsync(csvPath, input.CsvData);
+ await File.WriteAllTextAsync(scriptPath, input.PythonCode);
+ Console.WriteLine($"[sandbox] Wrote dataset: {csvPath}");
+ Console.WriteLine($"[sandbox] Wrote generated script: {scriptPath}");
+
+ // The generated script reads /tmp/data.csv. Copy into the canonical location
+ // so the LLM doesn't need to know about per-invocation working directories.
+ File.Copy(csvPath, "/tmp/data.csv", overwrite: true);
+ Console.WriteLine("[sandbox] Mounted dataset at expected path: /tmp/data.csv");
+ string[] csvLines = input.CsvData.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+ if (csvLines.Length > 0)
+ {
+ string[] csvHeaders = csvLines[0].Split(',');
+ int csvRowCount = csvLines.Length - 1;
+ Console.WriteLine($"[sandbox] Dataset loaded: {csvRowCount} rows × {csvHeaders.Length} columns [{string.Join(", ", csvHeaders)}]");
+ }
+
+ using var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "python3",
+ ArgumentList = { scriptPath },
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ WorkingDirectory = workDir,
+ },
+ };
+
+ var sw = Stopwatch.StartNew();
+ process.Start();
+ Console.WriteLine($"[sandbox] Executing command: python3 {scriptPath}");
+
+ // Cap execution time so a runaway script can't hold the sandbox forever.
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ Task stdoutTask = process.StandardOutput.ReadToEndAsync(cts.Token);
+ Task stderrTask = process.StandardError.ReadToEndAsync(cts.Token);
+
+ try
+ {
+ await process.WaitForExitAsync(cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ try { process.Kill(entireProcessTree: true); }
+ catch { /* best effort */ }
+
+ Console.WriteLine("[sandbox] ERROR: Timeout: execution exceeded 30 seconds.");
+ return new ExecuteCodeOutput(
+ Stdout: string.Empty,
+ Stderr: "Execution timed out after 30 seconds.",
+ ExitCode: 124);
+ }
+
+ sw.Stop();
+ string stdout = await stdoutTask;
+ string stderr = await stderrTask;
+
+ Console.WriteLine($"[sandbox] Python process completed in {sw.ElapsedMilliseconds}ms (exit code {process.ExitCode})");
+
+ if (!string.IsNullOrWhiteSpace(stdout))
+ {
+ Console.WriteLine("[sandbox] stdout from generated script:");
+ Console.WriteLine(stdout.TrimEnd());
+ }
+ else
+ {
+ Console.WriteLine("[sandbox] stdout from generated script: ");
+ }
+
+ if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(stderr))
+ {
+ Console.WriteLine($"[sandbox] ERROR: {stderr.TrimEnd()}");
+ }
+
+ if (process.ExitCode == 0)
+ {
+ Console.WriteLine("[sandbox] Returning captured stdout to the orchestrator.");
+ }
+
+ return new ExecuteCodeOutput(stdout, stderr, process.ExitCode);
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Program.cs b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Program.cs
new file mode 100644
index 00000000..ff73d006
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/Program.cs
@@ -0,0 +1,27 @@
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+builder.Logging.AddSimpleConsole(options =>
+{
+ options.SingleLine = true;
+ options.UseUtcTimestamp = true;
+ options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ ";
+});
+
+builder.Services.AddDurableTaskWorker(workerBuilder =>
+{
+ workerBuilder.AddTasks(tasks =>
+ {
+ tasks.AddActivity();
+ });
+
+ // DTS injects endpoint, task hub, profile id, and runtime settings into the
+ // sandbox via environment variables — the worker image stays config-free.
+ workerBuilder.UseSandboxWorker();
+});
+
+await builder.Build().RunAsync();
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/sandbox-worker.csproj b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/sandbox-worker.csproj
new file mode 100644
index 00000000..9330cd5e
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/sandbox-worker/sandbox-worker.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ CodegenSandboxWorker
+ Demo.Codegen.SandboxWorker
+
+
+
+
+
+
+
+
+
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/scripts/acr-build.sh b/preview-features/on-demand-sandboxes/samples/dotnet/scripts/acr-build.sh
new file mode 100755
index 00000000..b6192a8e
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/scripts/acr-build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Builds the two container images for the On-demand Sandboxes demo server-side using
+# ACR Tasks (az acr build) — no local Docker required. Called by azd as a predeploy hook.
+#
+# - main-app : the orchestrator, deployed to AKS (azd reads SERVICE_MAINAPP_IMAGE_NAME
+# and skips its own build/push).
+# - sandbox : the worker image DTS starts on demand. Not deployed to AKS; its full
+# image reference is handed to the app via DTS_SANDBOX_CONTAINER_IMAGE.
+
+set -euo pipefail
+
+REGISTRY="${AZURE_CONTAINER_REGISTRY_NAME:?AZURE_CONTAINER_REGISTRY_NAME must be set}"
+REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:?AZURE_CONTAINER_REGISTRY_ENDPOINT must be set}"
+ENV_NAME="${AZURE_ENV_NAME:?AZURE_ENV_NAME must be set}"
+TAG="azd-deploy-$(date +%s)"
+
+# The .NET build context is the sample root so Directory.Build.props is available.
+build() {
+ local image_repo="$1" # e.g. dts-ondemand-sandboxes/main-app-
+ local containerfile="$2"
+ local full_image="${REGISTRY_ENDPOINT}/${image_repo}:${TAG}"
+
+ echo "==> Building ${image_repo}:${TAG} via ACR Tasks (--platform linux/amd64)..." >&2
+ az acr build \
+ --registry "${REGISTRY}" \
+ --image "${image_repo}:${TAG}" \
+ --platform linux/amd64 \
+ --file "${containerfile}" \
+ . \
+ --no-logs \
+ --output none >&2
+
+ echo "${full_image}"
+}
+
+MAIN_APP_IMAGE="$(build "dts-ondemand-sandboxes/main-app-${ENV_NAME}" "main-app/Containerfile")"
+SANDBOX_IMAGE="$(build "dts-ondemand-sandboxes/sandbox-worker-${ENV_NAME}" "sandbox-worker/Containerfile")"
+
+# azd uses SERVICE__IMAGE_NAME to skip its own build and deploy this image instead.
+azd env set SERVICE_MAINAPP_IMAGE_NAME "${MAIN_APP_IMAGE}"
+# The app declares the sandbox worker profile using this image reference.
+azd env set DTS_SANDBOX_CONTAINER_IMAGE "${SANDBOX_IMAGE}"
+
+echo "==> main-app image : ${MAIN_APP_IMAGE}"
+echo "==> sandbox image : ${SANDBOX_IMAGE}"
diff --git a/preview-features/on-demand-sandboxes/samples/dotnet/scripts/attach-scheduler-identity.sh b/preview-features/on-demand-sandboxes/samples/dotnet/scripts/attach-scheduler-identity.sh
new file mode 100755
index 00000000..3c216af4
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/dotnet/scripts/attach-scheduler-identity.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# Attaches the sample's user-assigned managed identity to the existing Durable Task
+# Scheduler so DTS can use it to pull the sandbox image and let the sandbox worker
+# connect back. Runs as an azd postprovision hook. The PATCH is merge-safe: it keeps
+# any identities already attached to the scheduler.
+#
+# NOTE: enabling the On-demand Sandboxes preview *feature* on the scheduler is a
+# separate, out-of-band step handled during private-preview onboarding.
+
+set -euo pipefail
+
+SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:?AZURE_SUBSCRIPTION_ID must be set}"
+SCHEDULER_NAME="${DTS_SCHEDULER_NAME:?DTS_SCHEDULER_NAME must be set}"
+SCHEDULER_RG="${DTS_SCHEDULER_RESOURCE_GROUP:?DTS_SCHEDULER_RESOURCE_GROUP must be set}"
+IDENTITY_ID="${AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID:?AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID must be set}"
+API_VERSION="2026-05-01-preview"
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "ERROR: python3 is required to merge the scheduler identity block." >&2
+ exit 1
+fi
+
+URI="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${SCHEDULER_RG}/providers/Microsoft.DurableTask/schedulers/${SCHEDULER_NAME}?api-version=${API_VERSION}"
+
+echo "==> Reading current identity on scheduler '${SCHEDULER_NAME}'..."
+CURRENT="$(az rest --method get --uri "${URI}")"
+
+BODY="$(IDENTITY_ID="${IDENTITY_ID}" python3 - "${CURRENT}" <<'PY'
+import json, os, sys
+
+current = json.loads(sys.argv[1])
+identity_id = os.environ["IDENTITY_ID"]
+
+identity = current.get("identity") or {}
+user_assigned = identity.get("userAssignedIdentities") or {}
+user_assigned[identity_id] = {}
+
+current_type = identity.get("type", "") or ""
+new_type = "SystemAssigned, UserAssigned" if "SystemAssigned" in current_type else "UserAssigned"
+
+print(json.dumps({"identity": {"type": new_type, "userAssignedIdentities": user_assigned}}))
+PY
+)"
+
+TMP="$(mktemp)"
+trap 'rm -f "${TMP}"' EXIT
+printf '%s' "${BODY}" > "${TMP}"
+
+echo "==> Attaching managed identity to scheduler..."
+az rest --method patch --uri "${URI}" --body "@${TMP}" >/dev/null
+echo "==> Done. Identity ${IDENTITY_ID##*/} is attached to '${SCHEDULER_NAME}'."
diff --git a/preview-features/on-demand-sandboxes/samples/python/Containerfile b/preview-features/on-demand-sandboxes/samples/python/Containerfile
new file mode 100644
index 00000000..a0d1a71c
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/Containerfile
@@ -0,0 +1,31 @@
+# syntax=docker/dockerfile:1.7
+#
+# Build from the python/ demo directory:
+#
+# docker build \
+# -f Containerfile \
+# -t .azurecr.io/dts-codegen-sandbox-python:v1 \
+# .
+#
+# Private preview requires the image to be publicly pullable by the sandbox platform.
+
+FROM python:3.12-slim AS runtime
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+ENV GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/etc/ssl/certs/ca-certificates.crt
+
+# Install the Durable Task SDK (with the sandboxes extension), plus pandas for
+# the LLM-generated scripts the sandbox executes.
+RUN pip install --no-cache-dir \
+ durabletask==1.6.0 \
+ durabletask-azuremanaged==1.6.0 \
+ azure-identity \
+ "pandas==2.2.*"
+
+COPY remote_worker.py /app/remote_worker.py
+COPY activities.py /app/activities.py
+
+ENTRYPOINT ["python", "/app/remote_worker.py"]
diff --git a/preview-features/on-demand-sandboxes/samples/python/Containerfile.mainapp b/preview-features/on-demand-sandboxes/samples/python/Containerfile.mainapp
new file mode 100644
index 00000000..d25f26f6
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/Containerfile.mainapp
@@ -0,0 +1,24 @@
+# syntax=docker/dockerfile:1.7
+#
+# Builds the orchestrator (main_app.py) image deployed to AKS. Build from the python/
+# sample directory so requirements.txt and data/ are in the build context:
+#
+# docker build -f Containerfile.mainapp -t /main-app: .
+
+FROM python:3.12-slim AS runtime
+WORKDIR /app
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+ENV GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/etc/ssl/certs/ca-certificates.crt
+
+COPY requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir -r /app/requirements.txt
+
+COPY main_app.py /app/main_app.py
+COPY activities.py /app/activities.py
+COPY data /app/data
+ENV DEMO_CSV_PATH=/app/data/sales_q1.csv
+
+ENTRYPOINT ["python", "/app/main_app.py"]
diff --git a/preview-features/on-demand-sandboxes/samples/python/README.md b/preview-features/on-demand-sandboxes/samples/python/README.md
new file mode 100644
index 00000000..cf79e16f
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/README.md
@@ -0,0 +1,190 @@
+# On-demand Sandboxes demo (Python): LLM-generated code interpreter
+
+The Python port of the [.NET demo](../dotnet/README.md). A three-step Durable Task
+workflow that demonstrates the **On-demand Sandboxes** preview of Azure Durable
+Task Scheduler (DTS), using the `durabletask.azuremanaged.preview.sandboxes`
+package.
+
+```
+ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
+ │ generate_code │ │ execute_code │ │ format_answer │
+ │ (in-process Python) │ -> │ (on-demand sandbox) │ -> │ (in-process Python) │
+ │ Azure OpenAI -> Python │ │ python3 + pandas │ │ Pick top region │
+ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘
+```
+
+The orchestrator asks a natural-language question over `data/sales_q1.csv`. The LLM
+returns a self-contained pandas script. That script is **untrusted** code, so it runs
+in a DTS-managed on-demand sandbox — not in the orchestrator's process. The first and
+last activities stay in-process; `execute_code` is fanned out one sandbox execution
+per region partition.
+
+## Layout
+
+```
+python/
+├── activities.py # Shared activity identities (execute_code is a SandboxActivity)
+├── main_app.py # Declarer app: orchestrator + in-process activities + profile
+├── remote_worker.py # Sandbox worker image entrypoint: runs execute_code via python3
+├── Containerfile # Builds the remote worker (sandbox) image
+├── Containerfile.mainapp # Builds the main_app image deployed to AKS
+├── requirements.txt # Declarer-app dependencies
+├── azure.yaml # azd service + hooks (Deploy to Azure)
+├── infra/ # Bicep: AKS, ACR, identity, Azure OpenAI, scheduler wiring
+├── scripts/ # acr-build.sh + attach-scheduler-identity.sh (azd hooks)
+├── manifests/ # K8s deployment template for main_app
+└── data/sales_q1.csv # Sample dataset (~300 rows)
+```
+
+- `execute_code` is declared as an on-demand sandbox activity by the `code-executor`
+ worker profile (the `@sandbox_worker_profile` class in `main_app.py`). It is never
+ registered on the main app worker.
+- `generate_code` and `format_answer` run in-process in the main app worker.
+
+## Prerequisites
+
+- Python 3.12+
+- Docker (to build the sandbox image)
+- A DTS scheduler + task hub with the On-demand Sandboxes preview enabled
+- An Azure Container Registry the sandbox platform can pull from
+- Two user-assigned managed identities (image pull + scheduler connect)
+- An Azure OpenAI deployment of a chat model (GPT-4o, GPT-4.1, etc.)
+
+## Install
+
+From the `python/` directory:
+
+```bash
+pip install -r requirements.txt
+pip install durabletask==1.6.0 durabletask-azuremanaged==1.6.0
+```
+
+## Build the sandbox image
+
+From the `python/` directory:
+
+```bash
+ACR=
+IMAGE=$ACR.azurecr.io/dts-codegen-sandbox-python:v1
+
+docker build \
+ -f Containerfile \
+ -t $IMAGE \
+ .
+
+# Enable anonymous pull so DTS can fetch the sandbox image without credentials
+az acr update --name $ACR --anonymous-pull-enabled true
+az acr login --name $ACR
+docker push $IMAGE
+```
+
+## Run the orchestrator
+
+```bash
+export DTS_ENDPOINT="https://"
+export DTS_TASK_HUB=""
+export DTS_WORKER_PROFILE_ID="code-executor"
+export DTS_SANDBOX_CONTAINER_IMAGE=".azurecr.io/dts-codegen-sandbox-python:v1"
+export DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID=""
+export DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID=""
+
+export AOAI_ENDPOINT="https://.openai.azure.com"
+export AOAI_DEPLOYMENT=""
+
+# Sign in so DefaultAzureCredential can reach DTS and Azure OpenAI
+az login
+
+python main_app.py "Which region had the highest total revenue in March 2025?"
+```
+
+The declarer prints a dataset preview, the AOAI-generated Python (prefixed
+`[generate]`), the orchestration id, and the final answer. The sandbox container
+logs (prefixed `[sandbox]`) stream through the DTS dashboard's **On-demand
+Sandboxes** tab while `execute_code` runs.
+
+## Deploy to Azure (AKS) with `azd`
+
+The `infra/` folder and `azure.yaml` deploy the **main_app** orchestrator to **Azure
+Kubernetes Service** with [`azd`](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd).
+The sandbox worker image (`remote_worker.py`) is built and pushed to ACR; DTS starts it
+on demand, so it is never deployed to the cluster.
+
+> The Durable Task Scheduler is **not created** by this template — you pass in an
+> existing one. On-demand Sandboxes is a private-preview feature that must be enabled on
+> the scheduler out of band, so the scheduler is patched separately and supplied here by
+> name.
+
+### What gets provisioned
+
+| Resource | Purpose |
+|----------|---------|
+| **AKS cluster** | Hosts the `main_app` orchestrator pod (workload identity enabled) |
+| **Azure Container Registry** | Stores the main-app and sandbox-worker images (built server-side via ACR Tasks) |
+| **User-assigned managed identity** + federated credential | Pod auth to DTS/Azure OpenAI, ACR pull for the sandbox, and the sandbox's connection back to DTS |
+| **Azure OpenAI** + `gpt-4o` deployment | Backs the in-process `generate_code` activity |
+| **VNet** | Network isolation for AKS |
+
+The deployment also **ensures the task hub** exists, grants the identity the roles it
+needs (AcrPull, Durable Task data access, Cognitive Services OpenAI User), and a
+`postprovision` hook **attaches the identity to your scheduler** (a merge-safe PATCH).
+
+### Prerequisites
+
+- An existing **DTS scheduler** with the On-demand Sandboxes preview enabled, and its
+ resource group name.
+- [Azure Developer CLI (`azd`)](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd), [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), and [kubectl](https://kubernetes.io/docs/tasks/tools/).
+- Azure OpenAI quota for `gpt-4o` in your target region.
+
+### Deploy
+
+```bash
+azd auth login && az login
+
+# Point the template at your existing (preview-enabled) scheduler.
+azd env set DTS_SCHEDULER_NAME ""
+azd env set DTS_SCHEDULER_RESOURCE_GROUP ""
+# Optional overrides: DTS_TASK_HUB (default: default), AZURE_OPENAI_LOCATION
+
+azd up
+```
+
+`azd` provisions the resources, builds both images via ACR Tasks, attaches the identity
+to your scheduler, and deploys the `main_app` pod. If you don't set `DTS_SCHEDULER_NAME`
+/ `DTS_SCHEDULER_RESOURCE_GROUP` first, `azd` prompts for them.
+
+### Verify
+
+```bash
+az aks get-credentials --resource-group --name # from `azd env get-values`
+kubectl get pods
+kubectl logs -l app=mainapp --tail=50
+```
+
+The `main_app` pod runs the orchestration; `[sandbox]` logs from `execute_code` stream in
+the DTS dashboard's **On-demand Sandboxes** tab.
+
+### Clean up
+
+```bash
+azd down
+```
+
+This removes the resources the template created. Your scheduler is left untouched (it was
+not created here); detach the identity manually if you no longer need it.
+
+## Sample questions to try
+
+- `Which region had the highest total revenue in March 2025?`
+- `What was the best-selling product in Q1?`
+- `Average revenue per transaction in February?`
+
+## What's in-process vs on-demand sandbox
+
+| Activity | Runs where | Why |
+| --------------- | ------------- | ----------------------------------------------------- |
+| generate_code | In-process | Plain Azure OpenAI HTTP call. No reason to split out. |
+| execute_code | **Sandbox** | Untrusted LLM-generated code + different runtime. |
+| format_answer | In-process | Trivial result aggregation. |
+
+Only `execute_code` is declared on the `code-executor` sandbox worker profile via
+`options.add_activity(...)`. Everything else runs wherever the orchestrator runs.
diff --git a/preview-features/on-demand-sandboxes/samples/python/activities.py b/preview-features/on-demand-sandboxes/samples/python/activities.py
new file mode 100644
index 00000000..72b0c21d
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/activities.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+"""Activity identities shared between the declarer app and the sandbox worker."""
+
+from durabletask.azuremanaged.preview.sandboxes import SandboxActivity
+
+# ExecuteCode runs in a DTS-managed on-demand sandbox. Python orchestrations call
+# activities by name, so the sandbox activity identity is unversioned.
+EXECUTE_CODE = SandboxActivity(name="execute_code", version=None)
+
+# In-process activities that run inside the main app worker.
+GENERATE_CODE = "generate_code"
+FORMAT_ANSWER = "format_answer"
diff --git a/preview-features/on-demand-sandboxes/samples/python/azure.yaml b/preview-features/on-demand-sandboxes/samples/python/azure.yaml
new file mode 100644
index 00000000..058f3559
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/azure.yaml
@@ -0,0 +1,24 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+# On-demand Sandboxes (Python) on Azure Kubernetes Service.
+#
+# Only the orchestrator (main_app.py) is deployed to AKS. The sandbox worker image
+# (remote_worker.py) is built and pushed to ACR by the predeploy hook; DTS starts it on
+# demand, so it is not an azd service. Images are built server-side with ACR Tasks
+# (az acr build) to avoid local Docker builds.
+
+metadata:
+ template: dts-ondemand-sandboxes-python-aks
+name: dts-ondemand-sandboxes-python
+hooks:
+ predeploy:
+ shell: bash
+ run: ./scripts/acr-build.sh
+ postprovision:
+ shell: bash
+ run: ./scripts/attach-scheduler-identity.sh
+services:
+ mainapp:
+ project: .
+ language: python
+ host: aks
diff --git a/preview-features/on-demand-sandboxes/samples/python/data/sales_q1.csv b/preview-features/on-demand-sandboxes/samples/python/data/sales_q1.csv
new file mode 100644
index 00000000..aa6d0ee8
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/data/sales_q1.csv
@@ -0,0 +1,301 @@
+date,region,product,units,revenue
+2025-01-01,West,Gadget,18,9000
+2025-01-01,East,Widget,25,5000
+2025-01-01,Central,Gizmo,32,9600
+2025-01-01,South,Gadget,39,19500
+2025-01-02,West,Widget,46,9200
+2025-01-02,East,Gizmo,53,15900
+2025-01-02,Central,Gadget,60,30000
+2025-01-03,South,Widget,24,4800
+2025-01-03,West,Gizmo,31,9300
+2025-01-03,East,Gadget,38,19000
+2025-01-03,Central,Widget,45,9000
+2025-01-04,South,Gizmo,52,15600
+2025-01-04,West,Gadget,59,29500
+2025-01-04,East,Widget,23,4600
+2025-01-05,Central,Gizmo,30,9000
+2025-01-05,South,Gadget,37,18500
+2025-01-05,West,Widget,44,8800
+2025-01-06,East,Gizmo,51,15300
+2025-01-06,Central,Gadget,58,29000
+2025-01-06,South,Widget,22,4400
+2025-01-06,West,Gizmo,29,8700
+2025-01-07,East,Gadget,36,18000
+2025-01-07,Central,Widget,43,8600
+2025-01-07,South,Gizmo,50,15000
+2025-01-08,West,Gadget,57,28500
+2025-01-08,East,Widget,21,4200
+2025-01-08,Central,Gizmo,28,8400
+2025-01-09,South,Gadget,35,17500
+2025-01-09,West,Widget,42,8400
+2025-01-09,East,Gizmo,49,14700
+2025-01-09,Central,Gadget,56,28000
+2025-01-10,South,Widget,20,4000
+2025-01-10,West,Gizmo,27,8100
+2025-01-10,East,Gadget,34,17000
+2025-01-11,Central,Widget,41,8200
+2025-01-11,South,Gizmo,48,14400
+2025-01-11,West,Gadget,55,27500
+2025-01-12,East,Widget,19,3800
+2025-01-12,Central,Gizmo,26,7800
+2025-01-12,South,Gadget,33,16500
+2025-01-12,West,Widget,40,8000
+2025-01-13,East,Gizmo,47,14100
+2025-01-13,Central,Gadget,54,27000
+2025-01-13,South,Widget,18,3600
+2025-01-14,West,Gizmo,25,7500
+2025-01-14,East,Gadget,32,16000
+2025-01-14,Central,Widget,39,7800
+2025-01-14,South,Gizmo,46,13800
+2025-01-15,West,Gadget,53,26500
+2025-01-15,East,Widget,60,12000
+2025-01-15,Central,Gizmo,24,7200
+2025-01-16,South,Gadget,31,15500
+2025-01-16,West,Widget,38,7600
+2025-01-16,East,Gizmo,45,13500
+2025-01-17,Central,Gadget,52,26000
+2025-01-17,South,Widget,59,11800
+2025-01-17,West,Gizmo,23,6900
+2025-01-17,East,Gadget,30,15000
+2025-01-18,Central,Widget,37,7400
+2025-01-18,South,Gizmo,44,13200
+2025-01-18,West,Gadget,51,25500
+2025-01-19,East,Widget,58,11600
+2025-01-19,Central,Gizmo,22,6600
+2025-01-19,South,Gadget,29,14500
+2025-01-20,West,Widget,36,7200
+2025-01-20,East,Gizmo,43,12900
+2025-01-20,Central,Gadget,50,25000
+2025-01-20,South,Widget,57,11400
+2025-01-21,West,Gizmo,21,6300
+2025-01-21,East,Gadget,28,14000
+2025-01-21,Central,Widget,35,7000
+2025-01-22,South,Gizmo,42,12600
+2025-01-22,West,Gadget,49,24500
+2025-01-22,East,Widget,56,11200
+2025-01-23,Central,Gizmo,20,6000
+2025-01-23,South,Gadget,27,13500
+2025-01-23,West,Widget,34,6800
+2025-01-23,East,Gizmo,41,12300
+2025-01-24,Central,Gadget,48,24000
+2025-01-24,South,Widget,55,11000
+2025-01-24,West,Gizmo,19,5700
+2025-01-25,East,Gadget,26,13000
+2025-01-25,Central,Widget,33,6600
+2025-01-25,South,Gizmo,40,12000
+2025-01-26,West,Gadget,47,23500
+2025-01-26,East,Widget,54,10800
+2025-01-26,Central,Gizmo,18,5400
+2025-01-26,South,Gadget,25,12500
+2025-01-27,West,Widget,32,6400
+2025-01-27,East,Gizmo,39,11700
+2025-01-27,Central,Gadget,46,23000
+2025-01-28,South,Widget,53,10600
+2025-01-28,West,Gizmo,60,18000
+2025-01-28,East,Gadget,24,12000
+2025-01-28,Central,Widget,31,6200
+2025-01-29,South,Gizmo,38,11400
+2025-01-29,West,Gadget,45,22500
+2025-01-29,East,Widget,52,10400
+2025-01-30,Central,Gizmo,59,17700
+2025-01-30,South,Gadget,23,11500
+2025-01-30,West,Widget,30,6000
+2025-01-31,East,Gizmo,37,11100
+2025-01-31,Central,Gadget,44,22000
+2025-01-31,South,Widget,51,10200
+2025-01-31,West,Gizmo,58,17400
+2025-02-01,East,Gadget,22,11000
+2025-02-01,Central,Widget,29,5800
+2025-02-01,South,Gizmo,36,10800
+2025-02-02,West,Gadget,43,21500
+2025-02-02,East,Widget,50,10000
+2025-02-02,Central,Gizmo,57,17100
+2025-02-03,South,Gadget,21,10500
+2025-02-03,West,Widget,28,5600
+2025-02-03,East,Gizmo,35,10500
+2025-02-03,Central,Gadget,42,21000
+2025-02-04,South,Widget,49,9800
+2025-02-04,West,Gizmo,56,16800
+2025-02-04,East,Gadget,20,10000
+2025-02-05,Central,Widget,27,5400
+2025-02-05,South,Gizmo,34,10200
+2025-02-05,West,Gadget,41,20500
+2025-02-06,East,Widget,48,9600
+2025-02-06,Central,Gizmo,55,16500
+2025-02-06,South,Gadget,19,9500
+2025-02-06,West,Widget,26,5200
+2025-02-07,East,Gizmo,33,9900
+2025-02-07,Central,Gadget,40,20000
+2025-02-07,South,Widget,47,9400
+2025-02-08,West,Gizmo,54,16200
+2025-02-08,East,Gadget,18,9000
+2025-02-08,Central,Widget,25,5000
+2025-02-08,South,Gizmo,32,9600
+2025-02-09,West,Gadget,39,19500
+2025-02-09,East,Widget,46,9200
+2025-02-09,Central,Gizmo,53,15900
+2025-02-10,South,Gadget,60,30000
+2025-02-10,West,Widget,24,4800
+2025-02-10,East,Gizmo,31,9300
+2025-02-11,Central,Gadget,38,19000
+2025-02-11,South,Widget,45,9000
+2025-02-11,West,Gizmo,52,15600
+2025-02-11,East,Gadget,59,29500
+2025-02-12,Central,Widget,23,4600
+2025-02-12,South,Gizmo,30,9000
+2025-02-12,West,Gadget,37,18500
+2025-02-13,East,Widget,44,8800
+2025-02-13,Central,Gizmo,51,15300
+2025-02-13,South,Gadget,58,29000
+2025-02-14,West,Widget,22,4400
+2025-02-14,East,Gizmo,29,8700
+2025-02-14,Central,Gadget,36,18000
+2025-02-14,South,Widget,43,8600
+2025-02-15,West,Gizmo,50,15000
+2025-02-15,East,Gadget,57,28500
+2025-02-15,Central,Widget,21,4200
+2025-02-16,South,Gizmo,28,8400
+2025-02-16,West,Gadget,35,17500
+2025-02-16,East,Widget,42,8400
+2025-02-17,Central,Gizmo,49,14700
+2025-02-17,South,Gadget,56,28000
+2025-02-17,West,Widget,20,4000
+2025-02-17,East,Gizmo,27,8100
+2025-02-18,Central,Gadget,34,17000
+2025-02-18,South,Widget,41,8200
+2025-02-18,West,Gizmo,48,14400
+2025-02-19,East,Gadget,55,27500
+2025-02-19,Central,Widget,19,3800
+2025-02-19,South,Gizmo,26,7800
+2025-02-20,West,Gadget,33,16500
+2025-02-20,East,Widget,40,8000
+2025-02-20,Central,Gizmo,47,14100
+2025-02-20,South,Gadget,54,27000
+2025-02-21,West,Widget,18,3600
+2025-02-21,East,Gizmo,25,7500
+2025-02-21,Central,Gadget,32,16000
+2025-02-22,South,Widget,39,7800
+2025-02-22,West,Gizmo,46,13800
+2025-02-22,East,Gadget,53,26500
+2025-02-22,Central,Widget,60,12000
+2025-02-23,South,Gizmo,24,7200
+2025-02-23,West,Gadget,31,15500
+2025-02-23,East,Widget,38,7600
+2025-02-24,Central,Gizmo,45,13500
+2025-02-24,South,Gadget,52,26000
+2025-02-24,West,Widget,59,11800
+2025-02-25,East,Gizmo,23,6900
+2025-02-25,Central,Gadget,30,15000
+2025-02-25,South,Widget,37,7400
+2025-02-25,West,Gizmo,44,13200
+2025-02-26,East,Gadget,51,25500
+2025-02-26,Central,Widget,58,11600
+2025-02-26,South,Gizmo,22,6600
+2025-02-27,West,Gadget,29,14500
+2025-02-27,East,Widget,36,7200
+2025-02-27,Central,Gizmo,43,12900
+2025-02-28,South,Gadget,50,25000
+2025-02-28,West,Widget,57,11400
+2025-02-28,East,Gizmo,21,6300
+2025-02-28,Central,Gadget,28,14000
+2025-03-01,South,Widget,35,7000
+2025-03-01,West,Gizmo,60,18000
+2025-03-01,East,Gadget,53,26500
+2025-03-02,Central,Widget,58,11600
+2025-03-02,South,Gizmo,20,6000
+2025-03-02,West,Gadget,45,22500
+2025-03-03,East,Widget,38,7600
+2025-03-03,Central,Gizmo,43,12900
+2025-03-03,South,Gadget,48,24000
+2025-03-03,West,Widget,73,14600
+2025-03-04,East,Gizmo,23,6900
+2025-03-04,Central,Gadget,28,14000
+2025-03-04,South,Widget,33,6600
+2025-03-05,West,Gizmo,58,17400
+2025-03-05,East,Gadget,51,25500
+2025-03-05,Central,Widget,56,11200
+2025-03-05,South,Gizmo,18,5400
+2025-03-06,West,Gadget,43,21500
+2025-03-06,East,Widget,36,7200
+2025-03-06,Central,Gizmo,41,12300
+2025-03-07,South,Gadget,46,23000
+2025-03-07,West,Widget,71,14200
+2025-03-07,East,Gizmo,64,19200
+2025-03-08,Central,Gadget,26,13000
+2025-03-08,South,Widget,31,6200
+2025-03-08,West,Gizmo,56,16800
+2025-03-08,East,Gadget,49,24500
+2025-03-09,Central,Widget,54,10800
+2025-03-09,South,Gizmo,59,17700
+2025-03-09,West,Gadget,41,20500
+2025-03-10,East,Widget,34,6800
+2025-03-10,Central,Gizmo,39,11700
+2025-03-10,South,Gadget,44,22000
+2025-03-11,West,Widget,69,13800
+2025-03-11,East,Gizmo,62,18600
+2025-03-11,Central,Gadget,24,12000
+2025-03-11,South,Widget,29,5800
+2025-03-12,West,Gizmo,54,16200
+2025-03-12,East,Gadget,47,23500
+2025-03-12,Central,Widget,52,10400
+2025-03-13,South,Gizmo,57,17100
+2025-03-13,West,Gadget,39,19500
+2025-03-13,East,Widget,32,6400
+2025-03-14,Central,Gizmo,37,11100
+2025-03-14,South,Gadget,42,21000
+2025-03-14,West,Widget,67,13400
+2025-03-14,East,Gizmo,60,18000
+2025-03-15,Central,Gadget,22,11000
+2025-03-15,South,Widget,27,5400
+2025-03-15,West,Gizmo,52,15600
+2025-03-16,East,Gadget,45,22500
+2025-03-16,Central,Widget,50,10000
+2025-03-16,South,Gizmo,55,16500
+2025-03-17,West,Gadget,37,18500
+2025-03-17,East,Widget,30,6000
+2025-03-17,Central,Gizmo,35,10500
+2025-03-17,South,Gadget,40,20000
+2025-03-18,West,Widget,65,13000
+2025-03-18,East,Gizmo,58,17400
+2025-03-18,Central,Gadget,20,10000
+2025-03-19,South,Widget,25,5000
+2025-03-19,West,Gizmo,50,15000
+2025-03-19,East,Gadget,43,21500
+2025-03-19,Central,Widget,48,9600
+2025-03-20,South,Gizmo,53,15900
+2025-03-20,West,Gadget,78,39000
+2025-03-20,East,Widget,28,5600
+2025-03-21,Central,Gizmo,33,9900
+2025-03-21,South,Gadget,38,19000
+2025-03-21,West,Widget,63,12600
+2025-03-22,East,Gizmo,56,16800
+2025-03-22,Central,Gadget,61,30500
+2025-03-22,South,Widget,23,4600
+2025-03-22,West,Gizmo,48,14400
+2025-03-23,East,Gadget,41,20500
+2025-03-23,Central,Widget,46,9200
+2025-03-23,South,Gizmo,51,15300
+2025-03-24,West,Gadget,76,38000
+2025-03-24,East,Widget,26,5200
+2025-03-24,Central,Gizmo,31,9300
+2025-03-25,South,Gadget,36,18000
+2025-03-25,West,Widget,61,12200
+2025-03-25,East,Gizmo,54,16200
+2025-03-25,Central,Gadget,59,29500
+2025-03-26,South,Widget,21,4200
+2025-03-26,West,Gizmo,46,13800
+2025-03-26,East,Gadget,39,19500
+2025-03-27,Central,Widget,44,8800
+2025-03-27,South,Gizmo,49,14700
+2025-03-27,West,Gadget,74,37000
+2025-03-28,East,Widget,24,4800
+2025-03-28,Central,Gizmo,29,8700
+2025-03-28,South,Gadget,34,17000
+2025-03-28,West,Widget,59,11800
+2025-03-29,East,Gizmo,52,15600
+2025-03-29,Central,Gadget,57,28500
+2025-03-29,South,Widget,19,3800
+2025-03-30,West,Gizmo,44,13200
+2025-03-30,East,Gadget,37,18500
+2025-03-30,Central,Widget,42,8400
+2025-03-31,South,Gizmo,47,14100
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/abbreviations.json b/preview-features/on-demand-sandboxes/samples/python/infra/abbreviations.json
new file mode 100644
index 00000000..10424d3d
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/abbreviations.json
@@ -0,0 +1,17 @@
+{
+ "analysisServicesServers": "as",
+ "apiManagementService": "apim-",
+ "appConfigurationStores": "appcs-",
+ "appManagedEnvironments": "cae-",
+ "appContainerApps": "ca-",
+ "authorizationPolicyDefinitions": "policy-",
+ "automationAutomationAccounts": "aa-",
+ "containerRegistryRegistries": "cr",
+ "containerServiceManagedClusters": "aks-",
+ "networkVirtualNetworks": "vnet-",
+ "networkNetworkSecurityGroups": "nsg-",
+ "managedIdentityUserAssignedIdentities": "id-",
+ "resourcesResourceGroups": "rg-",
+ "dts": "dts-",
+ "taskhub": "taskhub-"
+}
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/app/federated-identity.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/app/federated-identity.bicep
new file mode 100644
index 00000000..18cadeaa
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/app/federated-identity.bicep
@@ -0,0 +1,34 @@
+metadata description = 'Creates a federated identity credential for AKS workload identity.'
+
+@description('The name of the user-assigned managed identity')
+param identityName string
+
+@description('The name of the federated credential')
+param federatedCredentialName string
+
+@description('The OIDC issuer URL from the AKS cluster')
+param oidcIssuerUrl string
+
+@description('The Kubernetes namespace for the service account')
+param serviceAccountNamespace string = 'default'
+
+@description('The Kubernetes service account name')
+param serviceAccountName string
+
+resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
+ name: identityName
+}
+
+// Federated identity credential binds a Kubernetes service account to the
+// user-assigned managed identity, enabling pods to authenticate as that identity.
+resource federatedCredential 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2024-11-30' = {
+ parent: identity
+ name: federatedCredentialName
+ properties: {
+ issuer: oidcIssuerUrl
+ subject: 'system:serviceaccount:${serviceAccountNamespace}:${serviceAccountName}'
+ audiences: [
+ 'api://AzureADTokenExchange'
+ ]
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/app/openai.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/app/openai.bicep
new file mode 100644
index 00000000..bf724f67
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/app/openai.bicep
@@ -0,0 +1,92 @@
+metadata description = 'Creates an Azure OpenAI account with a chat model deployment for the GenerateCode activity.'
+
+@description('Name of the Azure OpenAI (Cognitive Services) account')
+param name string
+
+@description('Azure region for the Azure OpenAI account')
+param location string = resourceGroup().location
+
+@description('Tags to apply to the account')
+param tags object = {}
+
+@description('Custom subdomain used to build the account endpoint')
+param customSubDomainName string = name
+
+@description('Name of the chat model deployment the app calls')
+param chatDeploymentName string = 'gpt-5.1'
+
+@description('Chat model name')
+param chatModelName string = 'gpt-5.1'
+
+@description('Chat model version')
+param chatModelVersion string = '2025-11-13'
+
+@description('Deployment SKU name for the chat model')
+param chatDeploymentSkuName string = 'GlobalStandard'
+
+@description('Tokens-per-minute capacity (in thousands) for the chat deployment')
+param chatDeploymentCapacity int = 30
+
+@description('Principal id of the workload identity that calls Azure OpenAI')
+param workloadPrincipalId string
+
+@description('Principal id of the deploying user (optional)')
+param userPrincipalId string = ''
+
+resource account 'Microsoft.CognitiveServices/accounts@2024-10-01' = {
+ name: name
+ location: location
+ tags: tags
+ kind: 'OpenAI'
+ sku: {
+ name: 'S0'
+ }
+ properties: {
+ customSubDomainName: customSubDomainName
+ publicNetworkAccess: 'Enabled'
+ disableLocalAuth: true
+ }
+}
+
+resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
+ parent: account
+ name: chatDeploymentName
+ sku: {
+ name: chatDeploymentSkuName
+ capacity: chatDeploymentCapacity
+ }
+ properties: {
+ model: {
+ format: 'OpenAI'
+ name: chatModelName
+ version: chatModelVersion
+ }
+ }
+}
+
+// Cognitive Services OpenAI User role (5e0bd9bd-7b93-4f28-af87-19fc36ad61bd)
+var openAiUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')
+
+resource workloadOpenAiAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: account
+ name: guid(account.id, workloadPrincipalId, openAiUserRole)
+ properties: {
+ roleDefinitionId: openAiUserRole
+ principalId: workloadPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource userOpenAiAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(userPrincipalId)) {
+ scope: account
+ name: guid(account.id, userPrincipalId, openAiUserRole)
+ properties: {
+ roleDefinitionId: openAiUserRole
+ principalId: userPrincipalId
+ principalType: 'User'
+ }
+}
+
+output endpoint string = account.properties.endpoint
+output name string = account.name
+output chatDeploymentName string = chatDeployment.name
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/app/scheduler-access.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/app/scheduler-access.bicep
new file mode 100644
index 00000000..aea4ebf5
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/app/scheduler-access.bicep
@@ -0,0 +1,53 @@
+metadata description = 'Wires up an existing Durable Task Scheduler: ensures the task hub, grants data-plane access, and surfaces the endpoint. Deployed into the scheduler\'s resource group.'
+
+@description('Name of the existing Durable Task Scheduler')
+param schedulerName string
+
+@description('Name of the task hub to use (created if it does not already exist)')
+param taskHubName string = 'default'
+
+@description('Principal id of the workload identity that connects to DTS')
+param workloadPrincipalId string
+
+@description('Principal id of the deploying user, for dashboard access (optional)')
+param userPrincipalId string = ''
+
+// The scheduler is created and patched out of band (preview feature enablement +
+// managed-identity attach), so it is referenced as an existing resource here.
+resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' existing = {
+ name: schedulerName
+}
+
+// Ensure the task hub the app uses exists on the scheduler.
+resource taskHub 'Microsoft.DurableTask/schedulers/taskhubs@2025-11-01' = {
+ parent: scheduler
+ name: taskHubName
+}
+
+// Durable Task Data Contributor (0ad04412-c4d5-4796-b79c-f76d14c8d402) — data-plane
+// access used by the orchestrator app and the sandbox worker to connect to DTS.
+var dtsDataRole = '0ad04412-c4d5-4796-b79c-f76d14c8d402'
+
+resource workloadDtsAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: scheduler
+ name: guid(scheduler.id, workloadPrincipalId, dtsDataRole)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', dtsDataRole)
+ principalId: workloadPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource userDtsAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(userPrincipalId)) {
+ scope: scheduler
+ name: guid(scheduler.id, userPrincipalId, dtsDataRole)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', dtsDataRole)
+ principalId: userPrincipalId
+ principalType: 'User'
+ }
+}
+
+output endpoint string = scheduler.properties.endpoint
+output taskHubName string = taskHub.name
+output schedulerName string = scheduler.name
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/app/user-assigned-identity.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/app/user-assigned-identity.bicep
new file mode 100644
index 00000000..0583ab8d
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/app/user-assigned-identity.bicep
@@ -0,0 +1,17 @@
+metadata description = 'Creates a Microsoft Entra user-assigned identity.'
+
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: name
+ location: location
+ tags: tags
+}
+
+output name string = identity.name
+output resourceId string = identity.id
+output principalId string = identity.properties.principalId
+output clientId string = identity.properties.clientId
+output tenantId string = identity.properties.tenantId
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/core/host/aks-cluster.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/core/host/aks-cluster.bicep
new file mode 100644
index 00000000..d2a767e3
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/core/host/aks-cluster.bicep
@@ -0,0 +1,94 @@
+metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster.'
+
+@description('The name of the AKS cluster')
+param name string
+
+@description('The Azure region for the AKS cluster')
+param location string = resourceGroup().location
+
+@description('Tags to apply to the AKS cluster')
+param tags object = {}
+
+@description('The Kubernetes version for the AKS cluster')
+param kubernetesVersion string = '1.32'
+
+@description('The VM size for the default node pool')
+param agentVMSize string = 'standard_d4s_v5'
+
+@description('The number of nodes in the default node pool')
+param agentCount int = 2
+
+@description('The minimum number of nodes for autoscaling')
+param agentMinCount int = 1
+
+@description('The maximum number of nodes for autoscaling')
+param agentMaxCount int = 5
+
+@description('The subnet resource ID for the AKS nodes')
+param subnetId string = ''
+
+@description('The name of the container registry to attach')
+param containerRegistryName string = ''
+
+@description('Enable OIDC issuer for workload identity')
+param enableOidcIssuer bool = true
+
+@description('Enable workload identity')
+param enableWorkloadIdentity bool = true
+
+// AKS cluster with workload identity and OIDC issuer enabled
+resource aksCluster 'Microsoft.ContainerService/managedClusters@2024-09-01' = {
+ name: name
+ location: location
+ tags: tags
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ kubernetesVersion: kubernetesVersion
+ dnsPrefix: name
+ enableRBAC: true
+ agentPoolProfiles: [
+ {
+ name: 'system'
+ count: agentCount
+ vmSize: agentVMSize
+ mode: 'System'
+ osType: 'Linux'
+ osSKU: 'AzureLinux'
+ enableAutoScaling: true
+ minCount: agentMinCount
+ maxCount: agentMaxCount
+ vnetSubnetID: !empty(subnetId) ? subnetId : null
+ }
+ ]
+ networkProfile: {
+ networkPlugin: 'azure'
+ networkPolicy: 'azure'
+ serviceCidr: '10.1.0.0/16'
+ dnsServiceIP: '10.1.0.10'
+ }
+ oidcIssuerProfile: {
+ enabled: enableOidcIssuer
+ }
+ securityProfile: {
+ workloadIdentity: {
+ enabled: enableWorkloadIdentity
+ }
+ }
+ }
+}
+
+// Grant AKS kubelet identity AcrPull access to the container registry
+module registryAccess '../security/registry-access.bicep' = if (!empty(containerRegistryName)) {
+ name: 'aks-registry-access'
+ params: {
+ containerRegistryName: containerRegistryName
+ principalId: aksCluster.properties.identityProfile.kubeletidentity.objectId
+ }
+}
+
+output clusterName string = aksCluster.name
+output clusterId string = aksCluster.id
+output oidcIssuerUrl string = aksCluster.properties.oidcIssuerProfile.issuerURL
+output kubeletIdentityObjectId string = aksCluster.properties.identityProfile.kubeletidentity.objectId
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/core/host/container-registry.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/core/host/container-registry.bicep
new file mode 100644
index 00000000..4cd24453
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/core/host/container-registry.bicep
@@ -0,0 +1,59 @@
+metadata description = 'Creates an Azure Container Registry.'
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+@description('Indicates whether admin user is enabled')
+param adminUserEnabled bool = false
+
+@description('Indicates whether anonymous pull is enabled')
+param anonymousPullEnabled bool = false
+
+@description('SKU settings')
+param sku object = {
+ name: 'Standard'
+}
+
+@description('The log analytics workspace ID used for logging and monitoring')
+param workspaceId string = ''
+
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
+ name: name
+ location: location
+ tags: tags
+ sku: sku
+ properties: {
+ adminUserEnabled: adminUserEnabled
+ anonymousPullEnabled: anonymousPullEnabled
+ publicNetworkAccess: 'Enabled'
+ }
+}
+
+resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {
+ name: 'registry-diagnostics'
+ scope: containerRegistry
+ properties: {
+ workspaceId: workspaceId
+ logs: [
+ {
+ category: 'ContainerRegistryRepositoryEvents'
+ enabled: true
+ }
+ {
+ category: 'ContainerRegistryLoginEvents'
+ enabled: true
+ }
+ ]
+ metrics: [
+ {
+ category: 'AllMetrics'
+ enabled: true
+ timeGrain: 'PT1M'
+ }
+ ]
+ }
+}
+
+output id string = containerRegistry.id
+output loginServer string = containerRegistry.properties.loginServer
+output name string = containerRegistry.name
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/core/networking/vnet.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/core/networking/vnet.bicep
new file mode 100644
index 00000000..46764a8e
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/core/networking/vnet.bicep
@@ -0,0 +1,40 @@
+@description('The name of the Virtual Network')
+param name string
+
+@description('The Azure region where the Virtual Network should exist')
+param location string = resourceGroup().location
+
+@description('Optional tags for the resources')
+param tags object = {}
+
+@description('The address prefixes of the Virtual Network')
+param addressPrefixes array = ['10.0.0.0/16']
+
+@description('The subnets to create in the Virtual Network')
+param subnets array = [
+ {
+ name: 'aks-subnet'
+ properties: {
+ addressPrefix: '10.0.0.0/21'
+ delegations: []
+ privateEndpointNetworkPolicies: 'Disabled'
+ privateLinkServiceNetworkPolicies: 'Enabled'
+ }
+ }
+]
+
+resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
+ name: name
+ location: location
+ tags: tags
+ properties: {
+ addressSpace: {
+ addressPrefixes: addressPrefixes
+ }
+ subnets: subnets
+ }
+}
+
+output id string = vnet.id
+output name string = vnet.name
+output aksSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'aks-subnet')
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/core/security/registry-access.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/core/security/registry-access.bicep
new file mode 100644
index 00000000..f977b9f1
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/core/security/registry-access.bicep
@@ -0,0 +1,19 @@
+metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.'
+param containerRegistryName string
+param principalId string
+
+var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
+
+resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: containerRegistry // Use when specifying a scope that is different than the deployment scope
+ name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole)
+ properties: {
+ roleDefinitionId: acrPullRole
+ principalType: 'ServicePrincipal'
+ principalId: principalId
+ }
+}
+
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
+ name: containerRegistryName
+}
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/core/security/role.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/core/security/role.bicep
new file mode 100644
index 00000000..0b30cfd3
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/core/security/role.bicep
@@ -0,0 +1,21 @@
+metadata description = 'Creates a role assignment for a service principal.'
+param principalId string
+
+@allowed([
+ 'Device'
+ 'ForeignGroup'
+ 'Group'
+ 'ServicePrincipal'
+ 'User'
+])
+param principalType string = 'ServicePrincipal'
+param roleDefinitionId string
+
+resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId)
+ properties: {
+ principalId: principalId
+ principalType: principalType
+ roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/main.bicep b/preview-features/on-demand-sandboxes/samples/python/infra/main.bicep
new file mode 100644
index 00000000..66ee57f2
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/main.bicep
@@ -0,0 +1,222 @@
+targetScope = 'subscription'
+
+// Provisions the Azure resources to run the On-demand Sandboxes code-interpreter demo
+// in the cloud: the orchestrator (main-app) runs on AKS, and DTS starts the sandbox
+// worker image on demand. The Durable Task Scheduler is NOT created here — it is passed
+// in as an existing resource (schedulerName + schedulerResourceGroupName) because it is
+// patched out of band to enable the On-demand Sandboxes preview feature.
+
+@minLength(1)
+@maxLength(64)
+@description('Name of the environment which is used to generate a short unique hash used in all resources.')
+param environmentName string
+
+@minLength(1)
+@description('Primary location for all resources')
+param location string
+
+@description('Id of the user or app to assign application roles')
+param principalId string = ''
+
+// AKS parameters
+param aksClusterName string = ''
+param kubernetesVersion string = '1.32'
+param aksVmSize string = 'standard_d4s_v5'
+param aksNodeCount int = 2
+
+// Container registry parameters
+param containerRegistryName string = ''
+
+// Existing Durable Task Scheduler (created + patched out of band for the preview).
+@description('Name of the existing Durable Task Scheduler to use as the durable backend.')
+param schedulerName string
+
+@description('Resource group that contains the existing Durable Task Scheduler.')
+param schedulerResourceGroupName string
+
+@description('Task hub to use on the scheduler (created if it does not exist).')
+param taskHubName string = 'default'
+
+// Azure OpenAI parameters (used by the in-process GenerateCode activity).
+param openAiServiceName string = ''
+param openAiLocation string = 'eastus'
+param chatDeploymentName string = 'gpt-5.1'
+param chatModelName string = 'gpt-5.1'
+param chatModelVersion string = '2025-11-13'
+param chatDeploymentSkuName string = 'GlobalStandard'
+param chatDeploymentCapacity int = 30
+
+// Service name (must match the service name in azure.yaml).
+param mainAppServiceName string = 'mainapp'
+
+// Optional resource group name override
+param resourceGroupName string = ''
+
+var abbrs = loadJsonContent('./abbreviations.json')
+
+var tags = {
+ 'azd-env-name': environmentName
+}
+
+var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
+
+resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
+ name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
+ location: location
+ tags: tags
+}
+
+// ============================
+// Identity
+// ============================
+
+// A single user-assigned managed identity is used for everything in this sample:
+// - AKS workload identity (the app authenticates to DTS and Azure OpenAI)
+// - DTS image pull for the sandbox (AcrPull on the registry)
+// - the sandbox worker connecting back to DTS
+module identity './app/user-assigned-identity.bicep' = {
+ scope: rg
+ params: {
+ name: '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}'
+ location: location
+ tags: tags
+ }
+}
+
+// ============================
+// Networking
+// ============================
+
+module vnet './core/networking/vnet.bicep' = {
+ scope: rg
+ params: {
+ name: '${abbrs.networkVirtualNetworks}${resourceToken}'
+ location: location
+ tags: tags
+ }
+}
+
+// ============================
+// Container Registry
+// ============================
+
+module containerRegistry './core/host/container-registry.bicep' = {
+ name: 'container-registry'
+ scope: rg
+ params: {
+ name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}'
+ location: location
+ tags: tags
+ sku: {
+ name: 'Standard'
+ }
+ anonymousPullEnabled: false
+ }
+}
+
+// Grant the managed identity AcrPull so DTS can pull the sandbox worker image.
+module identityAcrPull './core/security/registry-access.bicep' = {
+ name: 'identity-acr-pull'
+ scope: rg
+ params: {
+ containerRegistryName: containerRegistry.outputs.name
+ principalId: identity.outputs.principalId
+ }
+}
+
+// ============================
+// AKS Cluster
+// ============================
+
+module aksCluster './core/host/aks-cluster.bicep' = {
+ name: 'aks-cluster'
+ scope: rg
+ params: {
+ name: !empty(aksClusterName) ? aksClusterName : '${abbrs.containerServiceManagedClusters}${resourceToken}'
+ location: location
+ tags: tags
+ kubernetesVersion: kubernetesVersion
+ agentVMSize: aksVmSize
+ agentCount: aksNodeCount
+ subnetId: vnet.outputs.aksSubnetId
+ containerRegistryName: containerRegistry.outputs.name
+ }
+}
+
+// ============================
+// Workload Identity Federation
+// ============================
+
+module federatedIdentityMainApp './app/federated-identity.bicep' = {
+ name: 'federated-identity-mainapp'
+ scope: rg
+ params: {
+ identityName: identity.outputs.name
+ federatedCredentialName: 'fed-${mainAppServiceName}'
+ oidcIssuerUrl: aksCluster.outputs.oidcIssuerUrl
+ serviceAccountNamespace: 'default'
+ serviceAccountName: mainAppServiceName
+ }
+}
+
+// ============================
+// Azure OpenAI
+// ============================
+
+module openAi './app/openai.bicep' = {
+ name: 'openai'
+ scope: rg
+ params: {
+ name: !empty(openAiServiceName) ? openAiServiceName : 'aoai-${resourceToken}'
+ location: openAiLocation
+ tags: tags
+ chatDeploymentName: chatDeploymentName
+ chatModelName: chatModelName
+ chatModelVersion: chatModelVersion
+ chatDeploymentSkuName: chatDeploymentSkuName
+ chatDeploymentCapacity: chatDeploymentCapacity
+ workloadPrincipalId: identity.outputs.principalId
+ userPrincipalId: principalId
+ }
+}
+
+// ============================
+// Existing Durable Task Scheduler
+// ============================
+
+module schedulerAccess './app/scheduler-access.bicep' = {
+ name: 'scheduler-access'
+ scope: resourceGroup(schedulerResourceGroupName)
+ params: {
+ schedulerName: schedulerName
+ taskHubName: taskHubName
+ workloadPrincipalId: identity.outputs.principalId
+ userPrincipalId: principalId
+ }
+}
+
+// ============================
+// Outputs
+// ============================
+
+output AZURE_LOCATION string = location
+output AZURE_TENANT_ID string = tenant().tenantId
+
+output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
+output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name
+
+output AZURE_AKS_CLUSTER_NAME string = aksCluster.outputs.clusterName
+
+output AZURE_USER_ASSIGNED_IDENTITY_NAME string = identity.outputs.name
+output AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID string = identity.outputs.clientId
+output AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID string = identity.outputs.resourceId
+
+// Scheduler details (DTS_ENDPOINT/DTS_TASK_HUB feed the app; name/RG feed the
+// postprovision identity-attach hook).
+output DTS_ENDPOINT string = schedulerAccess.outputs.endpoint
+output DTS_TASK_HUB string = schedulerAccess.outputs.taskHubName
+output DTS_SCHEDULER_NAME string = schedulerName
+output DTS_SCHEDULER_RESOURCE_GROUP string = schedulerResourceGroupName
+
+output AOAI_ENDPOINT string = openAi.outputs.endpoint
+output AOAI_DEPLOYMENT string = openAi.outputs.chatDeploymentName
diff --git a/preview-features/on-demand-sandboxes/samples/python/infra/main.parameters.json b/preview-features/on-demand-sandboxes/samples/python/infra/main.parameters.json
new file mode 100644
index 00000000..f98d2108
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/infra/main.parameters.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "environmentName": {
+ "value": "${AZURE_ENV_NAME}"
+ },
+ "location": {
+ "value": "${AZURE_LOCATION}"
+ },
+ "principalId": {
+ "value": "${AZURE_PRINCIPAL_ID}"
+ },
+ "schedulerName": {
+ "value": "${DTS_SCHEDULER_NAME}"
+ },
+ "schedulerResourceGroupName": {
+ "value": "${DTS_SCHEDULER_RESOURCE_GROUP}"
+ },
+ "taskHubName": {
+ "value": "${DTS_TASK_HUB=default}"
+ }
+ }
+}
diff --git a/preview-features/on-demand-sandboxes/samples/python/main_app.py b/preview-features/on-demand-sandboxes/samples/python/main_app.py
new file mode 100644
index 00000000..44c65071
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/main_app.py
@@ -0,0 +1,258 @@
+"""Declarer app for the On-demand Sandboxes code-interpreter demo.
+
+A three-step Durable Task workflow:
+
+ generate_code (in-process, Azure OpenAI -> Python)
+ -> execute_code (on-demand sandbox, python3 + pandas, fanned out per region)
+ -> format_answer (in-process)
+
+Only ``execute_code`` runs in a DTS-managed sandbox; the LLM-generated Python is
+untrusted, so it never executes inside this process.
+"""
+
+import os
+import sys
+
+from azure.identity import DefaultAzureCredential, get_bearer_token_provider
+from openai import AzureOpenAI
+
+from durabletask import client, task
+from durabletask.azuremanaged.client import DurableTaskSchedulerClient
+from durabletask.azuremanaged.preview.sandboxes import (
+ SandboxActivitiesClient,
+ SandboxWorkerProfile,
+ sandbox_worker_profile,
+)
+from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
+
+from activities import EXECUTE_CODE, FORMAT_ANSWER, GENERATE_CODE
+
+
+SYSTEM_PROMPT = """\
+You are a Python code generator. Given a question about a sales dataset,
+produce a single self-contained Python script that:
+
+1. Reads /tmp/data.csv with pandas. The columns are: date, region, product, units, revenue.
+2. Assumes the CSV contains rows for exactly one region.
+3. Computes the total revenue for March 2025 in this subset.
+4. Prints ONLY the numeric revenue total to stdout. No code fences, no explanation, no commentary.
+
+Constraints:
+- Use only the Python standard library and pandas.
+- Do not access the network or filesystem outside /tmp.
+- If there is no March 2025 data in this subset, print 0.
+- Output must be plain text containing only the number.
+
+Respond with the Python script only. No markdown, no backticks.
+"""
+
+
+def _require(name: str) -> str:
+ value = os.getenv(name)
+ if value and value.strip():
+ return value.strip()
+ raise RuntimeError(f"Set {name} before running the sandbox demo.")
+
+
+def _strip_code_fences(code: str) -> str:
+ trimmed = code.strip()
+ if trimmed.startswith("```"):
+ newline = trimmed.find("\n")
+ if newline > 0:
+ trimmed = trimmed[newline + 1:]
+ if trimmed.endswith("```"):
+ trimmed = trimmed[:-3]
+ return trimmed.strip()
+
+
+def split_csv_by_region(csv_data: str) -> list[tuple[str, str]]:
+ """Partition the dataset deterministically so one generated script runs once per region."""
+ lines = [line for line in csv_data.replace("\r\n", "\n").split("\n") if line.strip()]
+ if len(lines) < 2:
+ return []
+
+ header = lines[0]
+ rows_by_region: dict[str, list[str]] = {}
+ for row in lines[1:]:
+ cells = row.split(",")
+ if len(cells) < 2:
+ continue
+ region = cells[1].strip()
+ rows_by_region.setdefault(region, []).append(row)
+
+ return [
+ (region, "\n".join([header, *rows_by_region[region]]))
+ for region in sorted(rows_by_region, key=str.casefold)
+ ]
+
+
+# --- In-process activities (run in this app's worker) -----------------------
+
+def generate_code(ctx: task.ActivityContext, question: str) -> str:
+ """Translate a natural-language question into a self-contained pandas script."""
+ endpoint = _require("AOAI_ENDPOINT")
+ deployment = _require("AOAI_DEPLOYMENT")
+
+ token_provider = get_bearer_token_provider(
+ DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default")
+ aoai = AzureOpenAI(
+ azure_endpoint=endpoint,
+ azure_ad_token_provider=token_provider,
+ api_version="2024-10-21")
+
+ completion = aoai.chat.completions.create(
+ model=deployment,
+ messages=[
+ {"role": "system", "content": SYSTEM_PROMPT},
+ {"role": "user", "content": question},
+ ])
+
+ code = _strip_code_fences(completion.choices[0].message.content or "")
+ print(f"[generate] AOAI returned {len(code.splitlines())} lines of Python:")
+ print("---")
+ print(code)
+ print("---")
+ return code
+
+
+def format_answer(ctx: task.ActivityContext, payload: dict) -> str:
+ """Aggregate the per-region sandbox results into a single answer."""
+ question = payload["question"]
+ totals: list[tuple[str, float]] = []
+ for result in payload["results"]:
+ region = result["region"]
+ if result["exit_code"] != 0:
+ return (f"Sandbox execution failed for region '{region}' "
+ f"(exit code {result['exit_code']}): {result['stderr']}")
+ stdout = (result["stdout"] or "").strip()
+ try:
+ revenue = float(stdout)
+ except ValueError:
+ return f"Sandbox execution returned a non-numeric result for region '{region}': {stdout}"
+ totals.append((region, revenue))
+
+ for region, revenue in sorted(totals, key=lambda item: item[1], reverse=True):
+ print(f"[fan-out] Region {region}: {revenue}")
+
+ top_region = sorted(totals, key=lambda item: (-item[1], item[0]))[0][0]
+ return f"Q: {question}\nA: {top_region}"
+
+
+# --- Orchestrator -----------------------------------------------------------
+
+def analyze_sales(ctx: task.OrchestrationContext, payload: dict):
+ """3-step workflow answering a question over a CSV using LLM-generated Python."""
+ question = payload["question"]
+ csv_data = payload["csv"]
+
+ # Generate one chunk-friendly Python script up front and reuse it for every region.
+ code = yield ctx.call_activity(GENERATE_CODE, input=question)
+
+ # Fan out: one sandbox execution per region-specific CSV partition.
+ chunks = split_csv_by_region(csv_data)
+ executions = [
+ ctx.call_activity(EXECUTE_CODE.name, input={"code": code, "csv": chunk_csv})
+ for _region, chunk_csv in chunks
+ ]
+
+ # Fan in: wait for every sandbox result, then hand the set to the formatter.
+ results = yield task.when_all(executions)
+ region_results = [
+ {"region": region, **result}
+ for (region, _csv), result in zip(chunks, results)
+ ]
+
+ return (yield ctx.call_activity(
+ FORMAT_ANSWER,
+ input={"question": question, "results": region_results}))
+
+
+# --- Sandbox worker profile -------------------------------------------------
+
+worker_profile_id = os.getenv("DTS_WORKER_PROFILE_ID", "code-executor")
+container_image = os.getenv("DTS_SANDBOX_CONTAINER_IMAGE") or "dts-codegen-sandbox-python:local"
+image_pull_managed_identity_client_id = _require("DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID")
+scheduler_managed_identity_client_id = _require("DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID")
+
+
+@sandbox_worker_profile(worker_profile_id)
+class CodeSandboxWorkerProfile(SandboxWorkerProfile):
+ """Declares the on-demand sandbox that hosts ``execute_code`` in an isolated container."""
+
+ def configure(self, options) -> None:
+ options.image.image_ref = container_image
+ options.image.managed_identity_client_id = image_pull_managed_identity_client_id
+ options.scheduler_managed_identity_client_id = scheduler_managed_identity_client_id
+ options.cpu = "1000m"
+ options.memory = "2048Mi"
+ options.max_concurrent_activities = 1
+ options.add_activity(EXECUTE_CODE.name, version=EXECUTE_CODE.version)
+
+
+# --- Entry point ------------------------------------------------------------
+
+def main() -> int:
+ endpoint = _require("DTS_ENDPOINT")
+ taskhub = os.getenv("DTS_TASK_HUB", "default")
+ csv_path = os.getenv("DEMO_CSV_PATH") or os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), "data", "sales_q1.csv")
+ question = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else (
+ "Which region had the highest total revenue in March 2025?")
+
+ if not os.path.exists(csv_path):
+ print(f"CSV file not found at: {csv_path}", file=sys.stderr)
+ return 1
+
+ with open(csv_path, encoding="utf-8") as handle:
+ csv_data = handle.read()
+
+ # Print demo context so the audience understands the dataset before orchestration starts.
+ all_lines = csv_data.splitlines()
+ headers = all_lines[0].split(",")
+ print(f"[demo] Dataset: {os.path.abspath(csv_path)}")
+ print(f"[demo] {len(all_lines) - 1} rows x {len(headers)} columns: [{', '.join(headers)}]")
+ print(f"[demo] Question: {question}\n")
+
+ secure_channel = endpoint.startswith("https://") or endpoint.startswith("grpcs://")
+ credential = DefaultAzureCredential() if secure_channel else None
+
+ # Declare the sandbox worker profile with DTS so it can route execute_code to a sandbox.
+ sandbox_client = SandboxActivitiesClient(
+ host_address=endpoint,
+ secure_channel=secure_channel,
+ taskhub=taskhub,
+ token_credential=credential)
+ sandbox_client.enable_sandbox_activities()
+
+ with DurableTaskSchedulerWorker(
+ host_address=endpoint,
+ secure_channel=secure_channel,
+ taskhub=taskhub,
+ token_credential=credential) as worker:
+ worker.add_orchestrator(analyze_sales)
+ worker.add_activity(generate_code)
+ worker.add_activity(format_answer)
+ worker.use_work_item_filters()
+ worker.start()
+
+ durable_client = DurableTaskSchedulerClient(
+ host_address=endpoint,
+ secure_channel=secure_channel,
+ taskhub=taskhub,
+ token_credential=credential)
+ instance_id = durable_client.schedule_new_orchestration(
+ analyze_sales,
+ input={"question": question, "csv": csv_data})
+ print(f"Started orchestration: {instance_id}")
+
+ state = durable_client.wait_for_orchestration_completion(instance_id, timeout=300)
+ print(f"Status: {state.runtime_status if state else 'unknown'}\n")
+ if state and state.runtime_status == client.OrchestrationStatus.COMPLETED:
+ print(state.serialized_output)
+ elif state and state.failure_details:
+ print(f"[failure] {state.failure_details}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/preview-features/on-demand-sandboxes/samples/python/manifests/deployment.tmpl.yaml b/preview-features/on-demand-sandboxes/samples/python/manifests/deployment.tmpl.yaml
new file mode 100644
index 00000000..fa8cf849
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/manifests/deployment.tmpl.yaml
@@ -0,0 +1,57 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: mainapp
+ namespace: default
+ annotations:
+ azure.workload.identity/client-id: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ labels:
+ azure.workload.identity/use: "true"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mainapp
+ namespace: default
+ labels:
+ app: mainapp
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mainapp
+ template:
+ metadata:
+ labels:
+ app: mainapp
+ azure.workload.identity/use: "true"
+ spec:
+ serviceAccountName: mainapp
+ containers:
+ - name: mainapp
+ image: {{ .Env.SERVICE_MAINAPP_IMAGE_NAME }}
+ env:
+ - name: DTS_ENDPOINT
+ value: {{ .Env.DTS_ENDPOINT }}
+ - name: DTS_TASK_HUB
+ value: {{ .Env.DTS_TASK_HUB }}
+ - name: AOAI_ENDPOINT
+ value: {{ .Env.AOAI_ENDPOINT }}
+ - name: AOAI_DEPLOYMENT
+ value: {{ .Env.AOAI_DEPLOYMENT }}
+ - name: AZURE_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ # The sandbox worker profile (main_app.py) reads these:
+ - name: DTS_SANDBOX_CONTAINER_IMAGE
+ value: {{ .Env.DTS_SANDBOX_CONTAINER_IMAGE }}
+ - name: DTS_SANDBOX_IMAGE_PULL_UMI_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ - name: DTS_SANDBOX_SCHEDULER_UMI_CLIENT_ID
+ value: {{ .Env.AZURE_USER_ASSIGNED_IDENTITY_CLIENT_ID }}
+ resources:
+ requests:
+ cpu: 250m
+ memory: 256Mi
+ limits:
+ cpu: "1"
+ memory: 512Mi
diff --git a/preview-features/on-demand-sandboxes/samples/python/remote_worker.py b/preview-features/on-demand-sandboxes/samples/python/remote_worker.py
new file mode 100644
index 00000000..33102d06
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/remote_worker.py
@@ -0,0 +1,115 @@
+"""Remote worker image entrypoint for the On-demand Sandboxes code-interpreter demo.
+
+Runs the untrusted, LLM-generated Python in an isolated DTS-managed sandbox.
+Each invocation writes the CSV partition and generated script to /tmp and shells
+out to python3, capturing stdout/stderr and the exit code.
+"""
+
+import os
+import subprocess
+import threading
+import uuid
+
+from durabletask import task
+from durabletask.azuremanaged.preview.sandboxes import SandboxWorker
+
+from activities import EXECUTE_CODE
+
+EXECUTION_TIMEOUT_SECONDS = 30
+MAX_DISPLAY_LINES = 30
+
+
+def execute_code(ctx: task.ActivityContext, payload: dict) -> dict:
+ """Activity that runs the generated pandas script inside the sandbox container."""
+ sandbox_name = os.getenv("DTS_SANDBOX_ID") or os.uname().nodename
+ python_code = payload["code"]
+ csv_data = payload["csv"]
+
+ print(f"[sandbox] Starting execute_code in sandbox '{sandbox_name}' (pid={os.getpid()})")
+ print("[sandbox] This is isolated on-demand sandbox compute managed by DTS.")
+
+ code_lines = python_code.split("\n")
+ byte_count = len(python_code.encode("utf-8"))
+ print(f"[sandbox] Received generated Python ({len(code_lines)} lines, {byte_count} bytes)")
+ print("[sandbox] --- generated script ---")
+ for line in code_lines[:MAX_DISPLAY_LINES]:
+ print(line)
+ if len(code_lines) > MAX_DISPLAY_LINES:
+ print(f"... (truncated, {len(code_lines) - MAX_DISPLAY_LINES} more lines)")
+ print("[sandbox] --- end script ---")
+
+ work_dir = os.path.join("/tmp", f"run-{uuid.uuid4().hex}")
+ os.makedirs(work_dir, exist_ok=True)
+ print(f"[sandbox] Created isolated work directory: {work_dir}")
+
+ csv_path = os.path.join(work_dir, "data.csv")
+ script_path = os.path.join(work_dir, "script.py")
+ with open(csv_path, "w", encoding="utf-8") as handle:
+ handle.write(csv_data)
+ with open(script_path, "w", encoding="utf-8") as handle:
+ handle.write(python_code)
+ print(f"[sandbox] Wrote dataset: {csv_path}")
+ print(f"[sandbox] Wrote generated script: {script_path}")
+
+ # The generated script reads /tmp/data.csv. Copy into the canonical location
+ # so the LLM doesn't need to know about per-invocation working directories.
+ with open("/tmp/data.csv", "w", encoding="utf-8") as handle:
+ handle.write(csv_data)
+ print("[sandbox] Mounted dataset at expected path: /tmp/data.csv")
+
+ csv_lines = [line for line in csv_data.split("\n") if line.strip()]
+ if csv_lines:
+ csv_headers = csv_lines[0].split(",")
+ print(f"[sandbox] Dataset loaded: {len(csv_lines) - 1} rows x "
+ f"{len(csv_headers)} columns [{', '.join(csv_headers)}]")
+
+ print(f"[sandbox] Executing command: python3 {script_path}")
+ try:
+ completed = subprocess.run(
+ ["python3", script_path],
+ cwd=work_dir,
+ capture_output=True,
+ text=True,
+ timeout=EXECUTION_TIMEOUT_SECONDS,
+ check=False)
+ except subprocess.TimeoutExpired:
+ print(f"[sandbox] ERROR: Timeout: execution exceeded {EXECUTION_TIMEOUT_SECONDS} seconds.")
+ return {
+ "stdout": "",
+ "stderr": f"Execution timed out after {EXECUTION_TIMEOUT_SECONDS} seconds.",
+ "exit_code": 124,
+ }
+
+ stdout = completed.stdout or ""
+ stderr = completed.stderr or ""
+ print(f"[sandbox] Python process completed (exit code {completed.returncode})")
+ if stdout.strip():
+ print("[sandbox] stdout from generated script:")
+ print(stdout.rstrip())
+ else:
+ print("[sandbox] stdout from generated script: ")
+ if completed.returncode != 0 and stderr.strip():
+ print(f"[sandbox] ERROR: {stderr.rstrip()}")
+ if completed.returncode == 0:
+ print("[sandbox] Returning captured stdout to the orchestrator.")
+
+ return {"stdout": stdout, "stderr": stderr, "exit_code": completed.returncode}
+
+
+execute_code.__name__ = EXECUTE_CODE.name
+
+
+def main() -> None:
+ with SandboxWorker() as worker:
+ worker.add_activity(execute_code, version=EXECUTE_CODE.version)
+ worker.start()
+ print("Python on-demand sandbox worker is running. Press Ctrl+C to stop.")
+ try:
+ threading.Event().wait()
+ except KeyboardInterrupt:
+ # Expected on Ctrl+C: let the context manager stop the worker gracefully.
+ pass
+
+
+if __name__ == "__main__":
+ main()
diff --git a/preview-features/on-demand-sandboxes/samples/python/requirements.txt b/preview-features/on-demand-sandboxes/samples/python/requirements.txt
new file mode 100644
index 00000000..4b56dadc
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/requirements.txt
@@ -0,0 +1,5 @@
+# Declarer-app (main_app.py) dependencies.
+durabletask==1.6.0
+durabletask-azuremanaged==1.6.0
+azure-identity>=1.16
+openai>=1.40
diff --git a/preview-features/on-demand-sandboxes/samples/python/scripts/acr-build.sh b/preview-features/on-demand-sandboxes/samples/python/scripts/acr-build.sh
new file mode 100755
index 00000000..6f93d402
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/scripts/acr-build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Builds the two container images for the On-demand Sandboxes demo server-side using
+# ACR Tasks (az acr build) — no local Docker required. Called by azd as a predeploy hook.
+#
+# - main-app : the orchestrator (main_app.py), deployed to AKS (azd reads
+# SERVICE_MAINAPP_IMAGE_NAME and skips its own build/push).
+# - sandbox : the worker image (remote_worker.py) DTS starts on demand. Not deployed
+# to AKS; its full image reference is handed to the app via
+# DTS_SANDBOX_CONTAINER_IMAGE.
+
+set -euo pipefail
+
+REGISTRY="${AZURE_CONTAINER_REGISTRY_NAME:?AZURE_CONTAINER_REGISTRY_NAME must be set}"
+REGISTRY_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:?AZURE_CONTAINER_REGISTRY_ENDPOINT must be set}"
+ENV_NAME="${AZURE_ENV_NAME:?AZURE_ENV_NAME must be set}"
+TAG="azd-deploy-$(date +%s)"
+
+build() {
+ local image_repo="$1" # e.g. dts-ondemand-sandboxes/main-app-
+ local containerfile="$2"
+ local full_image="${REGISTRY_ENDPOINT}/${image_repo}:${TAG}"
+
+ echo "==> Building ${image_repo}:${TAG} via ACR Tasks (--platform linux/amd64)..." >&2
+ az acr build \
+ --registry "${REGISTRY}" \
+ --image "${image_repo}:${TAG}" \
+ --platform linux/amd64 \
+ --file "${containerfile}" \
+ . \
+ --no-logs \
+ --output none >&2
+
+ echo "${full_image}"
+}
+
+MAIN_APP_IMAGE="$(build "dts-ondemand-sandboxes/main-app-${ENV_NAME}" "Containerfile.mainapp")"
+SANDBOX_IMAGE="$(build "dts-ondemand-sandboxes/sandbox-worker-${ENV_NAME}" "Containerfile")"
+
+# azd uses SERVICE__IMAGE_NAME to skip its own build and deploy this image instead.
+azd env set SERVICE_MAINAPP_IMAGE_NAME "${MAIN_APP_IMAGE}"
+# The app declares the sandbox worker profile using this image reference.
+azd env set DTS_SANDBOX_CONTAINER_IMAGE "${SANDBOX_IMAGE}"
+
+echo "==> main-app image : ${MAIN_APP_IMAGE}"
+echo "==> sandbox image : ${SANDBOX_IMAGE}"
diff --git a/preview-features/on-demand-sandboxes/samples/python/scripts/attach-scheduler-identity.sh b/preview-features/on-demand-sandboxes/samples/python/scripts/attach-scheduler-identity.sh
new file mode 100755
index 00000000..3c216af4
--- /dev/null
+++ b/preview-features/on-demand-sandboxes/samples/python/scripts/attach-scheduler-identity.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# Attaches the sample's user-assigned managed identity to the existing Durable Task
+# Scheduler so DTS can use it to pull the sandbox image and let the sandbox worker
+# connect back. Runs as an azd postprovision hook. The PATCH is merge-safe: it keeps
+# any identities already attached to the scheduler.
+#
+# NOTE: enabling the On-demand Sandboxes preview *feature* on the scheduler is a
+# separate, out-of-band step handled during private-preview onboarding.
+
+set -euo pipefail
+
+SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:?AZURE_SUBSCRIPTION_ID must be set}"
+SCHEDULER_NAME="${DTS_SCHEDULER_NAME:?DTS_SCHEDULER_NAME must be set}"
+SCHEDULER_RG="${DTS_SCHEDULER_RESOURCE_GROUP:?DTS_SCHEDULER_RESOURCE_GROUP must be set}"
+IDENTITY_ID="${AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID:?AZURE_USER_ASSIGNED_IDENTITY_RESOURCE_ID must be set}"
+API_VERSION="2026-05-01-preview"
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "ERROR: python3 is required to merge the scheduler identity block." >&2
+ exit 1
+fi
+
+URI="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${SCHEDULER_RG}/providers/Microsoft.DurableTask/schedulers/${SCHEDULER_NAME}?api-version=${API_VERSION}"
+
+echo "==> Reading current identity on scheduler '${SCHEDULER_NAME}'..."
+CURRENT="$(az rest --method get --uri "${URI}")"
+
+BODY="$(IDENTITY_ID="${IDENTITY_ID}" python3 - "${CURRENT}" <<'PY'
+import json, os, sys
+
+current = json.loads(sys.argv[1])
+identity_id = os.environ["IDENTITY_ID"]
+
+identity = current.get("identity") or {}
+user_assigned = identity.get("userAssignedIdentities") or {}
+user_assigned[identity_id] = {}
+
+current_type = identity.get("type", "") or ""
+new_type = "SystemAssigned, UserAssigned" if "SystemAssigned" in current_type else "UserAssigned"
+
+print(json.dumps({"identity": {"type": new_type, "userAssignedIdentities": user_assigned}}))
+PY
+)"
+
+TMP="$(mktemp)"
+trap 'rm -f "${TMP}"' EXIT
+printf '%s' "${BODY}" > "${TMP}"
+
+echo "==> Attaching managed identity to scheduler..."
+az rest --method patch --uri "${URI}" --body "@${TMP}" >/dev/null
+echo "==> Done. Identity ${IDENTITY_ID##*/} is attached to '${SCHEDULER_NAME}'."