Agent Development with Temporal¶
Understanding when Temporal becomes essential—and when it's not necessary—helps you choose the right architecture. Here are six scenarios progressing from simple to complex, showing when you don't need Temporal and when it becomes critical:
1. Simple Tools¶
Scenario: Basic agent with a simple web search tool. User asks a question, agent uses the tool, returns result.
flowchart LR
subgraph Interaction["User Interaction"]
direction TB
User([User])
Agent[Agent]
User <--> Agent
end
subgraph Processing["Agent Processing"]
direction TB
Tool[Tool: Web Search]
end
Agent --> Tool
style User fill:#e1f5ff
style Agent fill:#d4edda
style Tool fill:#fff3cd
Code Example:
from agents import Agent, Runner, WebSearchTool
agent = Agent(
name="Assistant",
tools=[
WebSearchTool(),
],
)
result = await Runner.run(
agent,
"Which coffee shop should I go to, taking into account my preferences and the weather today in SF?"
)
print(result.final_output)
Why Temporal is NOT needed: For simple single-step tools, Temporal is not necessary. Base Async ACP or even Sync ACP is sufficient for basic operations that complete quickly and don't require durability. Use Base ACP without Temporal for these cases—it's simpler, faster to develop, and has less operational overhead. Temporal only becomes essential when you need the capabilities shown in the scenarios below.
2. Durability: Complex Multi-Step Tools¶
Scenario: Invoice processing with multiple sequential steps: Download File → Extract Info → Validate Info → Initiate Payment
graph LR
User([User])
Agent[Agent]
Tool[Tool: Process<br/>Invoice]
subgraph ToolSteps["Tool Execution Steps"]
Step1[Download File]
Step2[Extract Info]
Step3[Validate Info]
Step4[Initiate Payment]
end
User --> Agent
Agent --> Tool
Tool --> Step1
Step1 --> Step2
Step2 --> Step3
Step3 --> Step4
style User fill:#e1f5ff
style Agent fill:#d4edda
style Tool fill:#fff3cd
style Step1 fill:#f8d7da
style Step2 fill:#f8d7da
style Step3 fill:#f8d7da
style Step4 fill:#f8d7da
Code Example:
@function_tool
def process_invoice(file_uri: str) -> str:
file = workflow.execute_activity(download_file, file_uri)
vendor_info = workflow.execute_activity(extract, file)
valid = workflow.execute_activity(validate, vendor_info)
if valid:
workflow.execute_activity(pay, vendor_info.vendor)
return f"Success: payment sent to {vendor_info.vendor}"
else:
return "Failure: invalid invoice."
Why Temporal is needed: Without Temporal, if the "extract" step fails, you'd have to re-download the file. If "validate" fails, you'd have to re-download AND re-extract. Temporal persists state after each execute_activity call, so failures only retry the failed step. This is critical for expensive operations (file downloads, API calls, payments) where you can't afford to redo work. Temporal also provides automatic retries with backoff policies for transient failures.
3. Composability: Human-in-the-Loop¶
Scenario: Invoice processing that requires human approval before proceeding with the full workflow.
flowchart LR
subgraph Interaction["User Interaction"]
direction TB
User([User])
Agent[Agent]
Tool[Tool: Process<br/>Invoice]
User <--> Agent
Agent --> Tool
end
subgraph WorkflowSteps["Approval Workflow"]
direction TB
Approval[Human Approval]
Step1[Download File]
Step2[Extract Info]
Step3[Validate Info]
Step4[Initiate Payment]
Approval --> Step1
Step1 --> Step2
Step2 --> Step3
Step3 --> Step4
end
Tool --> Approval
style User fill:#e1f5ff
style Agent fill:#d4edda
style Tool fill:#fff3cd
style Approval fill:#ffeaa7
style Step1 fill:#f8d7da
style Step2 fill:#f8d7da
style Step3 fill:#f8d7da
style Step4 fill:#f8d7da
Code Example:
@function_tool
async def process_invoice(file_uri: str) -> str:
return await workflow.execute_child_workflow(...)
@workflow.defn
class ApproveAndProcessInvoiceWorkflow:
def __init__(self, approved: bool = False):
self._approved = approved
@workflow.signal
async def on_approval(self, event: ApprovalEvent) -> None:
self._approved = event.approved
@workflow.run
async def run(self, file_uri: str) -> str:
# re-evaluated when approval signal arrives
await workflow.wait_condition(lambda: self._approved)
if self._approved:
# download, extract, validate, and pay (if valid)
else:
return "Failure: invalid invoice."
Why Temporal is needed: The agent needs to wait for human approval, which could take minutes, hours, or days. Temporal workflows can pause indefinitely using wait_condition() without consuming active resources. When the human approves (via a signal), the workflow wakes up and continues exactly where it left off. Without Temporal, you'd need to build a complex state machine with polling or webhooks to track where each workflow paused and resume it later. Temporal handles all of this automatically through durable execution and signals.
4. Autonomous Execution: Scheduled Wake-ups¶
Scenario: Agent schedules a future check (e.g., "check for invoice comments in 3 days"), sleeps, then wakes itself up to continue work.
flowchart LR
subgraph Interaction["User Interaction"]
direction TB
User([User])
Agent[Agent]
Tool[Tool: Schedule<br/>for later]
User <--> Agent
Agent --> Tool
end
subgraph DelayedWorkflow["Delayed Child Workflow"]
direction TB
Wait[Wait delay secs]
Ping[Ping OG agent]
Wake[Agent wakes up]
Wait --> Ping
Ping --> Wake
end
Tool --> Wait
Wake -.-> Agent
style User fill:#e1f5ff
style Agent fill:#d4edda
style Tool fill:#fff3cd
style Wait fill:#ffeaa7
style Ping fill:#f8d7da
style Wake fill:#d4edda
Code Example:
@function_tool
async def check_for_comments(invoice_id: str, delay: int) -> str:
return await workflow.start_child_workflow(
DelayedWorkflow, prompt, start_delay=delay
)
@workflow.defn
class DelayedWorkflow:
@workflow.run
async def run(self, invoice_id: str) -> str:
handle = workflow.get_external_workflow_handle_for(AgentWorkflow)
await handle.signal(
AgentWorkflow.on_event,
Event(prompt=f"Resolve comments for invoice: {invoice_id}")
)
@workflow.defn
class AgentWorkflow:
@workflow.signal
async def on_event(self, event: Event) -> None:
# Add event.prompt to Agent inputs and move forwards
Why Temporal is needed: Agents need to schedule future actions without polling. Temporal child workflows can sleep for arbitrary durations (seconds, days, weeks) using start_delay, then signal parent workflows to wake them up. This enables "check back later" patterns without maintaining active connections or cron jobs. Without Temporal, you'd need external scheduling systems, polling mechanisms, or complex timer infrastructure. Temporal makes time-based orchestration a first-class primitive.
5. Queueing: Async Batch Processing¶
Scenario: Agent receives events from multiple sources (webhooks, users, schedules) and processes them in batches rather than one-by-one.
flowchart LR
subgraph EventSources["Event Sources"]
direction TB
Sources[Webhooks<br/>Users<br/>Schedules]
Event1[1]
Event2[2]
Event3[3]
Event4[4]
Sources --> Event1
Sources --> Event2
Sources --> Event3
Sources --> Event4
end
subgraph AgentProcessing["Agent Processing"]
direction TB
Queue[Queue: Events]
Process[Process Batch]
Queue --> Process
end
Event1 --> Queue
Event2 --> Queue
Event3 --> Queue
Event4 --> Queue
style Sources fill:#e1f5ff
style Event1 fill:#fff3cd
style Event2 fill:#fff3cd
style Event3 fill:#fff3cd
style Event4 fill:#fff3cd
style Queue fill:#ffeaa7
style Process fill:#d4edda
Code Example:
@workflow.defn
class AgentWorkflow:
@workflow.init
def __init__(self, TaskParams):
self._queue: asyncio.Queue[Event] = asyncio.Queue()
self._batch_size = BATCH_SIZE
@workflow.signal
async def on_event(self, event: Event) -> None:
self._queue.put(event)
@workflow.run
async def run(params: TaskParams):
# process events as they come in
while True:
await workflow.wait_condition(
lambda: self._queue.qsize() >= self._batch_size,
)
current_batch = dequeue_all(self._queue, self._batch_size)
await workflow.execute_child_workflow( # blocking
workflow=ProcessAgentEvents.run,
args=[current_batch],
)
Why Temporal is needed: Processing each event individually is inefficient (API rate limits, batching for analytics, coordinated actions). Temporal workflows maintain in-memory queues (asyncio.Queue) as part of workflow state, allowing events to accumulate and batch process when thresholds are met. Events received via signals are stored durably in the workflow's event history. Without Temporal, you'd need external queuing systems (Redis, SQS) with complex coordination logic. Temporal makes the queue part of the workflow itself, with automatic persistence and exactly-once processing guarantees.
6. Long-Running Workflows: Real-World Business Processes¶
Scenario: A 50+ day procurement workflow that wakes and sleeps in response to real-world events:
- Day 1: Approve HVAC → Issue PO → Sleep
- Day 14: Check tracking → Detect delay → Surface options → Sleep
- Day 15: Human cancels → Reorder from alternate → Sleep
- Day 50: Windows arrive, steel delayed → Coordinate storage → Sleep
graph TB
Day1["Day 1:<br/>HVAC Approved"]
Day14["Day 14:<br/>Check Tracking"]
Day15["Day 15:<br/>PM Decision"]
Day50["Day 50:<br/>Windows Arrived"]
Day1 -.Sleep.-> Day14
Day14 -.Sleep.-> Day15
Day15 -.Sleep.-> Day50
subgraph Run1["Day 1 Agent Run"]
Event1["Wake"]
PO1["Issue PO"]
Track1["Create Tracking"]
Sleep1["Sleep"]
Event1 --> PO1
PO1 --> Track1
Track1 --> Sleep1
end
subgraph Run14["Day 14 Agent Run"]
Event14["Wake"]
Query["Query Vendor<br/>System"]
Delay["Detect 3+ Week<br/>Delay"]
Options["Surface 3 Options<br/>to PM"]
Sleep14["Sleep"]
Event14 --> Query
Query --> Delay
Delay --> Options
Options --> Sleep14
end
subgraph Run15["Day 15 Agent Run"]
Event15["Wake"]
Cancel["Cancel Old<br/>Order"]
Reorder["Issue New<br/>Order"]
Notify["Notify Installation<br/>Crews"]
Sleep15["Sleep"]
Event15 --> Cancel
Cancel --> Reorder
Reorder --> Notify
Notify --> Sleep15
end
subgraph Run50["Day 50 Agent Run"]
Event50["Wake"]
Detect["Materials Out<br/>of Order"]
Coordinate["Coordinate<br/>Storage"]
Adjust["Adjust Crew<br/>Schedules"]
Sleep50["Sleep..."]
Event50 --> Detect
Detect --> Coordinate
Coordinate --> Adjust
Adjust --> Sleep50
end
Day1 --> Event1
Day14 --> Event14
Day15 --> Event15
Day50 --> Event50
style Day1 fill:#e1f5ff
style Day14 fill:#e1f5ff
style Day15 fill:#e1f5ff
style Day50 fill:#e1f5ff
style Event1 fill:#d4edda
style Event14 fill:#d4edda
style Event15 fill:#d4edda
style Event50 fill:#d4edda
style Sleep1 fill:#e7e7ff
style Sleep14 fill:#e7e7ff
style Sleep15 fill:#e7e7ff
style Sleep50 fill:#e7e7ff
style PO1 fill:#fff3cd
style Track1 fill:#fff3cd
style Query fill:#fff3cd
style Delay fill:#f8d7da
style Options fill:#fff3cd
style Cancel fill:#f8d7da
style Reorder fill:#fff3cd
style Notify fill:#fff3cd
style Detect fill:#f8d7da
style Coordinate fill:#fff3cd
style Adjust fill:#fff3cd
Why Temporal is needed: Business processes span arbitrary timeframes (days, weeks, months) with unpredictable events. Temporal workflows can run indefinitely, waking in response to external signals (webhooks, scheduled checks, human decisions) while maintaining complete state continuity. The workflow knows it's on Day 50, which vendor was used, what decisions were made on Day 15, and can access the full history. Without Temporal, you'd need to persist all state externally, build resumption logic, handle partial failures, and orchestrate wake-ups manually. Temporal makes long-running, event-driven processes as simple as writing sequential code with signals and wait conditions.
Key Insight: Most business-critical processes are long and dynamic. They must adapt to unpredictable, real-world events. Agentex, with its native Temporal integration, gives AI agents the critical capabilities needed to orchestrate complex workflows.