Skip to content

Research Assistant (Temporal Runtime)

This example demonstrates a durable research assistant using the Temporal runtime. It shows:

  • Temporal workflow and activity setup
  • Activity timeout configuration
  • Worker/client architecture
  • Crash-resistant execution
  • Sub-agent orchestration as child workflows

Source Code

The full example is in examples/research-assistant-temporal/.

Prerequisites

  • Node.js 18+
  • Docker (for running Temporal server)
  • OpenAI API key

Project Structure

examples/research-assistant-temporal/
├── src/
│   ├── agent.ts          # Agent definition
│   ├── types.ts          # State and output schemas
│   ├── workflows.ts      # Temporal workflow
│   ├── activities.ts     # Activity definitions
│   ├── worker.ts         # Worker entry point
│   ├── client.ts         # Client entry point
│   ├── tools/
│   │   ├── web-search.ts
│   │   ├── take-notes.ts
│   │   └── summarize.ts
│   └── agents/
│       └── summarizer.ts # Sub-agent
├── docker-compose.yml
└── package.json

Running the Example

1. Start Temporal Server

bash
cd examples/research-assistant-temporal
npm run docker:up

This starts:

  • Temporal server on localhost:7233
  • Temporal UI on http://localhost:8080

2. Start the Worker

bash
# In one terminal
export OPENAI_API_KEY=sk-xxx
npm run worker

3. Run the Client

bash
# In another terminal
npm run client

# Or with a custom topic
npm run client "quantum computing applications"

Key Components

Temporal Workflow

The workflow orchestrates agent execution with durability:

typescript
// src/workflows.ts
import {
  proxyActivities,
  executeChild,
  isCancellation,
  log,
  workflowInfo,
} from '@temporalio/workflow';

import { agentWorkflow as v7AgentWorkflow } from '@helix-agents/runtime-temporal/workflow';
import type { AgentWorkflowInput, AgentWorkflowResult } from '@helix-agents/runtime-temporal';

// Main workflow function — delegates to v7's agentWorkflow.
// The imported `agentWorkflow` sets up `proxyActivities<GenericActivities>`,
// the INTERRUPT_SIGNAL_NAME handler, and child-workflow dispatch internally.
// The wrapper exists only to register under a stable name your
// `TemporalAgentExecutor.workflowName` expects.
export async function agentWorkflow(input: AgentWorkflowInput): Promise<AgentWorkflowResult> {
  return v7AgentWorkflow(input);
}

Key points:

  • v7 stateless suspension: the workflow exits at every HITL boundary; the executor spawns a fresh __resume-N workflow on resume
  • agentWorkflow from @helix-agents/runtime-temporal/workflow handles the entire agent loop including proxyActivities<GenericActivities>, the INTERRUPT_SIGNAL_NAME handler, and child-workflow dispatch
  • Sub-agents run as child workflows automatically
  • Activity retries happen automatically on transient failures

Activities

Activities perform all I/O operations:

typescript
// src/activities.ts
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { GenericActivities, AgentRegistry } from '@helix-agents/runtime-temporal';

import { ResearchAssistantAgent } from './agent.js';
import { SummarizerAgent } from './agents/summarizer.js';

// Create agent registry
export function createRegistry(): AgentRegistry {
  const registry = new AgentRegistry();
  registry.register(ResearchAssistantAgent);
  registry.register(SummarizerAgent);
  return registry;
}

// Create activities for worker registration. v7 ships GenericActivities
// (renamed from the v6 GenericAgentActivities) — pass the construction
// dependencies and use its activity functions directly.
export function createActivities() {
  const registry = createRegistry();
  const stateStore = new InMemoryStateStore();
  const streamManager = new InMemoryStreamManager();
  const llmAdapter = new VercelAIAdapter();

  return new GenericActivities({
    registry,
    stateStore,
    streamManager,
    llmAdapter,
  }).getActivities();
}

Key points:

  • GenericActivities (v7-renamed from GenericAgentActivities) provides the v7 stateless-suspension activity surface — applyResultsAndReload, commitSuspendedStep, runLLMStep, etc.
  • getActivities() returns a record of bound activity methods ready to register with the worker
  • Registry maps agent names to definitions
  • For production, use RedisStateStore instead of InMemoryStateStore

Worker

The worker bundles workflows and registers activities:

typescript
// src/worker.ts
import { Worker, bundleWorkflowCode, NativeConnection } from '@temporalio/worker';
import { createActivities } from './activities.js';
import path from 'path';

const TEMPORAL_ADDRESS = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
const TASK_QUEUE = process.env.TASK_QUEUE || 'research-assistant';

async function main() {
  // Connect to Temporal server
  const connection = await NativeConnection.connect({
    address: TEMPORAL_ADDRESS,
  });

  // Bundle workflow code for isolation
  const workflowBundle = await bundleWorkflowCode({
    workflowsPath: path.resolve(__dirname, './workflows.ts'),
  });

  // Create activities
  const activities = createActivities();

  // Create and start worker
  const worker = await Worker.create({
    connection,
    namespace: 'default',
    taskQueue: TASK_QUEUE,
    workflowBundle,
    activities,
  });

  // Run the worker (blocks until shutdown)
  await worker.run();
}

