Skip to content

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/boxed

Basic 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 instance

Connection 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

Released under the MIT License.