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.jsonRunning the Example
1. Start Temporal Server
cd examples/research-assistant-temporal
npm run docker:upThis starts:
- Temporal server on
localhost:7233 - Temporal UI on
http://localhost:8080
2. Start the Worker
# In one terminal
export OPENAI_API_KEY=sk-xxx
npm run worker3. Run the Client
# 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:
// src/workflows.ts
import {
proxyActivities,
executeChild,
isCancellation,
log,
workflowInfo,
} from '@temporalio/workflow';
import type { AgentWorkflowActivities } from '@helix-agents/runtime-temporal/workflow-exports';
import { runAgentWorkflow } from '@helix-agents/runtime-temporal/workflow-exports';
import type { AgentWorkflowInput, AgentWorkflowResult } from '@helix-agents/runtime-temporal';
// Configure activity timeouts
const activities = proxyActivities<AgentWorkflowActivities>({
// Maximum time for a single activity execution
startToCloseTimeout: '5m',
// Retry configuration
retry: {
initialInterval: '1s',
maximumInterval: '30s',
backoffCoefficient: 2,
maximumAttempts: 3,
},
});
// Main workflow function
export async function agentWorkflow(input: AgentWorkflowInput): Promise<AgentWorkflowResult> {
const info = workflowInfo();
return runAgentWorkflow(input, activities, {
// Start child workflows for sub-agents
startChildWorkflow: async (childInput: AgentWorkflowInput) => {
return executeChild(agentWorkflow, {
workflowId: childInput.runId,
args: [childInput],
taskQueue: info.taskQueue,
parentClosePolicy: 'ABANDON',
});
},
// Check for Temporal cancellation
isCancellation: (error: unknown) => isCancellation(error),
// Workflow-safe logging
log: {
info: (msg: string, data?: unknown) => log.info(msg, data as object),
warn: (msg: string, data?: unknown) => log.warn(msg, data as object),
error: (msg: string, data?: unknown) => log.error(msg, data as object),
},
});
}Key points:
proxyActivitiescreates activity proxies with timeout configurationrunAgentWorkflowfrom the framework handles the agent loopstartChildWorkflowenables sub-agent execution as child workflows- Activity retries happen automatically on transient failures
Activities
Activities perform all I/O operations:
// src/activities.ts
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { GenericAgentActivities, 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
export function createActivities() {
const registry = createRegistry();
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();
const llmAdapter = new VercelAIAdapter();
// Create the activities instance
const agentActivities = new GenericAgentActivities({
registry,
stateStore,
streamManager,
llmAdapter,
});
// Return bound activity methods
return {
initializeAgentState: agentActivities.initializeAgentState.bind(agentActivities),
executeAgentStep: agentActivities.executeAgentStep.bind(agentActivities),
executeToolCall: agentActivities.executeToolCall.bind(agentActivities),
registerSubAgents: agentActivities.registerSubAgents.bind(agentActivities),
recordSubAgentResult: agentActivities.recordSubAgentResult.bind(agentActivities),
markAgentFailed: agentActivities.markAgentFailed.bind(agentActivities),
markAgentAborted: agentActivities.markAgentAborted.bind(agentActivities),
endAgentStream: agentActivities.endAgentStream.bind(agentActivities),
failAgentStream: agentActivities.failAgentStream.bind(agentActivities),
checkExistingState: agentActivities.checkExistingState.bind(agentActivities),
updateAgentStatus: agentActivities.updateAgentStatus.bind(agentActivities),
};
}Key points:
GenericAgentActivitiesprovides all the standard agent activities- Activities are bound methods for proper
thiscontext - Registry maps agent names to definitions
- For production, use
RedisStateStoreinstead ofInMemoryStateStore
Worker
The worker bundles workflows and registers activities:
// 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:
// 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:
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 progressmaximumAttempts: 3-5 for transient failures
Sub-Agents as Child Workflows
Sub-agents run as independent child workflows:
// 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 terminatedREQUEST_CANCEL: Child receives cancellation signal
Production Considerations
State Storage
For production with multiple workers, use Redis:
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:
# 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:
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()- useimport { sleep } from '@temporalio/workflow'Math.random()- use workflow-safe alternatives- Network calls - use activities instead
- Non-deterministic libraries - check Temporal compatibility
The runAgentWorkflow function handles this for you, but be careful with custom workflow code.
Next Steps
- Cloudflare Example - Edge deployment
- Temporal Runtime Reference - Full API documentation
- Custom Loop - Build your own executor