main();

Key points:

  • Workflows are bundled for deterministic replay
  • Worker listens on a task queue
  • Multiple workers can share the same task queue for scaling

Client

The client starts workflows and retrieves results:

typescript
// src/client.ts
import { Client, Connection } from '@temporalio/client';
import { v4 as uuidv4 } from 'uuid';
import type { agentWorkflow } from './workflows.js';
import type { AgentWorkflowInput, AgentWorkflowResult } from '@helix-agents/runtime-temporal';

const TEMPORAL_ADDRESS = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
const TASK_QUEUE = process.env.TASK_QUEUE || 'research-assistant';

async function main() {
  const topic = process.argv[2] || 'the benefits of TypeScript';

  // Connect to Temporal
  const connection = await Connection.connect({
    address: TEMPORAL_ADDRESS,
  });

  const client = new Client({ connection });

  // Generate unique run ID
  const runId = `research-${Date.now()}-${uuidv4().slice(0, 8)}`;

  // Prepare workflow input
  const input: AgentWorkflowInput = {
    agentType: 'research-assistant',
    runId,
    streamId: runId,
    message: topic,
  };

  // Start the workflow
  const handle = await client.workflow.start<typeof agentWorkflow>('agentWorkflow', {
    workflowId: runId,
    taskQueue: TASK_QUEUE,
    args: [input],
  });

  console.log(`Workflow started: ${runId}`);
  console.log(`View in UI: http://localhost:8080/namespaces/default/workflows/${runId}`);

  // Wait for result
  const result: AgentWorkflowResult = await handle.result();

  if (result.status === 'completed' && result.output) {
    console.log('Research Output:', result.output);
  } else if (result.status === 'failed') {
    console.error('Research failed:', result.error);
  }

  await connection.close();
}

main();

Activity Timeouts

Configure timeouts based on your use case:

typescript
const activities = proxyActivities<AgentWorkflowActivities>({
  // Time for activity to complete (includes queue time)
  scheduleToCloseTimeout: '10m',

  // Time from start to completion
  startToCloseTimeout: '5m',

  // Heartbeat timeout (for long activities)
  heartbeatTimeout: '30s',

  // Retry policy
  retry: {
    initialInterval: '1s',
    maximumInterval: '30s',
    backoffCoefficient: 2,
    maximumAttempts: 3,
    // Don't retry non-retryable errors
    nonRetryableErrorTypes: ['ValidationError'],
  },
});

Timeout recommendations:

  • startToCloseTimeout: 2-5 minutes for LLM calls (they can be slow)
  • heartbeatTimeout: 30 seconds for activities that report progress
  • maximumAttempts: 3-5 for transient failures

Sub-Agents as Child Workflows

Sub-agents run as independent child workflows:

typescript
// In workflows.ts
startChildWorkflow: async (childInput: AgentWorkflowInput) => {
  return executeChild(agentWorkflow, {
    // Child workflow ID (must be unique)
    workflowId: childInput.runId,

    // Pass the sub-agent input
    args: [childInput],

    // Use same task queue as parent
    taskQueue: info.taskQueue,

    // Don't cancel child when parent completes
    parentClosePolicy: 'ABANDON',
  });
},

Parent close policies:

  • ABANDON: Child continues running (recommended for sub-agents)
  • TERMINATE: Child is terminated
  • REQUEST_CANCEL: Child receives cancellation signal

Production Considerations

State Storage

For production with multiple workers, use Redis:

typescript
import { RedisStateStore } from '@helix-agents/store-redis';

const stateStore = new RedisStateStore({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
});

Scaling Workers

Run multiple worker instances:

bash
# Start multiple workers on same task queue
npm run worker &
npm run worker &
npm run worker &

Temporal automatically distributes work across workers.

Temporal Cloud

For production, use Temporal Cloud:

typescript
const connection = await Connection.connect({
  address: 'your-namespace.tmprl.cloud:7233',
  tls: {
    clientCertPair: {
      crt: fs.readFileSync('./client.pem'),
      key: fs.readFileSync('./client.key'),
    },
  },
});

const client = new Client({
  connection,
  namespace: 'your-namespace',
});

Monitoring

Temporal provides visibility into workflow execution:

  • Web UI: View workflow history, status, and stack traces
  • Queries: Inspect running workflow state
  • Search: Find workflows by custom attributes

Workflow Determinism

Workflows must be deterministic for replay. Avoid:

  • Date.now() - use import { sleep } from '@temporalio/workflow'
  • Math.random() - use workflow-safe alternatives
  • Network calls - use activities instead
  • Non-deterministic libraries - check Temporal compatibility

The agentWorkflow function handles this for you, but be careful with custom workflow code.

Next Steps

Released under the MIT License.