---
title: A model decides
description: Write a Workflow in which a model chooses the next state, and answer its question at a Gate.
---

# A model decides

At the end of this page, you have a [Workflow](../reference/glossary.md#workflow), `triage`, in which a model makes a
decision. An [Agent](../reference/glossary.md#agent) reads a feature request and picks one of three events. When it asks
a question, the run waits at a [Gate](../reference/glossary.md#gate) until you answer. You answer one run from the CLI
and one run from the [Console](../reference/glossary.md#console).

## Before you start

Do [Connect a model](connect-a-model.md) first. The [Instance](../reference/glossary.md#instance) `my-instance` must
have the model of your tab.

You need the port forward of [Create an Instance](create-an-instance.md). Keep it open in its terminal. Then open a new
terminal in `my-instance` for `jr2 send`. This page calls it the send terminal. Give the other commands in your first
terminal, in `my-instance`.

## Write the Workflow

Make the file `workflows/triage.ts` in `my-instance`. The name of the file is the name of the Workflow, so the Workflow
is `triage`. Copy this code into the file:

**Anthropic**

```ts
import { assign } from "xstate";
import { z } from "zod";
import { agent, defineEvent, jr2Setup } from "@jr2/orchestrator";

// The run input: what you give `jr2 run triage --input`. The Console makes its start form from it.
const door = z.object({
  request: z.string().describe("A feature request, in the requester's own words."),
});

// The Agent's three picks. Each one is a tool that the model can call.
const accept = defineEvent({
  name: "accept",
  description: "The request is clear enough to build: it names what to change, where, and how to tell it is done.",
  input: z.object({ summary: z.string().describe("The request restated as one actionable sentence.") }),
});
const needsInfo = defineEvent({
  name: "needs_info",
  description: "The request could be built more than one way, or its outcome cannot be checked. Ask one question.",
  input: z.object({ question: z.string().describe("The one question whose answer would make it buildable.") }),
});
const reject = defineEvent({
  name: "reject",
  description: "The request is not a change to software, or it asks for something harmful.",
  input: z.object({ reason: z.string().describe("Why, in one sentence.") }),
});
// Your one reply. Only a Gate takes it, so the Agent can never send it.
const answer = defineEvent({
  name: "answer",
  description: "Answer the triager's question. It triages the request again with the answer in hand.",
  input: z.object({ info: z.string().describe("The answer, in prose.") }),
});

type Ctx = z.infer<typeof door> & {
  // Each question that the Agent asked, and the answer that it got.
  exchange: { question: string; info?: string }[];
  // Why the last Turn ended with no pick, when it did.
  fault?: string;
  summary?: string;
  reason?: string;
};
// The output of the run: the decision. The job of a triage is the decision itself, so "rejected" is a
// correct answer: a final state, with the reason in the output, and not a Failure.
type Output = { outcome: "accepted"; summary: string } | { outcome: "rejected"; reason: string };

export const machine = jr2Setup({
  types: {} as { context: Ctx; input: z.infer<typeof door>; output: Output },
  events: [accept, needsInfo, reject, answer],
  actors: {
    triager: agent({
      // The provider, then the model. jr2.config.ts holds the key for the provider.
      model: "anthropic/claude-haiku-5-5",
      description: "Reads one feature request and decides whether it can be built as written.",
      // No Sandbox and no Working tools: the Agent can only read its prompt and pick.
      workspace: "none",
      thinkingLevel: "off",
      instructions: `You triage feature requests for a small software team. You read one request and
decide, by calling exactly one tool:

- accept — it names WHAT to change, WHERE (which command, screen, or file), and how to tell it
  is done.
- needs_info — any of those is missing. Ask the ONE question that would settle it. Never guess
  what the requester meant: if you would have to assume, ask.
- reject — it is not a change to software, or it asks for something harmful.

You have no tools but these three, and you MUST end your turn by calling one of them.`,
    }),
  },
}).createMachine({
  id: "triage",
  input: door,
  context: ({ input }) => ({ request: input.request, exchange: [] }),
  initial: "triaging",
  states: {
    // The Agent's Menu is the events that this state takes: accept, needs_info and reject.
    triaging: {
      invoke: { src: "triager", input: ({ context }) => ({ prompt: prompt(context) }) },
      on: {
        accept: { target: "accepted", actions: assign({ summary: ({ event }) => event.summary }) },
        reject: { target: "rejected", actions: assign({ reason: ({ event }) => event.reason }) },
        needs_info: {
          target: "clarify",
          actions: assign({
            exchange: ({ context, event }) => [...context.exchange, { question: event.question }],
            fault: undefined,
          }),
        },
        // The Turn failed, with no pick. Wait at the Gate, so that your answer starts a new Turn.
        "agent.fault": { target: "clarify", actions: assign({ fault: ({ event }) => event.reason }) },
      },
    },
    // The Gate. Its name is the name of this state, and it takes the events of this state: answer.
    clarify: {
      invoke: {
        src: "gate",
        input: ({ context }) => ({
          meta:
            context.fault !== undefined ? { fault: context.fault } : { question: context.exchange.at(-1)!.question },
        }),
      },
      on: {
        answer: {
          target: "triaging",
          actions: assign({
            exchange: ({ context, event }) =>
              context.fault !== undefined
                ? context.exchange
                : [...context.exchange.slice(0, -1), { ...context.exchange.at(-1)!, info: event.info }],
            fault: undefined,
          }),
        },
      },
    },
    accepted: { type: "final", output: ({ context }): Output => ({ outcome: "accepted", summary: context.summary! }) },
    rejected: { type: "final", output: ({ context }): Output => ({ outcome: "rejected", reason: context.reason! }) },
  },
  // xstate reads only this root `output`. It gets the output of the final state as `event.output`.
  output: ({ event }) => event.output as Output,
});

// The prompt of each Turn: the request, then each question and its answer.
function prompt(context: Ctx): string {
  const answered = context.exchange
    .filter((e) => e.info !== undefined)
    .map((e) => `\n\nYou asked: ${e.question}\nThe requester answered: ${e.info}`)
    .join("");
  return `Feature request:\n${context.request}${answered}`;
}
```

**Local**

```ts
import { assign } from "xstate";
import { z } from "zod";
import { agent, defineEvent, jr2Setup } from "@jr2/orchestrator";

// The run input: what you give `jr2 run triage --input`. The Console makes its start form from it.
const door = z.object({
  request: z.string().describe("A feature request, in the requester's own words."),
});

// The Agent's three picks. Each one is a tool that the model can call.
const accept = defineEvent({
  name: "accept",
  description: "The request is clear enough to build: it names what to change, where, and how to tell it is done.",
  input: z.object({ summary: z.string().describe("The request restated as one actionable sentence.") }),
});
const needsInfo = defineEvent({
  name: "needs_info",
  description: "The request could be built more than one way, or its outcome cannot be checked. Ask one question.",
  input: z.object({ question: z.string().describe("The one question whose answer would make it buildable.") }),
});
const reject = defineEvent({
  name: "reject",
  description: "The request is not a change to software, or it asks for something harmful.",
  input: z.object({ reason: z.string().describe("Why, in one sentence.") }),
});
// Your one reply. Only a Gate takes it, so the Agent can never send it.
const answer = defineEvent({
  name: "answer",
  description: "Answer the triager's question. It triages the request again with the answer in hand.",
  input: z.object({ info: z.string().describe("The answer, in prose.") }),
});

type Ctx = z.infer<typeof door> & {
  // Each question that the Agent asked, and the answer that it got.
  exchange: { question: string; info?: string }[];
  // Why the last Turn ended with no pick, when it did.
  fault?: string;
  summary?: string;
  reason?: string;
};
// The output of the run: the decision. The job of a triage is the decision itself, so "rejected" is a
// correct answer: a final state, with the reason in the output, and not a Failure.
type Output = { outcome: "accepted"; summary: string } | { outcome: "rejected"; reason: string };

export const machine = jr2Setup({
  types: {} as { context: Ctx; input: z.infer<typeof door>; output: Output },
  events: [accept, needsInfo, reject, answer],
  actors: {
    triager: agent({
      // `local` is the provider id in jr2.config.ts. The rest is the name that the endpoint serves.
      model: "local/qwen3.6-35b-a3b",
      description: "Reads one feature request and decides whether it can be built as written.",
      // No Sandbox and no Working tools: the Agent can only read its prompt and pick.
      workspace: "none",
      thinkingLevel: "off",
      instructions: `You triage feature requests for a small software team. You read one request and
decide, by calling exactly one tool:

- accept — it names WHAT to change, WHERE (which command, screen, or file), and how to tell it
  is done.
- needs_info — any of those is missing. Ask the ONE question that would settle it. Never guess
  what the requester meant: if you would have to assume, ask.
- reject — it is not a change to software, or it asks for something harmful.

You have no tools but these three, and you MUST end your turn by calling one of them.`,
    }),
  },
}).createMachine({
  id: "triage",
  input: door,
  context: ({ input }) => ({ request: input.request, exchange: [] }),
  initial: "triaging",
  states: {
    // The Agent's Menu is the events that this state takes: accept, needs_info and reject.
    triaging: {
      invoke: { src: "triager", input: ({ context }) => ({ prompt: prompt(context) }) },
      on: {
        accept: { target: "accepted", actions: assign({ summary: ({ event }) => event.summary }) },
        reject: { target: "rejected", actions: assign({ reason: ({ event }) => event.reason }) },
        needs_info: {
          target: "clarify",
          actions: assign({
            exchange: ({ context, event }) => [...context.exchange, { question: event.question }],
            fault: undefined,
          }),
        },
        // The Turn failed, with no pick. Wait at the Gate, so that your answer starts a new Turn.
        "agent.fault": { target: "clarify", actions: assign({ fault: ({ event }) => event.reason }) },
      },
    },
    // The Gate. Its name is the name of this state, and it takes the events of this state: answer.
    clarify: {
      invoke: {
        src: "gate",
        input: ({ context }) => ({
          meta:
            context.fault !== undefined ? { fault: context.fault } : { question: context.exchange.at(-1)!.question },
        }),
      },
      on: {
        answer: {
          target: "triaging",
          actions: assign({
            exchange: ({ context, event }) =>
              context.fault !== undefined
                ? context.exchange
                : [...context.exchange.slice(0, -1), { ...context.exchange.at(-1)!, info: event.info }],
            fault: undefined,
          }),
        },
      },
    },
    accepted: { type: "final", output: ({ context }): Output => ({ outcome: "accepted", summary: context.summary! }) },
    rejected: { type: "final", output: ({ context }): Output => ({ outcome: "rejected", reason: context.reason! }) },
  },
  // xstate reads only this root `output`. It gets the output of the final state as `event.output`.
  output: ({ event }) => event.output as Output,
});

// The prompt of each Turn: the request, then each question and its answer.
function prompt(context: Ctx): string {
  const answered = context.exchange
    .filter((e) => e.info !== undefined)
    .map((e) => `\n\nYou asked: ${e.question}\nThe requester answered: ${e.info}`)
    .join("");
  return `Feature request:\n${context.request}${answered}`;
}
```

The two tabs are different only in the `model` line and its comment. The file has five parts:

- **The run input.** `door` is a zod schema. A run of `triage` starts with one `request`.
- **The Vocabulary.** Each [`defineEvent()`](../reference/authoring-surface/defineevent.md) makes one event of the
  [Vocabulary](../reference/glossary.md#vocabulary). Each transition belongs to the party that its state invokes.
  `triaging` invokes the Agent, so only the Agent can send `accept`, `needs_info` and `reject`. `clarify` invokes a
  Gate, so only the Gate can take `answer`.
- **The Agent.** [`agent()`](../reference/authoring-surface/agent.md) makes the Agent `triager`, with its model and its
  instructions. `workspace: "none"` gives it no [Sandbox](../reference/glossary.md#sandbox) and no
  [Working tools](../reference/glossary.md#working-tools). It can only read its prompt and pick an event.
  `thinkingLevel: "off"` makes the model pick with no thinking step. [Agents and Turns](../concepts/agents-and-turns.md)
  explains the other fields.
- **The Machine.** [`jr2Setup()`](../reference/authoring-surface/jr2setup.md) takes the Vocabulary and the Agent. Then
  `createMachine` gives the states of the [Machine](../reference/glossary.md#machine).
- **The prompt.** `prompt()` makes the text of each [Turn](../reference/glossary.md#turn) from the request and each
  question and answer.

The state `triaging` invokes `triager`. Its transitions take `accept`, `needs_info` and `reject`, so these three events
are the [Menu](../reference/glossary.md#menu) of the Agent. The model sees each event as a tool, with its `description`
and its input schema. The Agent ends its Turn when it calls one of them. The fourth transition,
[`agent.fault`](../reference/mechanism-events.md#agent-fault), is not on the Menu. jr2 sends it when a Turn fails with
no pick. Then the run waits at the Gate, and the Gate shows a `fault` in place of a question.

The state `clarify` invokes [`gate`](../reference/authoring-surface/jr2setup.md), which jr2 supplies. Its `meta` holds
the question of the Agent, and its one transition takes `answer`. The Gate has the name of its state: `clarify`. When
you send `answer`, the run goes back to `triaging`. The next Turn gets the request, the question, and your answer in its
prompt.

## Converge the Instance

[`jr2 up`](../reference/cli.md#up) publishes the Workflows to the [Orchestrator](../reference/glossary.md#orchestrator).
Thus you run it each time that you change a Workflow. In the first terminal, run:

```sh
jr2 up
```

`jr2 up` prints who can send each event, in each state of `triage`. Then it converges the cluster.

**Anthropic**

The output shows where the model key goes. Then `jr2 up` asks your model for one tool call, from inside the cluster:

```text
→ .env: ANTHROPIC_API_KEY
jr2 up — instance "my-instance" → context kind-jr2 / namespace my-instance
typecheck: tsc --noEmit (the instance's own compiler)
seats: workflow "triage" — who may send what, per state (ADR-0015)
  triage.triaging agent:triager: [accept, needs_info, reject]
  triage.clarify gate: [answer]
agents: triager (carried by 2 workflow machine(s))
…
secret ANTHROPIC_API_KEY: Held in the Instance Harness
the Instance Harness: Custodian holds ANTHROPIC_API_KEY (provider)
held secret ANTHROPIC_API_KEY → api.anthropic.com (literal)
held secrets: minted the Instance's CA into jr2-held-ca
provider: checking anthropic/claude-haiku-5-5 on the Instance Harness, through a Custodian that holds ANTHROPIC_API_KEY for api.anthropic.com
provider: the check pod has up to 5 min to start, then up to 6 min for its model
…
provider: anthropic/claude-haiku-5-5 reachable, and it completed a tool call
…
instance harness: converging (triager declares workspace: "none")
instance harness: verified — 1 pod(s) running ghcr.io/snapwich/jr2-harness:<version>
…
converged — `jr2 run <workflow>` when ready
```

The warning of Connect a model, `secrets[0] (ANTHROPIC_API_KEY) is named by nothing; goes nowhere`, is gone. `triager`
names an Anthropic model, so `jr2 up` gives the key to the [Custodian](../reference/glossary.md#custodian) of the pod
that runs its Turns. Then it checks the model through a Custodian that holds the key. If the check fails, `jr2 up` stops
and prints what to examine.

**Local**

`jr2 up` asks your model for one tool call, from inside the cluster:

```text
→ .env: LOCAL_MODEL_URL
jr2 up — instance "my-instance" → context kind-jr2 / namespace my-instance
typecheck: tsc --noEmit (the instance's own compiler)
seats: workflow "triage" — who may send what, per state (ADR-0015)
  triage.triaging agent:triager: [accept, needs_info, reject]
  triage.clarify gate: [answer]
agents: triager (carried by 2 workflow machine(s))
…
the Instance Harness: Custodian holds nothing
provider: checking local/qwen3.6-35b-a3b on the Instance Harness, with no held key (harness.provider has no apiKey), direct, as a pod that holds no secret calls it
provider: the check pod has up to 5 min to start, then up to 6 min for its model
…
provider: local/qwen3.6-35b-a3b reachable, and it completed a tool call
…
instance harness: converging (triager declares workspace: "none")
instance harness: verified — 1 pod(s) running ghcr.io/snapwich/jr2-harness:<version>
…
converged — `jr2 run <workflow>` when ready
```

The line of Connect a model, `provider: <url> configured, but no carried Agent names a "local/…" model — skipped`, is
gone. `triager` names a `local/…` model, so `jr2 up` checks that the endpoint completes a tool call. If the check fails,
`jr2 up` stops and prints what to examine.

The Orchestrator loads the new Workflows with no restart, so the port forward stays open.

## Start the first run

In the first terminal, run:

```sh
jr2 run triage --input '{"request":"People keep asking what version they are running."}'
```

[`jr2 run`](../reference/cli.md#run) starts the run and stays attached to it. The Agent reads the request. The request
does not say where to show the version, so the Agent normally asks a question. The run goes to `clarify` and waits
there:

```text
…
→ context kind-jr2 / namespace my-instance
run 8fecb896-2100-4ee6-88d6-788eaf0d7244 (triage) — attached
  → active "triaging"
  → active "triaging"
  → active "clarify"
```

The output of each `jr2` command starts with the line `→ .env:`, with the keys that the CLI took from `.env`. From this
step on, the output on this page shows `…` in place of that line. Your run id is different. The line
`→ active "triaging"` can show more than one time. `jr2 run` prints a line for each status that the run sends. Two lines
can show the same state. `jr2 run` does not return while the run waits. Keep it open.

> **Note**
>
> The model decides, so your run can be different. If the Agent picks `accept` or `reject`, the run ends with no Gate. A
> request that says what to change and how to tell that it is done is clear, and the Agent accepts it immediately. For
> example, this run started with the request
> `Add a --version flag to the CLI. It prints the installed version and exits.`:
>
> ```text
> …
> → context kind-jr2 / namespace my-instance
> run 89cc6ea5-9e2d-4f8a-8d02-18a6e47f2a5c (triage) — attached
>   → active "triaging"
>   → done "accepted"
> {"runId":"89cc6ea5-9e2d-4f8a-8d02-18a6e47f2a5c","workflow":"triage","instanceId":"fac049b9-9dd2-4ba5-aca8-0b3ebe619701","status":"done","value":"accepted","context":{"request":"Add a --version flag to the CLI. It prints the installed version and exits.","exchange":[],"summary":"Add a --version flag to the CLI that prints the installed version and exits."},"children":[],"waiting":[],"held":[],"assembly":"3eb6cd4d61bf","output":{"outcome":"accepted","summary":"Add a --version flag to the CLI that prints the installed version and exits."}}
> ```
>
> Your run id and your [Instance ID](../reference/glossary.md#instance-id) are different, and so is the `summary`, the
> text of the model. If your run with the first request ends with no Gate, start it again until it waits at `clarify`.

## Read the question

The Gate holds the question of the Agent in its `meta`. In the send terminal, run:

```sh
jr2 status <run id>
```

`<run id>` is the run id that `jr2 run` printed. You can give its first 4 characters or more, such as `8fecb896`.
[`jr2 status`](../reference/cli.md#status) prints the status of the run as one line of JSON. The Gate is in `gates`, at
the end:

```text
…
→ context kind-jr2 / namespace my-instance
{"runId":"8fecb896-2100-4ee6-88d6-788eaf0d7244","workflow":"triage","instanceId":"a814d943-9b27-410c-a790-2c4225aa05ba","status":"active","value":"clarify","context":{"request":"People keep asking what version they are running.","exchange":[{"question":"Where specifically should the version number be displayed (e.g., CLI flag, About screen, or help output), and what exact user action should trigger it?"}]},"children":[],"waiting":[],"held":[],"assembly":"3eb6cd4d61bf","gates":[{"gate":"clarify","path":["clarify"],"accepts":[{"name":"answer","description":"Answer the triager's question. It triages the request again with the answer in hand.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"info":{"type":"string","description":"The answer, in prose."}},"required":["info"],"additionalProperties":false}}],"acceptsNow":["answer"],"meta":{"question":"Where specifically should the version number be displayed (e.g., CLI flag, About screen, or help output), and what exact user action should trigger it?"}}]}
```

The `question` is in the words of the model, so your question is different. Your instance id is different too. The Gate
`clarify` takes `answer`, with an `info` string.

## Answer from the CLI

In the send terminal, run:

```sh
jr2 send <run id> --gate clarify --event answer --input '{"info":"Add a --version flag to the CLI. It prints the installed version and exits."}'
```

[`jr2 send`](../reference/cli.md#send) gives the event to the Gate:

```text
…
→ context kind-jr2 / namespace my-instance
delivered answer to gate "clarify" on 8fecb896-2100-4ee6-88d6-788eaf0d7244
```

The run goes back to `triaging`, and the Agent reads your answer. Look at the first terminal. When the Agent picks
`accept`, the run ends, and `jr2 run` prints the last status:

```text
…
  → active "triaging"
  → active "triaging"
  → done "accepted"
{"runId":"8fecb896-2100-4ee6-88d6-788eaf0d7244","workflow":"triage","instanceId":"a814d943-9b27-410c-a790-2c4225aa05ba","status":"done","value":"accepted","context":{"request":"People keep asking what version they are running.","exchange":[{"question":"Where specifically should the version number be displayed (e.g., CLI flag, About screen, or help output), and what exact user action should trigger it?","info":"Add a --version flag to the CLI. It prints the installed version and exits."}],"summary":"Add a --version flag to the CLI that prints the installed version and exits."},"children":[],"waiting":[],"held":[],"assembly":"3eb6cd4d61bf","output":{"outcome":"accepted","summary":"Add a --version flag to the CLI that prints the installed version and exits."}}
```

`summary` is the request as the model wrote it again, in one sentence. Your summary is different. The `output` of the
run holds the decision. The job of a triage is the decision, so a `reject` is a correct answer too: the run ends `done`,
with `"outcome":"rejected"` and the reason in its output. If the Agent asks a second question, the run goes to `clarify`
again. Read the question, and answer it in the same way.

## Start the second run

In the first terminal, run:

```sh
jr2 run triage --input '{"request":"The greeting looks wrong sometimes."}'
```

This request does not say what is wrong. The run goes to `clarify` and waits, as the first run did:

```text
…
→ context kind-jr2 / namespace my-instance
run 84079cb2-3cdd-4b9a-ae39-51ab873c5f3c (triage) — attached
  → active "triaging"
  → active "clarify"
```

`84079cb2-3cdd-4b9a-ae39-51ab873c5f3c` is the id of the second run. Your run id is different. If the Agent picks
`accept` or `reject`, start the run again.

## Answer from the Console

Open `http://localhost:4000` in your browser. If the badge after the `instance token` box shows `live`, go to the next
step. If the box is empty, get the token. In the send terminal, run:

```sh
kubectl -n my-instance get secret jr2-instance -o jsonpath='{.data.JR2_INSTANCE_TOKEN}' | base64 -d
```

Type the token in the `instance token` box. Push Enter. The badge after the box shows `live`.

Click `triage` in the Fleet rail on the left. The center shows the Machine of `triage`, with its states and transitions.
Click the run in the rail. The Console lights the state of the run, `clarify`.

Click the `Attention` tab in the drawer on the right. It has one card for the Gate:

- The card starts with `⚑ clarify`, then `triage` and the first 8 characters of the run id.
- Under `question`, it shows the question of the Agent.
- Under `answer`, it has a form with the field `info *`. The `*` shows that the field is necessary.

Type this text in the `info` field:

```text
Spaces around the name show in the greeting. Trim the name.
```

Click `send`. The card disappears, and the run goes back to `triaging`. If the Agent asks one more question, a new card
shows it. Answer it in your own words, in the same way. When the Agent picks `accept`, the run ends. The `jr2 run` in
the first terminal prints the last status.

## What happened

You wrote a Machine with two waiting states. In `triaging`, a model chose the next state. In `clarify`, a person sent
the event that moved it. The Machine gave all of the transitions. The model chose only among the events of its state.
[Agents and Turns](../concepts/agents-and-turns.md) explains the Agent and its Turn.

The Menu of `triager` came from the transitions of `triaging`. You did not give the Agent a list of picks. A pick that
the state does not take moves nothing. [The Menu](../concepts/the-menu.md) explains what the Agent can pick, and
[ADR-0015](../reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md) gives the reasons.

The Gate `clarify` was open only while the run was in its state. It took only `answer`. `jr2 send` and the Console sent
the same event to a Gate with the same name, through the same route. [Gates](../concepts/gates.md) explains who can
answer a Gate. [Answer a Gate](../guides/answer-a-gate.md) gives the other ways to answer one.

`triager` has `workspace: "none"`, so its Turns ran on the
[Instance Harness](../reference/glossary.md#instance-harness). No Sandbox started for the run.
[Architecture](../concepts/architecture.md) shows what runs where.

## Next

[An Agent in a Sandbox](an-agent-in-a-sandbox.md).
