> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ntropii.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Running locally

> Run a runbook end-to-end in-process with no Temporal cluster — sub-second feedback for dev + the Claude MA sandbox path.

A **workflow** is a binding of a **runbook** (the deterministic Python templates that drive the process) to an **entity**, optionally on a schedule. Outside of production (which uses Temporal for durability + replay), runbook code runs in-process under `LocalRuntime` — no broker, no Docker, no `temporalio` cluster required.

This page covers when and how to use it.

## When `LocalRuntime` is active

The runtime backend is auto-selected at first attribute access on the `runbook` / `activity` facades. `LocalRuntime` wins when:

| Context                               | `temporalio` installed | `TEMPORAL_HOST` set | Runtime           |
| ------------------------------------- | ---------------------- | ------------------- | ----------------- |
| **ntro-worker pod (production)**      | yes                    | yes                 | `TemporalRuntime` |
| **CI smoke job**                      | yes                    | no                  | `LocalRuntime`    |
| **`pytest` / `python -m runbooks.X`** | maybe                  | no                  | `LocalRuntime`    |
| **Claude MA sandbox**                 | no                     | no                  | `LocalRuntime`    |

A single INFO log line on the first call confirms which is active:

```
ntro.workflow: LocalRuntime active (no Temporal detected; running in-process)
```

## A minimal local run

`ntro.workflow.run_local` is the entry helper for a one-shot in-process run. It instantiates the runbook, sets up the `WorkflowInfo` contextvar, and awaits the `@runbook.run` method.

```python theme={null}
# runbooks/nav-monthly/main.py — sketch
import asyncio
from ntro.workflow import run_local

from .templates.workflow import NavMonthlyWorkflow
from .templates.models import NavMonthlyContext


async def main():
    ctx = NavMonthlyContext(
        period="2026-03",
        tenant_slug="acme-fund",
        entity_slug="acme-spv1",
        # …whatever your context needs
    )
    result = await run_local(NavMonthlyWorkflow, ctx, workflow_id="local-test")
    print(result)


asyncio.run(main())
```

Then:

```bash theme={null}
python -m runbooks.nav-monthly.main
# → ntro.workflow: LocalRuntime active (no Temporal detected; running in-process)
# → PeriodSummary(…)
```

Iteration cycle: sub-second per run (modulo your activities' own work — DB writes, LLM calls, etc.).

## What you get under `LocalRuntime`

* **Direct activity dispatch.** `runbook.execute_activity(fn, arg)` directly `await`s `fn(arg)` with an `ActivityInfo` pushed onto a contextvar. Same call shape as Temporal — same signature, same semantics, just no broker.
* **Slug-based child workflows.** `NtroWorkflow.run_child_workflow(slug=…)` resolves the slug via `ntro.workflow.registry` (populated when the bundle module is imported) and awaits the child's `@runbook.run`.
* **Signal-driven gates.** `@runbook.signal` handlers are plain methods; fire them from any other coroutine on the same event loop and `wait_condition` predicates pick up the state change at 50ms polling granularity.
* **Workflow + activity context.** `runbook.info()` and `activity.info()` return small dataclasses with `workflow_id` / `activity_id` synthesised at entry, so introspection code that's used to Temporal works unchanged.

## What you don't get

`LocalRuntime` is deliberately thin:

* **No durability.** A KeyboardInterrupt or process crash mid-run loses everything. Use Temporal in production; use the local runtime for the inner loop.
* **No retries.** Activity options like `start_to_close_timeout`, `retry_policy`, `heartbeat_timeout` are accepted and discarded. Wrap the activity call in `asyncio.wait_for` if you want a local timeout.
* **No replay.** Workflow code that uses `datetime.now()` / `time.time()` / `random.X` / `uuid.uuid4()` directly will silently work under `LocalRuntime` but produces non-deterministic Temporal history in production. The N-102 determinism lint flags these in CI — see [Build runbooks](/workflows/runbooks/overview#determinism-rules) for the standard alternatives (`runbook.now()`, value-via-activity-input).
* **No sandboxing.** Workflow code runs in the same process as your tests / dev script. Don't rely on isolation behaviour Temporal provides.

## Forcing a specific runtime from code

If your test fixture needs to pin a runtime (e.g. an integration test that always wants `LocalRuntime` regardless of env), call `set_runtime` *before* importing any runbook module:

```python theme={null}
import os
os.environ.pop("TEMPORAL_HOST", None)

from ntro.workflow import set_runtime
from ntro.workflow.runtime import LocalRuntime
set_runtime(LocalRuntime())

# Now import your runbook — its decorators see LocalRuntime.
from runbooks.nav_monthly.templates.workflow import NavMonthlyWorkflow
```

Once the cache is primed (first `get_runtime()` call), swapping is unsafe — existing decorators have already been applied to user code.

## Related

<CardGroup cols={2}>
  <Card title="Build runbooks" icon="code" href="/workflows/runbooks/overview">
    Concept walkthrough for authoring a runbook.
  </Card>

  <Card title="ntro.workflow reference" icon="book" href="/sdk/workflow">
    Full API surface — facade methods, runtime backends, `WorkflowError`.
  </Card>

  <Card title="Testing locally (harness)" icon="flask" href="/deploy-and-run/testing-locally">
    `ntro workflow test` — scenario-driven test runner that pairs with a real in-memory Temporal for replay-faithful coverage.
  </Card>

  <Card title="Deploy to production" icon="rocket" href="/deploy-and-run/deploy-to-production">
    `ntro workflow create --path` — push the same runbook code that ran locally to a Temporal-backed worker.
  </Card>
</CardGroup>
