Skip to content

Durable workflows that read like plain TypeScript

Norn is a thin layer on the Temporal TypeScript SDK. Workflows are decorated classes, activities are plain classes, and the client is typed from the workflow itself. No codegen, no config files, one package.

All you need is Node.js 22.18 or later. pnpm dev downloads and starts a local Temporal server for you.

Terminal window
pnpm create @norn-ts my-app
cd my-app
pnpm dev

Then, in a second terminal, run pnpm order and open the link it prints. You’ll see an order workflow running in the Temporal Web UI.

A workflow charges a card, then waits five minutes for a cancellation. If the worker crashes during the wait, Temporal replays the history on another worker and the timer carries on.

src/workflows/order.workflow.ts
import { condition } from '@temporalio/workflow';
import { Query, Signal, Workflow, useActivities } from '@norn-ts/norn/workflow';
import type { PaymentActivities } from '../activities/payment.activities';
@Workflow({ taskQueue: 'orders', id: 'order-{orderId}' })
export class OrderWorkflow {
private status: 'pending' | 'paid' | 'refunded' = 'pending';
private canceled = false;
private readonly payments = useActivities<PaymentActivities>('payments', {
timeout: '30s',
retries: 3,
nonRetryable: ['CardDeclinedError'],
});
@Signal() cancel(_reason: string) {
this.canceled = true;
}
@Query() getStatus() {
return this.status;
}
async run(input: { orderId: string; cents: number }) {
const charge = await this.payments.charge(input.orderId, input.cents);
this.status = 'paid';
if (await condition(() => this.canceled, '5 minutes')) {
await this.payments.refund(charge.id);
this.status = 'refunded';
}
return this.status;
}
}

The client takes the workflow class itself, so names, inputs, results and handler arguments are all checked. Hover over the code to see the types:

import {
class OrderWorkflow
OrderWorkflow
} from './order.workflow';
const
const norn: NornClient
norn
=
function createClient(options?: ClientOptions): NornClient

Creates a typed Temporal client. Pass @Workflow classes to workflow().

createClient
();
// The workflow ID, order-o-42, comes from the template.
const
const order: WorkflowHandle<ClassContract<typeof OrderWorkflow>>
order
= await
const norn: NornClient
norn
.
NornClient.workflow<typeof OrderWorkflow>(workflowClass: typeof OrderWorkflow): WorkflowClient<ClassContract<typeof OrderWorkflow>>

A typed client for a @Workflow class: norn.workflow(OrderWorkflow).start(input).

workflow
(
class OrderWorkflow
OrderWorkflow
).
WorkflowClient<ClassContract<typeof OrderWorkflow>>.start(input: {
orderId: string;
cents: number;
}, options?: StartOptions<ClassContract<typeof OrderWorkflow>> | undefined): Promise<WorkflowHandle<ClassContract<typeof OrderWorkflow>>>
start
({
orderId: string
orderId
: 'o-42',
cents: number
cents
: 1999 });
const status = await
const order: WorkflowHandle<ClassContract<typeof OrderWorkflow>>
order
.
WorkflowHandle<ClassContract<typeof OrderWorkflow>>.query<"getStatus">(key: "getStatus"): Promise<"pending" | "paid" | "refunded">
query
('getStatus');
const status: "pending" | "paid" | "refunded"
// Wrong arguments and unknown fields are compile errors:
await
const order: WorkflowHandle<ClassContract<typeof OrderWorkflow>>
order
.
WorkflowHandle<ClassContract<typeof OrderWorkflow>>.signal<"cancel">(key: "cancel", _reason: string): Promise<void>
signal
('cancel', 42);
Error ts(2345) ― Argument of type 'number' is not assignable to parameter of type 'string'.
const norn: NornClient
norn
.
NornClient.workflow<typeof OrderWorkflow>(workflowClass: typeof OrderWorkflow): WorkflowClient<ClassContract<typeof OrderWorkflow>>

A typed client for a @Workflow class: norn.workflow(OrderWorkflow).start(input).

workflow
(
class OrderWorkflow
OrderWorkflow
).
WorkflowClient<ClassContract<typeof OrderWorkflow>>.get(target: HandleTarget<ClassContract<typeof OrderWorkflow>>): WorkflowHandle<ClassContract<typeof OrderWorkflow>>

A handle by ID fields (from the contract's template) or { workflowId, runId? }. Makes no call.

get
({ orderID: 'o-42' });
Error ts(2353) ― Object literal may only specify known properties, and 'orderID' does not exist in type 'HandleTarget<ClassContract<typeof OrderWorkflow>>'.

Just classes

Decorated workflow classes, plain activity classes, and a worker you hand both to. Nothing is generated, and there’s nothing to keep in sync.

Typed from the class

Inputs, results, handler arguments and activity signatures come straight from your classes, in workflows and in the client.

Sandbox-safe

Workflows import activity classes with import type, so activity code never reaches the workflow sandbox.

Tests that skip time

useTestEnv() runs your workflows on Temporal’s time-skipping server with fake activities, so a five-day timer takes milliseconds.