<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Part 6: Durable Execution with Temporal | Garage Education</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/</link><atom:link href="https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/index.xml" rel="self" type="application/rss+xml"/><description>Part 6: Durable Execution with Temporal</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-US</language><copyright>©</copyright><lastBuildDate>Sat, 10 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.garageeducation.org/media/logo.svg</url><title>Part 6: Durable Execution with Temporal</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/</link></image><item><title>Part 6: Durable Execution with Temporal</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/00-overview/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/00-overview/</guid><description>&lt;p&gt;Every watcher run is a Temporal workflow. Temporal stores each step, the approval wait, and the result. A worker crash resumes the run where it stopped.&lt;/p&gt;
&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "Temporal keeps the run; workers come and go.",
"nodes": [
{"id": "temporal", "x": 0, "y": 60, "w": 240, "label": "Temporal server", "detail": "each step, the approval wait, the result", "tone": "operator"},
{"id": "w1", "x": 360, "y": 0, "w": 220, "label": "Worker", "detail": "crashes after step 2", "tone": "isolated"},
{"id": "w2", "x": 360, "y": 130, "w": 220, "label": "Worker, restarted", "detail": "resumes at step 3", "tone": "platform"}
],
"edges": [
{"from": "temporal", "to": "w1", "label": "steps 1, 2", "fromSide": "right", "toSide": "left"},
{"from": "temporal", "to": "w2", "label": "history, then step 3", "fromSide": "right", "toSide": "left", "animated": true}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 6.0. The run lives in Temporal, not in the worker. Chapter 23 turns this into an exercise.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item><item><title>Chapter 23: Run state and messages</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/23-run-state-and-messages/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/23-run-state-and-messages/</guid><description>&lt;p&gt;Temporal workflow history holds the run state. It holds the step count, the state, the approval, and the result. Spring AI Session holds the messages and tool calls. Use the workflow id as the session id. Do not store run state in a separate table. Two stores for one run drift apart.&lt;/p&gt;
&lt;figure class="arch"&gt;
&lt;script type="application/json"&gt;
{
"title": "One run, two stores, one id.",
"nodes": [
{"id": "id", "x": 300, "y": 0, "w": 220, "label": "Workflow id", "detail": "= session id", "tone": "platform"},
{"id": "hist", "x": 0, "y": 140, "w": 260, "label": "Temporal workflow history", "detail": "step count, state, approval, result", "tone": "operator"},
{"id": "sess", "x": 560, "y": 140, "w": 260, "label": "Spring AI Session", "detail": "messages and tool calls", "tone": "data"},
{"id": "table", "x": 300, "y": 270, "w": 220, "label": "Separate run-state table", "detail": "do not add: it drifts", "tone": "isolated"}
],
"edges": [
{"from": "id", "to": "hist", "label": "run state"},
{"from": "id", "to": "sess", "label": "messages"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 23.1. Each fact about a run has exactly one home. A third store, struck out here, would have to agree with the other two and eventually would not.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Exercise.&lt;/strong&gt; Start a run. Kill the worker after step 2. Restart it. Confirm the run completes without repeating step 1.&lt;/p&gt;
&lt;figure class="seq"&gt;
&lt;script type="application/json"&gt;
{
"title": "The exercise: kill, restart, resume.",
"lifelines": [
{"id": "you", "label": "You", "tone": "external"},
{"id": "temporal", "label": "Temporal", "tone": "operator"},
{"id": "w1", "label": "Worker", "tone": "platform"},
{"id": "w2", "label": "Worker, restarted", "tone": "platform"}
],
"messages": [
{"from": "you", "to": "temporal", "label": "start a run"},
{"from": "temporal", "to": "w1", "label": "run step 1"},
{"from": "w1", "to": "temporal", "label": "step 1 done, recorded", "kind": "response"},
{"from": "temporal", "to": "w1", "label": "run step 2"},
{"from": "w1", "to": "temporal", "label": "step 2 done, recorded", "kind": "response"},
{"from": "you", "to": "w1", "label": "kill the worker", "kind": "note"},
{"from": "temporal", "to": "w2", "label": "history: steps 1 and 2 already done"},
{"from": "w2", "to": "temporal", "label": "steps 3 onward; step 1 is not repeated", "kind": "response"},
{"from": "temporal", "to": "you", "label": "run completes", "kind": "response"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 23.2. What you should see in the exercise. Recorded results are replayed from history, not run again.&lt;/figcaption&gt;
&lt;/figure&gt;</description></item><item><title>Chapter 24: Temporal basics for the watcher</title><link>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/24-temporal-basics-for-the-watcher/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.garageeducation.org/docs/agent-harness-spring-ai/06-durable-execution/24-temporal-basics-for-the-watcher/</guid><description>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Use in the watcher&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflow&lt;/td&gt;
&lt;td&gt;One watcher run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activity&lt;/td&gt;
&lt;td&gt;One model call, one tool call, one alert post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Human approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;Approval deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry policy&lt;/td&gt;
&lt;td&gt;Retry a failed model or tool call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;Read the current state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Determinism rule.&lt;/strong&gt; Workflow code must give the same result on replay. Model calls, tool calls, database writes, and REST calls go in activities. Use &lt;code&gt;Workflow.await&lt;/code&gt;, &lt;code&gt;Workflow.sleep&lt;/code&gt;, and &lt;code&gt;Workflow.currentTimeMillis()&lt;/code&gt; in the workflow. Do not use &lt;code&gt;Thread.sleep&lt;/code&gt; or &lt;code&gt;Instant.now()&lt;/code&gt; there.&lt;/p&gt;
&lt;figure class="flow"&gt;
&lt;script type="application/json"&gt;
{
"title": "Where does this line of code go?",
"steps": [
{"label": "Is it pure logic: same input, same result on replay?", "no": "Activity: model, tool, database, or REST call"},
{"label": "Does it wait or read the time?", "no": "Workflow code, as is"},
{"label": "Workflow code, using Workflow.await, Workflow.sleep, or Workflow.currentTimeMillis()", "tone": "platform"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 24.1. The determinism rule as two questions. &lt;code&gt;Thread.sleep&lt;/code&gt; and &lt;code&gt;Instant.now()&lt;/code&gt; never appear in workflow code.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@WorkflowInterface&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;WatcherWorkflow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@WorkflowMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RunResult&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WatchRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@SignalMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;decideApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@QueryMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="seq"&gt;
&lt;script type="application/json"&gt;
{
"title": "WatcherWorkflow: start, activities, query, signal, and result.",
"lifelines": [
{"id": "rest", "label": "REST endpoint", "tone": "platform"},
{"id": "wf", "label": "WatcherWorkflow", "tone": "operator"},
{"id": "act", "label": "Activities", "tone": "platform"},
{"id": "person", "label": "Person", "tone": "external"}
],
"messages": [
{"from": "rest", "to": "wf", "label": "start: run(WatchRequest)"},
{"from": "wf", "to": "act", "label": "model call (retry policy)"},
{"from": "act", "to": "wf", "label": "reply", "kind": "response"},
{"from": "wf", "to": "act", "label": "tool call (retry policy)"},
{"from": "act", "to": "wf", "label": "result", "kind": "response"},
{"from": "wf", "to": "wf", "label": "critical: timer for the approval deadline"},
{"from": "rest", "to": "wf", "label": "query: state()"},
{"from": "wf", "to": "rest", "label": "current state", "kind": "response"},
{"from": "person", "to": "wf", "label": "signal: decideApproval(true)"},
{"from": "wf", "to": "act", "label": "alert post"},
{"from": "wf", "to": "rest", "label": "RunResult", "kind": "response"}
]
}
&lt;/script&gt;
&lt;figcaption&gt;Figure 24.2. Every concept in the table, in one run. The figure leaves out how the person's approval reaches the signal; in the exercise below, a REST call sends it.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; Confirm the annotation packages and the starter configuration keys for SDK 1.35.0. Temporal&amp;rsquo;s Spring Boot starter starts the worker automatically when the app starts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise.&lt;/strong&gt; Start a run through a REST endpoint. Read its state with a query. Approve a critical run. Confirm one alert posts.&lt;/p&gt;</description></item></channel></rss>