Features - TypeScript SDK feature guide
The Features section of the Temporal Developer's guide provides basic implementation guidance on how to use many of the development features available to Workflows and Activities in the Temporal Platform.
In this section you can find the following:
- How to develop Signals
- How to develop Queries
- How to start a Child Workflow Execution
- How to start a Temporal Cron Job
- How to use Continue-As-New
- How to set Workflow timeouts & retries
- How to set Activity timeouts & retries
- How to Heartbeat an Activity
- How to Asynchronously complete an Activity
- How to register Namespaces
- How to use custom payload conversion
How to develop with Signals
A Signal is a message sent to a running Workflow Execution.
Signals are defined in your code and handled in your Workflow Definition. Signals can be sent to Workflow Executions from a Temporal Client or from another Workflow Execution.
How to define a Signal
A Signal has a name and can have arguments.
- The name, also called a Signal type, is a string.
- The arguments must be serializable.
import { defineSignal } from '@temporalio/workflow';
interface JoinInput {
userId: string;
groupId: string;
}
export const joinSignal = defineSignal<[JoinInput]>('join');
How to handle a Signal
Workflows listen for Signals by the Signal's name.
import { setHandler } from '@temporalio/workflow';
export async function yourWorkflow() {
const groups = new Map<string, Set<string>>();
setHandler(joinSignal, ({ userId, groupId }: JoinInput) => {
const group = groups.get(groupId);
if (group) {
group.add(userId);
} else {
groups.set(groupId, new Set([userId]));
}
});
}
How to send a Signal from a Temporal Client
When a Signal is sent successfully from the Temporal Client, the WorkflowExecutionSignaled Event appears in the Event History of the Workflow that receives the Signal.
import { Client } from '@temporalio/client';
import { joinSignal } from './workflows';
const client = new Client();
const handle = client.workflow.getHandle('workflow-id-123');
await handle.signal(joinSignal, { userId: 'user-1', groupId: 'group-1' });
How to send a Signal from a Workflow
A Workflow can send a Signal to another Workflow, in which case it's called an External Signal.
When an External Signal is sent:
- A SignalExternalWorkflowExecutionInitiated Event appears in the sender's Event History.
- A WorkflowExecutionSignaled Event appears in the recipient's Event History.
import { getExternalWorkflowHandle } from '@temporalio/workflow';
import { joinSignal } from './other-workflow';
export async function yourWorkflowThatSignals() {
const handle = getExternalWorkflowHandle('workflow-id-123');
await handle.signal(joinSignal, { userId: 'user-1', groupId: 'group-1' });
}
How to Signal-With-Start
Signal-With-Start is used from the Client. It takes a Workflow Id, Workflow arguments, a Signal name, and Signal arguments.
If there's a Workflow running with the given Workflow Id, it will be signaled. If there isn't, a new Workflow will be started and immediately signaled.
WorkflowClient.signalWithStart
import { Client } from '@temporalio/client';
import { joinSignal, yourWorkflow } from './workflows';
const client = new Client();
await client.workflow.signalWithStart(yourWorkflow, {
workflowId: 'workflow-id-123',
taskQueue: 'my-taskqueue',
args: [{ foo: 1 }],
signal: joinSignal,
signalArgs: [{ userId: 'user-1', groupId: 'group-1' }],
});
How to develop with Queries
A Query is a synchronous operation that is used to get the state of a Workflow Execution.
How to define a Query
A Query has a name and can have arguments.
- The name, also called a Query type, is a string.
- The arguments must be serializable.
Use defineQuery
to define the name, parameters, and return value of a Query.
import { defineQuery } from '@temporalio/workflow';
export const getValueQuery = defineQuery<number | undefined, [string]>(
'getValue',
);
How to handle a Query
Queries are handled by your Workflow.
Don’t include any logic that causes Command generation within a Query handler (such as executing Activities). Including such logic causes unexpected behavior.
Use handleQuery
to handle Queries inside a Workflow.
You make a Query with handle.query(query, ...args)
. A Query needs a return value, but can also take arguments.
export async function trackState(): Promise<void> {
const state = new Map<string, number>();
setHandler(setValueSignal, (key, value) => void state.set(key, value));
setHandler(getValueQuery, (key) => state.get(key));
await CancellationScope.current().cancelRequested;
}
How to send a Query
Queries are sent from a Temporal Client.
Use WorkflowHandle.query
to query a running or completed Workflow.
import { Client } from '@temporalio/client';
import { getValueQuery } from './workflows';
async function run(): Promise<void> {
const client = new Client();
const handle = client.workflow.getHandle('state-id-0');
const meaning = await handle.query(getValueQuery, 'meaning-of-life');
console.log({ meaning });
}
How to define Signals and Queries statically or dynamically
- Handlers for both Signals and Queries can take arguments, which can be used inside
setHandler
logic. - Only Signal Handlers can mutate state, and only Query Handlers can return values.
Define Signals and Queries statically
If you know the name of your Signals and Queries upfront, we recommend declaring them outside the Workflow Definition.
signals-queries/src/workflows.ts
import * as wf from '@temporalio/workflow';
export const unblockSignal = wf.defineSignal('unblock');
export const isBlockedQuery = wf.defineQuery<boolean>('isBlocked');
export async function unblockOrCancel(): Promise<void> {
let isBlocked = true;
wf.setHandler(unblockSignal, () => void (isBlocked = false));
wf.setHandler(isBlockedQuery, () => isBlocked);
wf.log.info('Blocked');
try {
await wf.condition(() => !isBlocked);
wf.log.info('Unblocked');
} catch (err) {
if (err instanceof wf.CancelledFailure) {
wf.log.info('Cancelled');
}
throw err;
}
}
This technique helps provide type safety because you can export the type signature of the Signal or Query to be called by the Client.
Define Signals and Queries dynamically
For more flexible use cases, you might want a dynamic Signal (such as a generated ID). You can handle it in two ways:
- Avoid making it dynamic by collapsing all Signals into one handler and move the ID to the payload.
- Actually make the Signal name dynamic by inlining the Signal definition per handler.
import * as wf from '@temporalio/workflow';
// "fat handler" solution
wf.setHandler(`genericSignal`, (payload) => {
switch (payload.taskId) {
case taskAId:
// do task A things
break;
case taskBId:
// do task B things
break;
default:
throw new Error('Unexpected task.');
}
});
// "inline definition" solution
wf.setHandler(wf.defineSignal(`task-${taskAId}`), (payload) => {
/* do task A things */
});
wf.setHandler(wf.defineSignal(`task-${taskBId}`), (payload) => {
/* do task B things */
});
// utility "inline definition" helper
const inlineSignal = (signalName, handler) =>
wf.setHandler(wf.defineSignal(signalName), handler);
inlineSignal(`task-${taskBId}`, (payload) => {
/* do task B things */
});
API Design FAQs
Why not "new Signal" and "new Query"?
The semantic of defineSignal
and defineQuery
is intentional.
They return Signal and Query definitions, not unique instances of Signals and Queries themselves
The following is their entire source code:
/**
* Define a signal method for a Workflow.
*/
export function defineSignal<Args extends any[] = []>(
name: string,
): SignalDefinition<Args> {
return {
type: 'signal',
name,
};
}
/**
* Define a query method for a Workflow.
*/
export function defineQuery<Ret, Args extends any[] = []>(
name: string,
): QueryDefinition<Ret, Args> {
return {
type: 'query',
name,
};
}
Signals and Queries are instantiated only in setHandler
and are specific to particular Workflow Executions.
These distinctions might seem minor, but they model how Temporal works under the hood, because Signals and Queries are messages identified by "just strings" and don't have meaning independent of the Workflow having a listener to handle them. This will be clearer if you refer to the Client-side APIs.
Why setHandler and not OTHER_API?
We named it setHandler
instead of subscribe
because a Signal or Query can have only one "handler" at a time, whereas subscribe
could imply an Observable with multiple consumers and is a higher-level construct.
wf.setHandler(MySignal, handlerFn1);
wf.setHandler(MySignal, handlerFn2); // replaces handlerFn1
If you are familiar with RxJS, you are free to wrap your Signals and Queries into Observables if you want, or you could dynamically reassign the listener based on your business logic or Workflow state.