Skip to main content

Guide for AI Coding Agents

Conventions for LLM coding agents (and humans in a hurry) adding Temporal code to a NestJS app with nestjs-temporal-core. The machine-readable index is llms.txt, also shipped inside the package at node_modules/nestjs-temporal-core/llms.txt.

The one rule​

Workflows are plain exported functions that run in a deterministic v8 sandbox. Activities are Nest providers that do all I/O. Almost every mistake is crossing that line.

NeedUseWhere it runs
Orchestration, waiting, branchingWorkflow functionSandbox: deterministic, no DI
HTTP, DB, files, randomness, clock, anything non-deterministicActivity (@Activity + @ActivityMethod)Normal Node, full Nest DI
Push an event into a running workflowSignalHandler declared in the workflow
Read workflow stateQueryRead-only, must not mutate
Change state and return a resultUpdateValidated, returns a value
Run another workflow from a workflowChild workflowIn the workflow
Run something on a scheduleTemporalService.upsertSchedule()Client side

File layout​

src/orders/
orders.module.ts imports TemporalModule (once, in AppModule) and provides the activities
orders.activities.ts @Injectable() @Activity() class: all I/O
orders.workflows.ts exported async functions, imports ONLY @temporalio/workflow (+ types)
orders.service.ts starts/signals/queries workflows through TemporalService

Exact patterns​

Module (once):

TemporalModule.register({
connection: { address: process.env.TEMPORAL_ADDRESS! },
taskQueue: 'orders',
worker: {
workflowsPath: path.join(__dirname, 'orders/orders.workflows'), // works from src/ and dist/
activityClasses: [OrdersActivities], // also add to providers: []
autoBundle: true, // cached bundling, see Bundling
},
});

Activity:

@Injectable()
@Activity()
export class OrdersActivities {
constructor(private readonly payments: PaymentsService) {}

@ActivityMethod('chargeCard') // the name the workflow calls
async chargeCard(orderId: string, cents: number): Promise<string> {
return this.payments.charge(orderId, cents);
}
}

Workflow (note: type-only import of the activity class):

import { proxyActivities, defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';
import type { OrdersActivities } from './orders.activities';

const { chargeCard } = proxyActivities<OrdersActivities>({
startToCloseTimeout: '1 minute',
retry: { maximumAttempts: 3 },
});

export const cancelSignal = defineSignal('cancel');
export const statusQuery = defineQuery<string>('status');

export async function orderWorkflow(orderId: string, cents: number): Promise<string> {
let status = 'pending';
let cancelled = false;
setHandler(cancelSignal, () => { cancelled = true; });
setHandler(statusQuery, () => status);

if (cancelled) return 'cancelled';
const receipt = await chargeCard(orderId, cents);
status = 'paid';
return receipt;
}

Start, signal, query from a service:

await this.temporal.startWorkflow('orderWorkflow', [id, cents], {
taskQueue: 'orders',
workflowId: `order-${id}`, // use a business key so retries do not double-start
});
await this.temporal.signalWorkflow(`order-${id}`, 'cancel');
const { result: status } = await this.temporal.queryWorkflow<string>(`order-${id}`, 'status');

startWorkflow, signalWorkflow, queryWorkflow, cancelWorkflow and terminateWorkflow live on TemporalService. Failures throw TemporalClientError with the SDK error as .cause.

Test with no server:

const moduleRef = await Test.createTestingModule({
imports: [TemporalTestingModule.register()],
providers: [OrdersService],
}).compile();
await moduleRef.get(OrdersService).place({ id: 'o1' });
expect(moduleRef.get(TemporalTestingRecorder).callsTo('startWorkflow')).toHaveLength(1);

Activities: createActivityHarness(OrdersActivities, { providers }). Real workflows with skipped timers: TemporalTestEnvironment.create({ timeSkipping: true }). Workflow edits: assertReplays(...). See Testing.

Common errors and fixes​

SymptomCauseFix
Activity not found / Activity function X is not registeredActivity class missing from activityClasses, or not in a module's providers, or @ActivityMethod name differs from the workflow's callDo all three: @Activity() class, activityClasses: [...], providers: [...]; match names
Cannot find module / webpack error at worker startWrong workflowsPath, or running from dist/ with a src/ pathUse path.join(__dirname, '...') with no extension; .ts/.js are swapped for you
Worker starts but workflow never runstaskQueue differs between worker and the startWorkflow callUse the same string; check worker.taskQueue in logs
Nondeterminism error / DeterminismViolationErrorWorkflow code changed under running executions, or uses Date.now(), Math.random(), setTimeout, I/OMove non-determinism to an activity; use patched() for edits; verify with assertReplays
Your Workflow code (or a library it imported) is importing the following disallowed modulesA workflow file imports nestjs-temporal-core, a service, or an activity valueWorkflows import only @temporalio/workflow; use import type for activities
Activity retries forever on a bad requestEvery error is retryable by defaulterrorMapping: true and @NonRetryable(); see Error handling
WorkflowExecutionAlreadyStartedErrorSame workflowId started twiceIntended: catch it, or set an id conflict policy
Shutdown hangs or drops tasksMissing app.enableShutdownHooks()Call it in main.ts
Must use import to load ES Module in JestNestJS 12 is ESM-onlyNode 24.9+ with --experimental-vm-modules, or NestJS 11. See Troubleshooting
Worker-only process exits immediatelyNo HTTP server keeps it aliveNestFactory.createApplicationContext and keep the process running; see Worker-only apps

Do not​

  • Do not import Nest, nestjs-temporal-core values, or application services into workflow files.
  • Do not call Date.now(), Math.random(), fetch, timers or process.env in a workflow. Use sleep(), uuid4() from @temporalio/workflow, and activities.
  • Do not share one connection between client and worker (the library already creates separate ones).
  • Do not add an exports map to a fork of this package: it breaks deep imports.
  • Do not inline timeout or retry numbers if a TIMEOUTS / RETRY_POLICIES preset fits.

Optional features, one line each​

WantOption / entry
Fast production bundlingworker.autoBundle (Bundling)
Stop retrying bad requestserrorMapping + @NonRetryable()
Trace across HTTP, workflow, activitycorrelation, nestjs-temporal-core/otel (Observability)
Encrypt payloadsnestjs-temporal-core/encryption (Security)
Health endpointTemporalHealthModule, nestjs-temporal-core/terminus
Unit and workflow testsnestjs-temporal-core/testing (Testing)