Client Usage
Learn how to use the typed client to execute workflows with full type safety.
Overview
The @temporal-contract/client package provides a type-safe wrapper around Temporal's client that enforces your contract definitions at compile time.
Installation
bash
pnpm add @temporal-contract/client @swan-io/boxedBasic Setup
typescript
import { Connection, Client } from '@temporalio/client';
import { TypedClient } from '@temporal-contract/client';
import { myContract } from './contract';
// Connect to Temporal
const connection = await Connection.connect({
address: 'localhost:7233',
});
// Create Temporal client and typed client
const temporalClient = new Client({ connection });
const client = TypedClient.create(myContract, temporalClient);Executing Workflows
Basic Execution
Execute a workflow and wait for completion using the Result/Future pattern:
typescript
const future = client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: {
orderId: 'ORD-123',
customerId: 'CUST-456',
},
});
// await the Future to get the Result
const result = await future;
// Handle the Result with pattern matching
result.match({
Ok: (output) => {
console.log('Order processed:', output.status); // TypeScript knows the shape!
},
Error: (error) => {
console.error('Workflow failed:', error);
},
});Start Without Waiting
Start a workflow without waiting for completion:
typescript
const handleResult = await client.startWorkflow('processOrder', {
workflowId: 'order-123',
args: {
orderId: 'ORD-123',
customerId: 'CUST-456',
},
});
handleResult.match({
Ok: async (handle) => {
// Get workflow ID
console.log('Started workflow:', handle.workflowId);
// Wait for result later
const result = await handle.result();
result.match({
Ok: (output) => console.log('Completed:', output),
Error: (error) => console.error('Failed:', error),
});
},
Error: (error) => {
console.error('Failed to start workflow:', error);
},
});Type Safety
The typed client provides compile-time safety:
typescript
// ✅ Correct - TypeScript validates args
await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: {
orderId: 'ORD-123',
customerId: 'CUST-456',
},
});
// ❌ Error - Missing required field
await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: {
orderId: 'ORD-123',
// customerId is missing - TypeScript error!
},
});
// ❌ Error - Wrong workflow name
await client.executeWorkflow('invalidWorkflow', {
workflowId: 'order-123',
args: { /* ... */ },
});Result/Future Pattern
The client uses @swan-io/boxed for explicit error handling:
typescript
import { Result } from '@swan-io/boxed';
const result = await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
});
// Handle result with pattern matching
result.match({
Ok: (value) => {
console.log('Order processed:', value.transactionId);
},
Error: (error) => {
console.error('Order failed:', error);
},
});Workflow Options
Pass standard Temporal workflow options:
typescript
await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
// Standard Temporal options
taskQueue: 'orders', // Override task queue
workflowExecutionTimeout: '1 hour',
workflowRunTimeout: '30 minutes',
retry: {
maximumAttempts: 3,
},
memo: {
description: 'Customer order processing',
},
searchAttributes: {
CustomerId: ['CUST-456'],
},
});Getting Workflow Handle
Get a handle to an existing workflow:
typescript
const handle = client.getHandle('order-123');
// Query the workflow
const status = await handle.query('getStatus');
// Signal the workflow
await handle.signal('cancelOrder', { reason: 'Customer request' });
// Get the result
const result = await handle.result();Multiple Workflows
The same client can execute any workflow in the contract:
typescript
const orderResult = await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
});
const refundResult = await client.executeWorkflow('processRefund', {
workflowId: 'refund-123',
args: { orderId: 'ORD-123', reason: 'Damaged item' },
});Error Handling
Workflow Execution Errors
typescript
const result = await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
});
result.match({
Ok: (value) => console.log('Success:', value),
Error: (error) => console.error('Workflow returned error:', error),
});Workflow Failures
typescript
import { WorkflowFailedError } from '@temporalio/client';
try {
await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
});
} catch (error) {
if (error instanceof WorkflowFailedError) {
console.error('Workflow failed:', error.message);
console.error('Cause:', error.cause);
}
}Connection Management
Single Connection
Reuse connections across clients:
typescript
const connection = await Connection.connect({
address: 'localhost:7233',
});
const temporalClient = new Client({ connection });
const orderClient = TypedClient.create(orderContract, temporalClient);
const inventoryClient = TypedClient.create(inventoryContract, temporalClient);
// Both clients share the same connection and Temporal client instanceConnection Pooling
For high-throughput applications:
typescript
const connection = await Connection.connect({
address: 'localhost:7233',
// Connection pool settings
maxConcurrentWorkflowTaskPollers: 10,
maxConcurrentActivityTaskPollers: 20,
});Closing Connections
typescript
// Close connection when done
await connection.close();Working with Multiple Contracts
Different clients for different contracts:
typescript
import { orderContract } from './contracts/order';
import { paymentContract } from './contracts/payment';
import { inventoryContract } from './contracts/inventory';
const temporalClient = new Client({ connection });
const orderClient = TypedClient.create(orderContract, temporalClient);
const paymentClient = TypedClient.create(paymentContract, temporalClient);
const inventoryClient = TypedClient.create(inventoryContract, temporalClient);
// Each client is typed to its contract
await orderClient.executeWorkflow('processOrder', { /* ... */ });
await paymentClient.executeWorkflow('processPayment', { /* ... */ });
await inventoryClient.executeWorkflow('updateStock', { /* ... */ });Testing
Mock the client for testing:
typescript
import { describe, it, expect, vi } from 'vitest';
import { Result } from '@swan-io/boxed';
describe('OrderService', () => {
it('should process order', async () => {
const mockClient = {
executeWorkflow: vi.fn().mockResolvedValue(
Result.Ok({ status: 'success', transactionId: 'tx-123' })
),
};
const service = new OrderService(mockClient);
const result = await service.createOrder({
orderId: 'ORD-123',
customerId: 'CUST-456',
});
expect(mockClient.executeWorkflow).toHaveBeenCalledWith(
'processOrder',
expect.objectContaining({
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
})
);
});
});Best Practices
1. Reuse Connections
typescript
// ✅ Good - single connection
const connection = await Connection.connect({ address: 'localhost:7233' });
const temporalClient = new Client({ connection });
const client = TypedClient.create(contract, temporalClient);
// ❌ Avoid - creating connections repeatedly
for (const order of orders) {
const connection = await Connection.connect({ address: 'localhost:7233' });
const temporalClient = new Client({ connection });
const client = TypedClient.create(contract, temporalClient);
await client.executeWorkflow(/* ... */);
}2. Use Meaningful Workflow IDs
typescript
// ✅ Good - descriptive and unique
workflowId: `order-${orderId}-${Date.now()}`
// ❌ Avoid - random or non-descriptive
workflowId: Math.random().toString()3. Handle Both Success and Error Cases
typescript
// ✅ Good - handle all cases
const result = await client.executeWorkflow('processOrder', {
workflowId: 'order-123',
args: { orderId: 'ORD-123', customerId: 'CUST-456' },
});
result.match({
Ok: (value) => {
// Handle success
updateDatabase(value);
},
Error: (error) => {
// Handle error
logError(error);
notifySupport(error);
},
});See Also
- Defining Contracts - Creating your contract definitions
- Worker Usage - Implementing workflows and activities
- Result Pattern - Understanding Result/Future error handling
- API Reference - Complete client API documentation