Skip to content

Quickstart

In ten minutes you will create a Norn project, run an order workflow on a local Temporal server, send it a signal, and stop the whole thing halfway through to watch it pick up where it left off.

You need Node.js 22.18 or later. Nothing else: the project downloads and starts a local Temporal server for you on the first run. If you already run one (temporal server start-dev), it uses that instead.

  1. Create the project.

    Terminal window
    pnpm create @norn-ts my-app

    This copies a small project into my-app/ and installs its dependencies. The rest of this page uses pnpm; with npm, write npm run dev for pnpm dev, and so on.

  2. Start it.

    Terminal window
    cd my-app
    pnpm dev
    Starting a local Temporal server (the first run downloads it)...
    Temporal is running on localhost:7233. Web UI: http://localhost:8233
    (webpack's report on bundling the workflows)
    Worker running. Press Ctrl+C to stop.

    Two things are now running: a Temporal server, which stores every workflow’s history, and your worker, the process that runs your workflow and activity code. The worker restarts when you save a file.

  3. Start an order. In a second terminal, in my-app:

    Terminal window
    pnpm order
    Started order-o-mh3k2x1a: http://localhost:8233/namespaces/default/workflows/order-o-mh3k2x1a
    Cancel it within 5 minutes with: pnpm order cancel o-mh3k2x1a

    Open the link. The Temporal Web UI shows the workflow’s event history: the workflow started, the payments.charge activity was scheduled and completed (your first terminal logs charged 1999 cents), and a five-minute timer started. The workflow is now waiting, and it isn’t using any memory or CPU on the worker while it waits.

  4. Send it a signal. Use the order ID from the previous step:

    Terminal window
    pnpm order status o-mh3k2x1a
    pnpm order cancel o-mh3k2x1a
    pnpm order status o-mh3k2x1a
    paid
    Canceled o-mh3k2x1a.
    refunded

    status is a query: it reads the workflow’s state without changing it. cancel is a signal: it delivers a message the workflow reacts to. The workflow stops waiting, runs the payments.refund activity and completes. Refresh the Web UI to see both in the history.

  5. Pull the plug. Start another order with pnpm order, then press Ctrl+C in the first terminal to stop everything: the worker and the Temporal server. Start it again with pnpm dev, and ask the order for its status:

    Terminal window
    pnpm order status <id>

    It is still paid, and still waiting out its five minutes. Temporal stored the history in .temporal/dev.db. When the worker came back, it replayed the history to rebuild the workflow’s state and carried on. In production this is what happens when a deploy, a crash or a lost machine takes a worker away. Durable execution in 5 minutes explains how replay works.

  6. Change the code. Open src/services/payment-gateway.ts and change the console.log in charge(). Save, and the worker restarts. Run pnpm order again to see your message.

  7. Run the tests.

    Terminal window
    pnpm test
    ✓ test/order.workflow.test.ts (3 tests)
    Tests 3 passed (3)

    The tests run on Temporal’s time-skipping test server, so the five-minute wait takes no time at all. The activities are fakes (plain vi.fn()s), so the tests never touch the payment gateway.

  • Directorysrc/
    • Directoryworkflows/
      • order.workflow.ts the workflow
      • index.ts exports every workflow the worker runs
    • Directoryactivities/
      • payment.activities.ts the steps that do I/O
    • Directoryservices/
      • payment-gateway.ts what the activities call
    • worker.ts the worker process
    • order.ts the client script behind pnpm order
  • Directorytest/
    • order.workflow.test.ts
  • Directoryscripts/
    • dev.ts what pnpm dev runs
  • .claude/skills/norn/SKILL.md teaches coding agents about Norn
  • AGENTS.md
  • package.json

Here’s how the pieces fit together:

  • The workflow (src/workflows/order.workflow.ts) is a class decorated with @Workflow. Its run() method is the business process: charge, wait up to five minutes for a cancellation, maybe refund. Its @Signal and @Query methods are how the outside world talks to it. Workflow code must be deterministic, because Temporal replays it, so it never does I/O itself. Determinism & the sandbox explains the rules.
  • The activities (src/activities/payment.activities.ts) do the I/O. They’re a plain class. The workflow imports it with import type and calls it through useActivities<PaymentActivities>('payments', { timeout, retries }), which is also where the retry policy lives. Activity code never reaches the workflow sandbox.
  • The worker (src/worker.ts) hands both to createWorker(): the module that exports your workflows, and your activity objects by name ({ payments: new PaymentActivities(gateway) }). That name is how the workflow finds them.
  • The client (src/order.ts) uses createClient() and the workflow class to start, signal and query orders, with every name and argument type-checked.
  • New to Temporal? Read Durable execution in 5 minutes.
  • Already know Temporal? Coming from the Temporal SDK maps the SDK to Norn.
  • Writing your own? Workflows and Activities cover everything the starter doesn’t.
  • Adding Norn to an existing project? pnpm add @norn-ts/norn @temporalio/client @temporalio/common @temporalio/worker @temporalio/workflow, then copy src/worker.ts, vitest.config.ts and tsconfig.json from the starter. Norn needs standard decorators (no experimentalDecorators).