<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yedidya Schwartz</title>
    <description>The latest articles on DEV Community by Yedidya Schwartz (@yedidya).</description>
    <link>https://dev.to/yedidya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2994362%2F67dc2bbc-637f-4300-82b1-ec484b965cce.jpg</url>
      <title>DEV Community: Yedidya Schwartz</title>
      <link>https://dev.to/yedidya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yedidya"/>
    <language>en</language>
    <item>
      <title>Orchestrating Our Agents SDLC on Temporal: The Hard Parts Nobody Warns You About</title>
      <dc:creator>Yedidya Schwartz</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:11:13 +0000</pubDate>
      <link>https://dev.to/aws-builders/orchestrating-our-agents-sdlc-on-temporal-the-hard-parts-nobody-warns-you-about-1mgc</link>
      <guid>https://dev.to/aws-builders/orchestrating-our-agents-sdlc-on-temporal-the-hard-parts-nobody-warns-you-about-1mgc</guid>
      <description>&lt;h2&gt;
  
  
  We turned a manual, prompt-by-prompt AI coding workflow into a durable, self-driving software development lifecycle. Here is everything that broke on the way, and how we fixed it.
&lt;/h2&gt;

&lt;p&gt;Lesson learned: an AI agents SDLC is not a script you write once and walk away from. It is a distributed system that runs continuously, and it demands the same operational seriousness as any other production service you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this started: doing it by hand
&lt;/h2&gt;

&lt;p&gt;Before there was any orchestration, there was a human (me) driving an AI coding assistant one prompt at a time.&lt;/p&gt;

&lt;p&gt;The loop was always the same. Feed the assistant a spec. Ask it to investigate the codebase. Ask it to plan. Ask it to write the code. Ask it to write tests. Ask it to review its own work with a fresh context. Ask it to fix what the review found. Translate the strings. Open the pull request. Every step fed the next one. Every step had a clear input, a clear output, and a clear pass/fail signal.&lt;/p&gt;

&lt;p&gt;After doing this by hand enough times, the realization was uncomfortable and obvious at the same time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is not creative work. This is a deterministic pipeline with an AI in the middle of each box.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And deterministic, repeatable, multi-step pipelines are a solved problem. We do not run them by hand. We orchestrate them.&lt;/p&gt;

&lt;p&gt;That is the whole origin of this project. We had a manual agent-driven SDLC that we ran with our hands, we noticed it was really a state machine, and we decided to make it durable and automated. The tool we reached for was Temporal, a durable execution engine that survives crashes, retries failed steps, and remembers exactly where every run is. We self-host it on EKS, right next to the worker fleet.&lt;/p&gt;

&lt;p&gt;This article is about what that actually cost us in engineering pain, and how we dealt with each problem. Everything here is generalized so you can apply it to your own stack, whatever agent, model, or cloud you are on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline and the agents
&lt;/h2&gt;

&lt;p&gt;Our SDLC is a fixed sequence of specialized agents, each with one job and a fresh context. An intent analyst turns a raw request into a structured spec. An investigator maps the codebase and finds what already exists to reuse. An architect (our most expensive model, because planning has the highest leverage) writes a file-by-file plan, which a separate plan critic has to approve before a single line is written. An editor then does the actual coding, and the same editor is reused later to apply fixes. Tests fan out in parallel: a unit-test writer and an e2e-test writer run at once, gated by a coverage critic. A staff QA agent verifies the build against the spec and the live UI in a bounded loop, sending work back to the editor until it passes. Then three reviewers on different models review in parallel and a consolidator merges their findings, feeding a fix loop. A fan-out of translators, one per locale, localizes every new string, and a merge step stitches them back together. Finally a PR opener assembles the pull request, a PR critic checks it, and a human takes it from there. The cheaper the model can be for a job, the cheaper it is: planning and review run on a top-tier model, editing on a mid-tier one, and mechanical work like translation on the cheapest. Every tier is served through Amazon Bedrock, so switching a step to a different model is a configuration change, not a new integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feajim71g8b993pcod0ei.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feajim71g8b993pcod0ei.webp" alt="Our SDLC workflow diagram" width="800" height="1964"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our SDLC workflow&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A reality check before you copy this
&lt;/h2&gt;

&lt;p&gt;Let me be blunt, because the marketing around “AI agents” makes this sound easier than it is.&lt;/p&gt;

&lt;p&gt;Temporal-as-agent-orchestrator is not a low-code toy. It is not for non-technical people. If your mental model of “AI automation” is dragging boxes in a visual builder, this is the wrong article.&lt;/p&gt;

&lt;p&gt;To do this you need to be comfortable with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributed systems thinking (workers, queues, retries, at-least-once execution).&lt;/li&gt;
&lt;li&gt;The workflow-versus-activity split, and Temporal’s determinism constraints.&lt;/li&gt;
&lt;li&gt;Object storage, secrets management, and container orchestration.&lt;/li&gt;
&lt;li&gt;The fact that your “automation” is now a fleet of long-lived worker processes that you deploy, monitor, upgrade, and get paged for.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you are not ready to own infrastructure full time, you are not ready to run an agents SDLC on Temporal.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I will keep coming back to that theme, because it is the single most underestimated part of the whole thing. The code that defines the pipeline is maybe a third of the work. The rest is operating it.&lt;/p&gt;

&lt;p&gt;With that out of the way, here are the real problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n2d4m7jxpfo55mi2yyg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n2d4m7jxpfo55mi2yyg.webp" alt="screenshot of a workflow run inside the Temporal UI" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshot from our a workflow run in our Temporal&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Determinism: the constraint that shapes everything
&lt;/h2&gt;

&lt;p&gt;Temporal splits your code into two worlds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow code&lt;/strong&gt; is the orchestration brain. It must be deterministic, because Temporal achieves durability by replaying the entire recorded event history from the beginning to rebuild state after a crash or a worker restart. Replaying the same history must produce the exact same sequence of decisions, or the engine raises a non-determinism error and refuses to proceed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity code&lt;/strong&gt; is where side effects live. Network calls, disk writes, randomness, clocks, and yes, LLM calls, all belong here. An activity’s result is recorded once and replayed from history, never recomputed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sounds academic until an AI pipeline smashes into it. An LLM call is the least deterministic thing in your entire system. It does network I/O, it is nondeterministic by design, and it costs money each time. So the rule writes itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Every model call, every file write, every API request lives in an activity. The workflow only decides what runs next.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The traps are subtle and they bite late:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reaching for the wall clock inside workflow code. Use the engine’s deterministic time source, never the language’s native clock.&lt;/li&gt;
&lt;li&gt;Randomness inside the workflow. If you need a random choice, generate it in an activity and pass it back.&lt;/li&gt;
&lt;li&gt;Importing heavy side-effecting libraries at workflow module load. Your AWS SDK (boto3) or database driver doing I/O at import time can poison determinism. Isolate those imports so the workflow definition stays pure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic once you internalize it. But it is a genuine mental shift, and it is the number one thing new team members get wrong. Onboarding an engineer into this model is a real, measurable cost, not a footnote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing state between steps: the payload limit will find you
&lt;/h2&gt;

&lt;p&gt;Here is the first wall almost everyone hits.&lt;/p&gt;

&lt;p&gt;Temporal moves inputs and outputs of workflows and activities through its persistence layer. Those payloads are capped. A single payload is limited to about 2 MB, and the whole gRPC request that carries it has a hard ceiling around 4 MB. The limit is there for a good reason: every payload becomes part of the durable history that gets replayed, so a fat payload is a fat history forever. Blow past it and you get a blob-size error that, in practice, kills the run.&lt;/p&gt;

&lt;p&gt;Now look at what an agents SDLC wants to pass around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-thousand-line specs.&lt;/li&gt;
&lt;li&gt;Whole plans and multi-step reports.&lt;/li&gt;
&lt;li&gt;Generated source files.&lt;/li&gt;
&lt;li&gt;The raw request and response bodies of model calls, which can be enormous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will blow through the payload limit almost immediately. And even if a payload squeaks under the cap, stuffing large blobs into workflow history bloats replay and slows everything down.&lt;/p&gt;

&lt;p&gt;The fix is the pattern every serious Temporal user converges on, sometimes called the claim-check pattern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do not pass the data. Pass a pointer to the data.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We offload every large artifact to object storage (S3 in our case) and thread only a small reference through the workflow: a URI, a content hash, and a filename. The shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What the workflow passes between steps.
# NOT the 200KB plan, just the address of it.
&lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArtifactRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# s3://bucket/&amp;lt;workflow_id&amp;gt;/&amp;lt;phase&amp;gt;/&amp;lt;file&amp;gt;
&lt;/span&gt;    &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;       &lt;span class="c1"&gt;# integrity check on download
&lt;/span&gt;    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;     &lt;span class="c1"&gt;# stable name to rematerialize under
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step uploads its output to object storage and returns an &lt;code&gt;ArtifactRef&lt;/code&gt;. The next step downloads the artifacts it needs into local scratch space before it invokes the model. The content hash guards against silent corruption or a truncated upload.&lt;/p&gt;

&lt;p&gt;This works, but be honest about what you just signed up for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A new failure mode.&lt;/strong&gt; Object storage can 404, throttle, or hand you a stale read. Your workflow now depends on it being healthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; Artifacts accumulate forever unless you add lifecycle rules or a scrubbing step. We ended up doing both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access management.&lt;/strong&gt; Your worker fleet needs credentials to read and write the bucket. On EKS that means IAM Roles for Service Accounts (IRSA) wired correctly, which is its own project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key discipline.&lt;/strong&gt; The object key has to be built from something globally unique and stable across retries. This detail caused us real pain, which is the next story.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The cross-pod /tmp trap: the bug that taught us the most
&lt;/h2&gt;

&lt;p&gt;This is the one to internalize, because it is invisible until you scale past one worker.&lt;/p&gt;

&lt;p&gt;When you run a single worker process, local disk feels shared. A step writes a file to local scratch space, a later step reads it back, everything works. You will build a mental model where &lt;code&gt;/tmp&lt;/code&gt; is a shared filesystem.&lt;/p&gt;

&lt;p&gt;It is not. The moment you run more than one worker replica (and you will, for throughput and availability), Temporal schedules steps across whichever worker is free. Step A lands on worker 1 and writes a file. Step B lands on worker 2 and looks for that file. It is not there. Worker 2’s local disk is a completely different disk.&lt;/p&gt;

&lt;p&gt;We hit this in three separate places, and each one looked like a different bug at first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A fan-out merge that read local scratch.&lt;/strong&gt; We fan out one child step per unit of work (think one per language, one per file), then a merge step combines the results. The children wrote their outputs to local disk. The merge step read local disk. With two workers, the children spread across both pods, and the merge only ever saw the half that happened to land on its own pod. Half the work silently vanished, and the merge cheerfully reported success on the half it could see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared input file that only one pod had.&lt;/strong&gt; A pre-step computed a canonical input once and wrote it to local disk. The fan-out children were told to read that file. Children scheduled on the other pod found nothing, and the agent, being an agent, improvised a polite little narrative explaining that its input file did not exist. That narrative then got captured as if it were the step’s real output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A latent parameter bug that only surfaced after we moved to object storage.&lt;/strong&gt; When the local-disk version worked by filename alone, we were passing the wrong identifier as the storage key and nobody noticed, because that field was never used for a path. The day we switched the consumer to build an object-storage key from that field, every download 404'd at once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson is short and absolute:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In a multi-worker fleet, local disk is a lie. Anything that crosses a step boundary must go through shared storage or the workflow payload. Never through the local filesystem.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every one of those bugs disappeared the moment the artifact, both inputs and outputs, went through object storage with the reference passed in the workflow. The fix is not clever. The discipline to apply it everywhere, before it bites, is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rerunning a single step without redoing the whole run
&lt;/h2&gt;

&lt;p&gt;Here is a mismatch that trips up newcomers.&lt;/p&gt;

&lt;p&gt;Temporal has a concept called replay. It sounds like exactly what you want when a step goes wrong. It is not. Replay means Temporal re-executes your workflow code against recorded history to rebuild in-memory state. It does not re-run your activities. It replays the decisions, not the side effects. That is a durability mechanism, not a “run this LLM call again” button.&lt;/p&gt;

&lt;p&gt;What you actually want, constantly, during development and during incident recovery, is: “re-execute this one step against the current state, and leave the other twelve alone.”&lt;/p&gt;

&lt;p&gt;The model that worked for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A step filter on the workflow input.&lt;/strong&gt; The trigger accepts an optional list of step names. When present, the workflow walks its state machine but only executes the named steps. This lets you rerun a single phase, or a contiguous slice, without paying for the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency by artifact check.&lt;/strong&gt; Before a step runs its expensive model call, it checks whether its output artifact already exists for this run. If it does, it short-circuits. Reruns become cheap and safe, and a retried step does not double-charge you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-scoped keys.&lt;/strong&gt; Every artifact key is namespaced by the unique run identifier. Rerunning a step overwrites its own slot and never collides with another run.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Design for “rerun one step” from day one. You will use it far more than you expect, both to debug and to recover cheaply.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Testing backdoors: how to validate the pipeline without going broke
&lt;/h2&gt;

&lt;p&gt;A full production run of an agents SDLC is expensive. Ours uses a top-tier model for planning, a mid-tier model for editing, and a cheap model for mechanical work. A single end-to-end run lands in the tens of dollars. You cannot iterate on the orchestration itself at that price. You need cheaper paths in, and you need them built with guardrails so they never fire in production.&lt;/p&gt;

&lt;p&gt;We built two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backdoor 1: run the entire pipeline on a cheap model
&lt;/h2&gt;

&lt;p&gt;A single global flag forces every step onto the cheapest available model, regardless of what that step normally uses. The full pipeline runs end to end for the price of a coffee instead of a nice dinner. This is how you validate control flow, artifact hand-off, retries, and the S3 plumbing without burning real budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_cheap_model_testing_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;active_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CHEAP_MODEL&lt;/span&gt;          &lt;span class="c1"&gt;# every phase, no exceptions
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;active_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferred_model&lt;/span&gt;  &lt;span class="c1"&gt;# production assignment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hard-won caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cheap model validates plumbing, not quality.&lt;/strong&gt; When we ran the whole thing on the cheapest model, the mechanical steps passed, but the code-writing steps produced beautiful reports and made zero actual edits. The cheap model was strong enough to describe the work and too weak to do it. That is fine, as long as you know that the cheap path proves the pipeline moves, not that the output is good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The flag must default to off, loudly.&lt;/strong&gt; A test backdoor that can accidentally run in production is a footgun. Default off, log conspicuously when on, and never let the production trigger set it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backdoor 2: run a single step in isolation
&lt;/h2&gt;

&lt;p&gt;The same step filter that powers cheap reruns doubles as a test harness. Want to test just the review step? Trigger the workflow with a filter of exactly one step name. Want a near-free smoke test that proves a worker can pick up a task, call the model, and write an artifact? Keep a dedicated trivial step that is opt-in only and never part of a default run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Bake in a cheap-model full run and a single-step run before you need them. The alternative is debugging a forty-dollar pipeline forty dollars at a time.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The design principle across both backdoors: test paths are injected through configuration (which model, which steps), never forked into a separate code path. A separate “test mode” codebase drifts from production and lies to you. One code path, parameterized, is the only version that stays honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering a run from GitHub Actions
&lt;/h2&gt;

&lt;p&gt;We did not want a bespoke UI just to start a run, and we did not want to expose the orchestrator to the public internet. GitHub Actions turned out to be the perfect front door. A manual &lt;code&gt;workflow_dispatch&lt;/code&gt; job gives you a free, permissioned, audited form: a dropdown and a few text fields that anyone on the team can fill in without touching &lt;code&gt;kubectl&lt;/code&gt;. The job itself does almost nothing. It takes the inputs, builds a small JSON body, and makes a single authenticated call to the orchestrator's API, which is what actually starts the durable run.&lt;/p&gt;

&lt;p&gt;The one detail worth copying: run that job on a self-hosted runner that lives inside the same network as your workers - in our case, inside the EKS cluster - and talk to the orchestrator over internal DNS. That way the trigger endpoint never has to be publicly reachable, and you get GitHub’s identity and audit trail for free instead of building your own auth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# what to build&lt;/span&gt;
      &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# source branch&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;dev&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;# which env to run against&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# optional single-step / subset filter&lt;/span&gt;
      &lt;span class="na"&gt;cheap_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;boolean&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# the cheap-model test path&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;            &lt;span class="c1"&gt;# in-cluster runner, reaches the API over internal DNS&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start the run&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsS -X POST "http://orchestrator.internal/runs" \&lt;/span&gt;
            &lt;span class="s"&gt;-H 'content-type: application/json' \&lt;/span&gt;
            &lt;span class="s"&gt;-d "$(jq -n \&lt;/span&gt;
                  &lt;span class="s"&gt;--arg t  '${{ inputs.target }}' \&lt;/span&gt;
                  &lt;span class="s"&gt;--arg b  '${{ inputs.branch }}' \&lt;/span&gt;
                  &lt;span class="s"&gt;--arg e  '${{ inputs.environment }}' \&lt;/span&gt;
                  &lt;span class="s"&gt;'{target:$t, branch:$b, environment:$e}')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same input surface is where your test backdoors live: the &lt;code&gt;steps&lt;/code&gt; field is your single-step rerun path, and the &lt;code&gt;cheap_mode&lt;/code&gt; toggle is your cheap-model full run. Both are just parameters on the same trigger, which keeps them honest, because there is exactly one way to start a run and the test paths are options on it, not a separate code path.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Do not build a launcher. You already have one. A&lt;/em&gt; &lt;code&gt;workflow_dispatch&lt;/code&gt; &lt;em&gt;form plus a self-hosted runner gives you a permissioned, audited trigger for free, and keeps the orchestrator off the public internet.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A couple of notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I placed the test-backdoor callback deliberately so this section reinforces the “one parameterized code path, not a separate test mode” point from earlier in the article. If you drop this section in right after the Testing backdoors section, that callback lands naturally. It also works as its own standalone section near the end.&lt;/li&gt;
&lt;li&gt;Everything is generic: &lt;code&gt;orchestrator.internal&lt;/code&gt;, generic field names, no product or company specifics. Adjust the endpoint path and field names to taste.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost: durable execution can quietly inflate the bill
&lt;/h2&gt;

&lt;p&gt;Cost shows up in more places than the token meter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model tokens.&lt;/strong&gt; The obvious one. Per-step model selection is your biggest lever: do not send a planning-tier model to do mechanical work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A run that fails late is pure waste.&lt;/strong&gt; We had a run escalate near the very end without producing a usable result. It had already spent most of a full run’s budget on the earlier steps. Money spent, nothing shipped. This is the expensive failure mode of a long pipeline, and it is an argument for gating and early exits, not just retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived workers cost money even when idle.&lt;/strong&gt; Durable execution keeps worker processes running and polling. That is compute you pay for around the clock, separate from any model spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History and storage growth.&lt;/strong&gt; Durable runs accumulate event history and artifacts. Left unmanaged, both grow without bound and both cost money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a hard ceiling hiding here that specifically bites agents. A single workflow execution’s event history is capped (tens of thousands of events, tens of megabytes), with a warning threshold well before that. A tight agent loop that iterates many times, each iteration recording several events, will march toward that ceiling. The escape hatch is to periodically close the current run and start a fresh one with the same identity and a clean history, carrying forward only the state you need. If your agent has any kind of unbounded “keep going until done” loop, you must either bound the iterations or plan for this rollover from the start. We bound our review-and-fix loops to a fixed number of rounds, which sidesteps the problem and, not coincidentally, also caps the cost of a single run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The durable-execution model trades money for reliability. That is usually a good trade. Just make sure you can see the bill, per run and per step, or it will surprise you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Long-running steps, heartbeats, and slow model responses
&lt;/h2&gt;

&lt;p&gt;Agent steps are slow. A single reasoning-heavy model call with tool use can run for many minutes. Temporal, sensibly, wants to know a step is still alive so it can reschedule genuinely dead ones.&lt;/p&gt;

&lt;p&gt;Temporal gives you several timeouts to reason about: how long a single attempt may run, how long the total including retries may run, how long a task may sit in the queue before a worker picks it up, and the maximum gap between heartbeats. For agents, two of them matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set your run-time timeout to reality.&lt;/strong&gt; A single attempt has to accommodate the slowest legitimate model response plus tool calls, with margin. Too tight and Temporal kills healthy work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat during the long gaps, or you will pay twice.&lt;/strong&gt; We heartbeat on every message the model streams back, and we run a baseline heartbeat on a timer so that even a long silent stretch of tool execution keeps the lease alive. This is not just about avoiding confusion. If a step stops heartbeating, Temporal assumes the worker died and retries the step, which fires a second, identical, expensive model call while the first one is very likely still running. Missing heartbeats do not just look bad in a dashboard, they double your token bill on your slowest steps.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer step:
  start a background heartbeat every 30s
  for each streamed message from the model:
     record output, heartbeat with progress
  stop the background heartbeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streaming responses are a gift here: each chunk is a natural heartbeat opportunity. If your model client only returns at the end, you must run a timer-based heartbeat or you will fight false timeouts forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries, backoff, and the difference between “retry” and “escalate”
&lt;/h2&gt;

&lt;p&gt;Model APIs are flaky. Rate limits, transient 5xx, the occasional refusal. Temporal’s retry policies handle the transient cases well: exponential backoff, capped attempts, per-activity tuning.&lt;/p&gt;

&lt;p&gt;One gotcha up front: the default retry policy retries forever. On a rate-limited model API, that default turns a temporary 429 into a retry storm that hammers the provider and burns your quota. Cap the attempts, cap the maximum interval, and when the provider hands you a &lt;code&gt;Retry-After&lt;/code&gt; header, honor it by setting the next retry delay explicitly instead of letting blind backoff guess.&lt;/p&gt;

&lt;p&gt;The deeper nuance that matters for agents is what should not be retried at all.&lt;/p&gt;

&lt;p&gt;Some failures are not transient. If a step produced output that fails a hard validation, retrying the identical step usually reproduces the identical failure while burning tokens each time. You want those to stop retrying and escalate, either to a different branch of the workflow or to a human.&lt;/p&gt;

&lt;p&gt;We model this with a small set of non-retryable error types. When a step raises one of those, Temporal does not spin; the workflow catches it and decides what to do next.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Retry transient infrastructure failures. Escalate deterministic content failures. Retrying an agent that is confidently wrong just pays to be told the same wrong thing again.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Agents lie about success, so put mechanical gates between them
&lt;/h2&gt;

&lt;p&gt;This one is philosophical and extremely practical.&lt;/p&gt;

&lt;p&gt;Agents are optimistic narrators. Ask a coding agent whether it ran the type checker and it will often tell you, sincerely, that everything passed. Then you look, and the workspace has half a dozen compile errors from hallucinated APIs.&lt;/p&gt;

&lt;p&gt;We learned the hard way that a prompt instruction like “you must run the build and confirm it is green before returning” is necessary but not sufficient. Agents skip mandated steps and report success anyway.&lt;/p&gt;

&lt;p&gt;The fix is to stop trusting self-reports and add deterministic gates:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A prompt-level requirement is one refactor away from being ignored. A mechanical gate is enforcement.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Concretely: after a code-writing step returns, an activity independently runs the real type check and build against the resulting workspace. If it fails, the step fails, with the compiler output attached, and the workflow escalates instead of advancing. The agent’s opinion of its own work is an input, not the verdict.&lt;/p&gt;

&lt;p&gt;This also fixed an observability lie we will get to below: once you have an external gate, “the agent said it passed” and “it actually passed” become two separate, comparable facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine-parseable contracts between agent and orchestrator
&lt;/h2&gt;

&lt;p&gt;Related trap: your orchestrator has to make control-flow decisions based on what an agent produced. Pass or fail. Revise or approve. If you parse that decision out of free-form prose, you are one polite paragraph away from a stuck workflow.&lt;/p&gt;

&lt;p&gt;We had a review step escalate the entire run for a dumb reason: the agent wrote a perfectly good summary of its fixes but never emitted the exact verdict token the orchestrator was grepping for. No token, no decision, escalate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The boundary between an agent and your control flow must be a strict, machine-parseable contract. Require an exact verdict line, and validate it. If the agent does not produce it, that itself is a failure, not a maybe.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prose is for humans. Control flow needs a token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: wiring LLM tracing into a durable engine
&lt;/h2&gt;

&lt;p&gt;Temporal gives you excellent visibility into its own world: every workflow, every step, every retry, every input and output, all queryable. What it does not give you is the thing you most want to see in an agents SDLC: what the model actually did. Tokens, prompts, responses, tool calls, cost per step.&lt;/p&gt;

&lt;p&gt;For that we plugged in a dedicated LLM tracing tool (Langfuse, self-hosted on the same EKS cluster, in our case). You end up running two observability planes: one for orchestration and durability, one for prompts, tokens, and cost. Getting them to line up was more friction than expected.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxb8cz0ahd8mdb5rkhxxk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxb8cz0ahd8mdb5rkhxxk.webp" alt="Langfuse screenshot showing the cost breakdown of a single LLM call" width="799" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshot from our Langfuse, showing a cost breakdown of a specific LLM call&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Span propagation across process boundaries.&lt;/strong&gt; Our model calls happen inside a subprocess spawned by the step. To make the model’s own spans nest under the right workflow-and-step span, we inject the trace context (the W3C trace parent and state) into the subprocess environment. Without that, you get two disconnected trees: Temporal’s view of the run, and a pile of orphan LLM spans with no idea which run or step they belong to. Correlating them by hand is misery. Propagate the trace context explicitly, or you do not really have distributed tracing, you have two separate logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tracing backend is infrastructure you now own.&lt;/strong&gt; This is the part nobody warns you about. Your LLM tracing tool has its own database, its own object storage, its own failure modes. Ours quietly stopped ingesting traces for a long time. The dashboard showed zero traces and everyone assumed the instrumentation was broken. The real cause: the object storage backing the tracing tool had run out of inodes (not out of space, out of inodes, because it wrote millions of tiny objects), and every write was failing silently behind a generic error. The fix was half cleanup and half migrating that storage to something with proper lifecycle management.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Your observability stack is production infrastructure. If you do not monitor the thing that monitors your agents, you will discover it is down only when you desperately need it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Watch out for observability that lies.&lt;/strong&gt; The best bug of the whole project: a per-step metric that counted how many tool calls the agent made. It always reported zero. For a while we believed the agents were doing nothing. They were doing plenty; the counter was checking the wrong attribute shape on the SDK’s response objects and silently matching nothing. A step that wrote dozens of files logged “0 tool calls.” An instrument that reports zero when the truth is fifty-five is worse than no instrument, because it actively misleads triage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Verify your instrumentation against ground truth at least once. A metric you never validated is a rumor.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Versioning workflows when the agent logic changes weekly
&lt;/h2&gt;

&lt;p&gt;Agent pipelines change constantly. You reorder steps, add a gate, change a prompt, split one step into two. Meanwhile, Temporal may have long-running executions in flight that started under the old code. Determinism means you cannot just change the workflow and redeploy; a replay of an old run against new code will diverge from its recorded history and fail.&lt;/p&gt;

&lt;p&gt;Temporal has first-class tools for this: patch-style version gates you place in the workflow code, and worker-level versioning (build IDs pinned to executions) so old and new runs route to compatible workers. Recent guidance leans toward worker versioning as the default for teams that can run versioned deployments. The practical takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your runs are short-lived, the simplest safe move is to drain old runs before rolling out incompatible workflow changes.&lt;/li&gt;
&lt;li&gt;If your runs are long, you must use the versioning primitives deliberately. Every incompatible change to workflow (not activity) code needs a version gate.&lt;/li&gt;
&lt;li&gt;Changing activity code is far more forgiving than changing workflow code. Push behavior into activities partly for this reason: it is the layer you can evolve freely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a recurring tax. Frequent agent-logic changes and long-lived durable executions are in natural tension, and you pay for it in versioning discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-in-the-loop is a feature, not an afterthought
&lt;/h2&gt;

&lt;p&gt;A fully autonomous agents SDLC that opens a pull request and merges it with nobody looking is a great way to ship confident nonsense to production.&lt;/p&gt;

&lt;p&gt;Temporal’s signals, updates, and timers make human checkpoints clean to model. A workflow can pause and wait for an external approval, with a timeout that escalates if nobody responds. We keep humans at the high-leverage gates: the plan looks right before code gets written, and the final change gets human review before it lands.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Autonomy is a dial, not a switch. Put the human where a wrong decision is most expensive, and let the machine run everything cheap and reversible.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Local development and debugging nondeterministic agents
&lt;/h2&gt;

&lt;p&gt;Testing a nondeterministic thing on a deterministic engine is inherently awkward, and no amount of tooling fully removes the awkwardness.&lt;/p&gt;

&lt;p&gt;Temporal’s test framework is genuinely good: a time-skipping test server, and replay tests that catch determinism violations before they reach production. Use both. Replay tests in particular are your safety net against accidentally breaking workflow determinism during a refactor.&lt;/p&gt;

&lt;p&gt;But the agent itself will never be reproducible. Replay tests catch code drift, not model drift. Two things made debugging bearable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persist the raw model exchanges.&lt;/strong&gt; We dump every request and response body to disk per run and per step. When a step misbehaves, the transcript is right there. This is how we proved the “0 tool calls” metric was lying and how we caught agents writing to the wrong output paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured, queryable step records.&lt;/strong&gt; Every step writes a record with its inputs, outputs, token counts, cost, and duration. When something goes wrong three steps back, you are reading records, not scrolling logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You cannot make the agent deterministic. You can make its behavior fully auditable after the fact. Do that.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The part nobody budgets for: this is a system you run forever
&lt;/h2&gt;

&lt;p&gt;Here is the theme I promised to keep hammering.&lt;/p&gt;

&lt;p&gt;The workflow code is not the product. The running system is the product, and it never stops needing you. A partial, honest list of what “operating an agents SDLC on Temporal” actually involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A worker fleet to deploy, scale, and roll safely on every change.&lt;/li&gt;
&lt;li&gt;Version pinning between workflow code, worker builds, and in-flight executions.&lt;/li&gt;
&lt;li&gt;A persistence store for Temporal itself (Amazon RDS, in our case), with its own capacity and backup story.&lt;/li&gt;
&lt;li&gt;The object storage layer for artifacts, with lifecycle and cleanup.&lt;/li&gt;
&lt;li&gt;The LLM tracing stack, with its own database and storage to monitor.&lt;/li&gt;
&lt;li&gt;Cloud credentials and workload identity (IAM roles and IRSA) for every component that touches a model API or a bucket, which rotate and expire and break at the worst time.&lt;/li&gt;
&lt;li&gt;Monitoring and alerting on all of the above, plus somebody on call when a run wedges at 2am.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During this project we lost time to expiring AWS SSO sessions, VPN and DNS resolution flakiness getting to the cluster, identity federation for model-provider auth breaking after a platform change, and an Amazon RDS password rotation that desynced and crashlooped a service. None of that is agent work. All of it is the cost of running the system that runs the agents.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Budget for operations as a standing cost, not a one-time setup. If you cannot staff the “run it forever” part, do not start the “build it once” part.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A comparison, briefly
&lt;/h2&gt;

&lt;p&gt;Temporal is not the only way to orchestrate agents, and it is heavier than the alternatives.&lt;/p&gt;

&lt;p&gt;It helps to separate three different jobs people all call “orchestration”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch and data scheduling&lt;/strong&gt; (the DAG schedulers): great at “run this graph of jobs on a schedule,” not built for durable per-step replay or human waits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent frameworks and graph libraries&lt;/strong&gt; (the AI-native tools): purpose-built for the agent loop itself, with streaming, memory, tool routing, and LLM evals baked in. They let you wire up multi-step agent flows in a single process with almost no infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable execution engines&lt;/strong&gt; (Temporal, and lighter cousins backed by a database you already run): built to survive a crash mid-run, resume exactly where you were, retry one step with backoff, wait days for a human signal, and keep a queryable record of thousands of concurrent runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI-native frameworks give you streaming, memory, and prompt management for free, and Temporal makes you build all of that yourself. What Temporal gives you in return is durability that the lightweight tools simply do not have. A very common and very sane pattern is to run both: a durable engine for the long-running orchestration, and an agent framework for the reasoning loop inside a step.&lt;/p&gt;

&lt;p&gt;That durability is the entire reason to pay Temporal’s tax.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Choose a durable engine when a half-finished run is expensive and “just start over” is not acceptable. Choose something lighter when it is.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you commit: a checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Are your steps genuinely a deterministic pipeline, or are you forcing structure onto something exploratory? Temporal rewards the former and fights the latter.&lt;/li&gt;
&lt;li&gt;Can you keep every side effect, especially model calls, inside activities without contorting your design?&lt;/li&gt;
&lt;li&gt;Have you designed artifact hand-off through object storage from the start, with references in the workflow and nothing large in the payload?&lt;/li&gt;
&lt;li&gt;Do you have a single-step rerun path and a cheap-model full run, with guardrails so test paths never hit production?&lt;/li&gt;
&lt;li&gt;Is every agent-to-orchestrator decision a strict, machine-parseable contract?&lt;/li&gt;
&lt;li&gt;Do you have mechanical gates that verify agent claims instead of trusting them?&lt;/li&gt;
&lt;li&gt;Is your LLM tracing stack monitored like the production infrastructure it is?&lt;/li&gt;
&lt;li&gt;Do you have the people and the on-call maturity to run this indefinitely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most of those are “yes,” Temporal will serve you extremely well. If most are “no,” you will spend more time fighting the engine than shipping features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: worth it, with eyes open
&lt;/h2&gt;

&lt;p&gt;Turning a manual, prompt-by-prompt agent workflow into a durable orchestrated SDLC was one of the highest-leverage things we built. Runs survive crashes. Failed steps retry themselves. A run that dies at step nine resumes at step nine. We can rerun one phase for pennies, trace every model call to its cost, and gate the agents behind checks they cannot talk their way past.&lt;/p&gt;

&lt;p&gt;But none of that came from the happy-path tutorial. It came from the payload limit, the cross-pod disk trap, the lying metric, the silent tracing outage, the confidently-wrong agents, and the steady operational drip of auth, DNS, and rollout problems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Temporal makes an agents SDLC durable. It does not make it easy, and it does not make it self-maintaining. It is a system, and systems have owners.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have the engineering maturity and the appetite to own infrastructure, orchestrating your agents SDLC on a durable execution engine is absolutely worth it. If you were hoping for a set-and-forget button, this is not that, and anyone who tells you otherwise has not run one in production.&lt;/p&gt;

&lt;p&gt;We did it by hand, we made it deterministic, and then we spent the real effort making the determinism survive contact with reality. That last part is the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading and resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Temporal core concepts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/" rel="noopener noreferrer"&gt;Temporal documentation home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/workflow-definition" rel="noopener noreferrer"&gt;Workflow definition and determinism constraints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/concepts" rel="noopener noreferrer"&gt;Temporal core concepts (workflows, activities, workers, task queues)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.temporal.io/courses/" rel="noopener noreferrer"&gt;Temporal courses (free, hands-on)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Determinism, replay, and versioning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/typescript/versioning" rel="noopener noreferrer"&gt;Workflow versioning with patching (TypeScript)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/go/versioning" rel="noopener noreferrer"&gt;Workflow versioning (Go)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/production-deployment/worker-deployments/worker-versioning" rel="noopener noreferrer"&gt;Worker versioning and build IDs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bitovi.com/blog/replay-testing-to-avoid-non-determinism-in-temporal-workflows" rel="noopener noreferrer"&gt;Replay testing to avoid non-determinism (Bitovi)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Payload size and passing large state (the claim-check pattern)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/troubleshooting/blob-size-limit-error" rel="noopener noreferrer"&gt;Troubleshooting the blob size limit error&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/external-storage" rel="noopener noreferrer"&gt;External storage / large payload codec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.temporal.io/t/best-practice-to-handle-large-workflow-activity-payload-blob-size-limit-2mb/9814" rel="noopener noreferrer"&gt;Community thread: handling large workflow/activity payloads&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timeouts, heartbeats, and long-running work
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://temporal.io/blog/activity-timeouts" rel="noopener noreferrer"&gt;Activity timeouts explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/encyclopedia/detecting-activity-failures" rel="noopener noreferrer"&gt;Detecting activity failures and heartbeating&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://temporal.io/blog/very-long-running-workflows" rel="noopener noreferrer"&gt;Very long-running workflows and Continue-As-New&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/workflow-execution/limits" rel="noopener noreferrer"&gt;Workflow execution limits (event history caps)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Retries, backoff, and flaky model APIs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/encyclopedia/retry-policies" rel="noopener noreferrer"&gt;Retry policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/ai-cookbook/http-retry-enhancement-python" rel="noopener noreferrer"&gt;Handling HTTP 429 / Retry-After in activities (Temporal AI cookbook)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://temporal.io/blog/rate-limit-downstream-apis" rel="noopener noreferrer"&gt;Rate-limiting downstream APIs from Temporal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Human-in-the-loop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/workflows" rel="noopener noreferrer"&gt;Signals, updates, queries, and timers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing and local development
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/python/testing-suite" rel="noopener noreferrer"&gt;Testing suite (Python)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/go/testing-suite" rel="noopener noreferrer"&gt;Testing suite (Go)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helpmetest.com/blog/temporal-workflow-testing/" rel="noopener noreferrer"&gt;Practical guide to Temporal workflow testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Observability: LLM-level tracing on top of Temporal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/integrations/native/opentelemetry" rel="noopener noreferrer"&gt;Langfuse OpenTelemetry integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://langfuse.com/docs/observability/sdk/instrumentation" rel="noopener noreferrer"&gt;Langfuse SDK instrumentation and trace context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/traceloop/openllmetry" rel="noopener noreferrer"&gt;OpenLLMetry / OpenLIT (open-source LLM OpenTelemetry)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/trace-context/" rel="noopener noreferrer"&gt;W3C Trace Context specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Temporal for AI agents specifically
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://temporal.io/blog/of-course-you-can-build-dynamic-ai-agents-with-temporal" rel="noopener noreferrer"&gt;Building dynamic AI agents with Temporal (official)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/ai-cookbook" rel="noopener noreferrer"&gt;Temporal AI cookbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xgrid.co/resources/temporal-ai-agent-orchestration-failure-patterns/" rel="noopener noreferrer"&gt;Production pitfalls of Temporal for AI agent orchestration (xGrid)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Self-hosting and cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/self-hosted-guide/defaults" rel="noopener noreferrer"&gt;Self-hosted Temporal defaults and persistence stores&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://temporal.io/pricing" rel="noopener noreferrer"&gt;Temporal Cloud pricing (Actions model)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Comparisons and alternatives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.langchain.com/resources/langgraph-vs-temporal" rel="noopener noreferrer"&gt;LangGraph vs Temporal (LangChain, vendor perspective)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.zenml.io/blog/temporal-alternatives" rel="noopener noreferrer"&gt;Temporal alternatives overview (ZenML)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hackernoon.com/a-practical-guide-to-temporal-what-it-does-how-it-compares-and-when-to-use-it" rel="noopener noreferrer"&gt;A practical guide to Temporal: what it does and when to use it (HackerNoon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restate.dev/" rel="noopener noreferrer"&gt;Restate (lightweight durable execution)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dbos.dev/" rel="noopener noreferrer"&gt;DBOS (durable execution on Postgres)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The AI-native agent frameworks (for the reasoning loop inside a step)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://langchain-ai.github.io/langgraph/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>temporal</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Human-in-the-Loop Factor: Partnering With Amazon Q During a Production Incident</title>
      <dc:creator>Yedidya Schwartz</dc:creator>
      <pubDate>Sat, 23 Aug 2025 17:24:49 +0000</pubDate>
      <link>https://dev.to/aws-builders/the-human-in-the-loop-factor-partnering-with-amazon-q-during-a-production-incident-312m</link>
      <guid>https://dev.to/aws-builders/the-human-in-the-loop-factor-partnering-with-amazon-q-during-a-production-incident-312m</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;A production outage hit our Debezium cluster that streams MongoDB change events into BigQuery. The first time it happened we realized only hours later, which meant data loss for everything that did not make it to BigQuery. When it happened again, I used Amazon Q to guide the investigation, propose a proven remediation, and help me implement and validate an automated fix that shrinks the data loss window from minutes to seconds. More importantly, this was not about asking the AI to ״solve the incident״ in one shot. I broke the problem into structured steps: letting Q investigate the cluster, reviewing remediation options, simulating failure in staging, validating deployments, and finally summarizing the process for the team.  &lt;/p&gt;

&lt;p&gt;The outcome was threefold: a practical safeguard that saved real data, a hands-on way to use AI as a Kubernetes CLI companion while learning commands myself, and a strong reminder that human judgment is still needed in the AI feedback loop. With the right thinking cues, the AI delivered context-aware, reliable help that went the extra mile during a critical production incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Debezium, and our case study
&lt;/h2&gt;

&lt;p&gt;Debezium is a change data capture platform. In our setup, Debezium Server tails MongoDB Atlas change streams and writes events to Google BigQuery. The value is simple: low latency ingestion of document changes into an analytics warehouse where downstream models and dashboards expect near real time data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: MongoDB Atlas change streams
&lt;/li&gt;
&lt;li&gt;Transport and processing: Debezium Server running on GKE as a StatefulSet
&lt;/li&gt;
&lt;li&gt;Sink: BigQuery
&lt;/li&gt;
&lt;li&gt;Goal: keep the pipeline hot so analytics stays fresh
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  An unfamiliar cluster, and an unavailable owner
&lt;/h2&gt;

&lt;p&gt;I've just had an internal transition in the company to take part in the data team infrastructure efforts, and I'm not familiar with all their systems yet. The teammate who normally owns this cluster was unavailable after a bike crash, so I was missing context and access. Amazon Q bridged that gap quickly by helping me authenticate to GCP, fetch GKE credentials, and verify connectivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: I want to connect to my gcp account and add to my local kube config the gke cluster debezium-server-stg in us-east1

Amazon Q: I can help you connect to your GCP account and add the GKE cluster to your local kubeconfig. Here are the steps:
Amazon Q: I will run the following shell command: gcloud auth login
Amazon Q: gcloud container clusters get-credentials debezium-server-stg --region us-east1
Amazon Q: Your current kubectl context is now set to: ***
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: Some of the chat excerpts with Amazon Q have been significantly shortened and summarized.&lt;br&gt;&lt;br&gt;
The article highlights only the essential parts to convey the ideas clearly and keep the reading experience smooth, without overwhelming the reader with repetitive technical details.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Background to the incident and why we needed automation
&lt;/h2&gt;

&lt;p&gt;The first occurrence went unnoticed for a few hours. Everything between the initial error and the manual remediation was lost to BigQuery. After it fired again, this time with better alerting, we decided to automate the remediation so the next failure would auto heal in seconds, not minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1. Asking Amazon Q what happened
&lt;/h2&gt;

&lt;p&gt;I asked Q to take the lead on understanding the failure, even though I already suspected the cause. The idea was to let him explore the pod state, events, and logs on his own, so his remediation suggestions would be grounded in context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: I need you to look at debezium-server-prod cluster, specifically what happened around 2am-3am Israel time. the Debezium stopped functioning with Atlas Mongo CDC integration, I need to understand the root cause

Amazon Q: I'll help you investigate what happened to the Debezium pod around 2am–3am Israel time today. Let me start by examining the cluster and finding the Debezium pod.

kubectl get pods -A | grep -i debezium
kubectl describe pod debezium-gke-prod-0 -n debezium-prod
kubectl logs debezium-gke-prod-0 -n debezium-prod --tail=100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evidence from logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;... Resume of change stream was not possible ...
... with error 286 (ChangeStreamHistoryLost) ...
... the resume point may no longer be in the oplog ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2. Asking Amazon Q for possible solutions
&lt;/h2&gt;

&lt;p&gt;Once Q had the picture, I asked for potential remediations. He quickly recommended few options, and we went with its recommendation of modifying the liveness probe to detect ChangeStreamHistoryLost errors and reset the offsets file so Debezium could restart cleanly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: my friend removed the offset file to solve it from inside the pod, that's what solved the issue. I would like to have an auto remediation for such issue if it happens again. what do you suggest?

Amazon Q: Great insight. Your friend's solution makes perfect sense. Removing the offsets file forces Debezium to start fresh instead of trying to resume from an invalid position. Let me suggest several auto-remediation approaches for this specific issue.

Amazon Q: Option 1: Smart liveness probe (recommended). Detect specific MongoDB resume failures such as ChangeStreamHistoryLost and self heal by removing the problematic offsets file, then exit with non zero to trigger a restart.

Amazon Q: Option 2: Sidecar container. Run a lightweight helper container in the same pod that tails the Debezium logs and triggers cleanup actions if the error is observed. This separates remediation logic from Debezium itself.
I considered other solutions, but the liveness probe approach was both simplest and fastest to implement while still giving us the observability and safety we needed. It required no extra infrastructure, no sidecars, and no external controllers - just Kubernetes doing what it already knows best.

Amazon Q: Option 3: Kubernetes CronJob or Operator. Periodically check Debezium health at the cluster level, and when unhealthy, reset the offsets and restart the StatefulSet. This decouples remediation from pod lifecycle.

Amazon Q: Option 4: External monitoring system integration. Alerting via Prometheus or Cloud Monitoring can trigger a remediation workflow (for example, a Cloud Function that restarts the StatefulSet).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3. Asking Amazon Q to implement the solution
&lt;/h2&gt;

&lt;p&gt;After seeing Q’s initial suggestion, I asked him to help me actually build the probe inside our Debezium StatefulSet. At first, Q suggested a very simple log check that would grep for &lt;code&gt;ChangeStreamHistoryLost&lt;/code&gt; and remove the offsets file. That worked in principle, but it was too naive for production.  &lt;/p&gt;

&lt;p&gt;I directed Q to think deeper:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle startup when the log might not exist yet,
&lt;/li&gt;
&lt;li&gt;make sure we back up the log with a timestamp for debugging,
&lt;/li&gt;
&lt;li&gt;prune old backups to avoid PVC bloat,
&lt;/li&gt;
&lt;li&gt;truncate the log in place after remediation,
&lt;/li&gt;
&lt;li&gt;and catch generic errors on the last line without panicking on old log noise.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how the probe evolved through our conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: Let's add a liveness probe to automatically detect the error and remove the offset file if it happens.

Amazon Q: Sure. We can create a simple probe that tails the log and looks for 'ChangeStreamHistoryLost'. If detected, remove offsets.dat and exit 1.

Me: That's a start, but we need something more comprehensive. It should handle missing logs at startup, back up the log with timestamps, clean up old backups, and truncate the file. Also, let’s still fail if the last line shows a fresh ERROR.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the final version we built together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/bin/sh&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;LOG="/debezium/data/logs/debezium.log"&lt;/span&gt;

      &lt;span class="s"&gt;# Healthy if log isn't there yet (startup/warmup)&lt;/span&gt;
      &lt;span class="s"&gt;if [ ! -f "$LOG" ]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Log file not found; assuming healthy."&lt;/span&gt;
        &lt;span class="s"&gt;exit 0&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="s"&gt;# Detect the resume issue only in the current log's tail&lt;/span&gt;
      &lt;span class="s"&gt;if tail -n 80 "$LOG" | grep -E -q 'ChangeStreamHistoryLost|Resume of change stream was not possible'; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "AUTO-REMEDIATION: Detected MongoDB oplog resume issue"&lt;/span&gt;

        &lt;span class="s"&gt;# Timestamped backup, UTC for portability&lt;/span&gt;
        &lt;span class="s"&gt;TS="$(date -u +%Y%m%dT%H%M%SZ)"&lt;/span&gt;
        &lt;span class="s"&gt;BAK="/debezium/data/logs/debezium.log.backup-$TS"&lt;/span&gt;

        &lt;span class="s"&gt;# Best-effort backup&lt;/span&gt;
        &lt;span class="s"&gt;cp "$LOG" "$BAK" 2&amp;gt;/dev/null || cat "$LOG" &amp;gt; "$BAK" || true&lt;/span&gt;
        &lt;span class="s"&gt;echo "Backed up current log to: $BAK"&lt;/span&gt;

        &lt;span class="s"&gt;# Clear offsets and truncate the active log in place&lt;/span&gt;
        &lt;span class="s"&gt;rm -f /debezium/data/offsets.dat || true&lt;/span&gt;
        &lt;span class="s"&gt;: &amp;gt; "$LOG" || true&lt;/span&gt;

        &lt;span class="s"&gt;# Optional: prune old backups (&amp;gt;7 days) to avoid PVC bloat&lt;/span&gt;
        &lt;span class="s"&gt;find /debezium/data/logs -maxdepth 1 -type f -name 'debezium.log.backup-*' -mtime +7 -print -delete 2&amp;gt;/dev/null || true&lt;/span&gt;

        &lt;span class="s"&gt;# Trigger immediate restart&lt;/span&gt;
        &lt;span class="s"&gt;exit 1&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="s"&gt;# Optional: fail on a fresh last-line ERROR&lt;/span&gt;
      &lt;span class="s"&gt;if tail -n 1 "$LOG" | grep -q 'ERROR'; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "ERROR found on last line; failing liveness."&lt;/span&gt;
        &lt;span class="s"&gt;exit 1&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="s"&gt;echo "Healthy."&lt;/span&gt;
      &lt;span class="s"&gt;exit 0&lt;/span&gt;
  &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides the script itself, the probe configuration had to be changed too, for reducing the reaction time of Kubernetes so remediation kicks in as soon as possible.&lt;/p&gt;

&lt;p&gt;Originally, the probe ran every 10 seconds (periodSeconds: 10) and allowed 5 failures (failureThreshold: 5) before marking the pod unhealthy. That means the pod could sit in a broken state for up to ~50 seconds before Kubernetes restarted it. In a streaming pipeline like Debezium, 50 seconds of downtime means thousands of events lost.&lt;/p&gt;

&lt;p&gt;By tightening it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialDelaySeconds: 60
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I achieved two key improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Faster detection: the probe now samples every 5 seconds, so it sees the error quickly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Immediate remediation: failing on the first detection (failureThreshold: 1) means Kubernetes restarts Debezium right away, instead of waiting for repeated confirmation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination shortens the outage window from potentially close to a minute down to just a few seconds, which is critical for minimizing data loss in CDC pipelines.&lt;/p&gt;

&lt;p&gt;By pushing Q step by step, I ended up with a much more resilient solution than the first draft. Instead of a hacky quick fix, the probe became a well-rounded, production-ready safeguard that not only healed the cluster automatically but also preserved valuable debugging context for the team.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4. Asking Amazon Q to explain the existing CD for the Debezium repository
&lt;/h2&gt;

&lt;p&gt;Before rolling the change, I asked Q to explain how our CD pipeline deploys manifests. He parsed the repo, confirmed the StatefulSet and config structure, and reassured me that applying this change through the CD path would be consistent and safe.&lt;/p&gt;

&lt;p&gt;Here is the exact guidance Q gave me after reviewing the GitHub Actions in the CDCStreaming project. He explained how the release trigger works and how staging deployments are normally done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: check the github actions in CDCStreaming project, how should I deploy to staging cluster?

Amazon Q: Perfect! Now I understand the deployment process. Based on the GitHub Actions workflow, here's how you should deploy to the staging cluster:

Deployment Process for Staging:
Option 1: Proper Git-based Deployment (Recommended)

Create a branch from main (not main itself, since manual triggers from main are blocked)

Commit your changes to the StatefulSet

Create a release targeting that branch (not main)

The GitHub Action will:
• Set environment variables for staging (deployment_env=stg, cpu=500, memory=2)
• Replace template variables in the YAML files
• Deploy to the debezium-server-stg cluster
• Restart the pod

Option 2: Manual Deployment (Quick Fix)

Since you want to test this now, let me help you deploy manually using the same process the GitHub Action uses.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5. After I shipped to staging, asking Amazon Q to verify
&lt;/h2&gt;

&lt;p&gt;Once the change was deployed to staging via CD, I asked Q to double check everything. He validated the probe configuration, pod health, and events to confirm that staging was stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: I applied it with CD. please confirm that the health checks look ok  

Amazon Q: Great! I see the pod restarted successfully on the staging cluster. The enhanced liveness probe is in place with:  
- Correct log path `/debezium/data/logs/debezium.log`  
- Detection of `ChangeStreamHistoryLost` errors  
- Auto-remediation with backup, offset removal, and restart trigger  
- Cleanup of old backups to prevent PVC bloat  

Debezium is streaming normally, no probe failures or unexpected restarts. The auto-remediation is active and will trigger within 10 seconds if the error reappears.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6. Asking Amazon Q to simulate the remediation on staging
&lt;/h2&gt;

&lt;p&gt;Instead of trusting the probe blindly, I asked Q to simulate the failure by injecting the ChangeStreamHistoryLost error into the logs. This way we could watch the remediation in action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: I want to simulate it. Please inject the error and let’s see that it works as expected  

Amazon Q: Great idea! I’ll inject a `ChangeStreamHistoryLost` error into the Debezium log and monitor the probe.  

→ Injected the error at 19:07:03Z  
→ Liveness probe detected it immediately and triggered remediation  
→ Pod restarted (restart count went from 0 → 1)  
→ Offset file removed, backup created (`debezium.log.backup-20250812T190703Z`)  
→ Debezium started cleanly: “No previous offsets found”  
→ Streaming resumed successfully  

🎉 **Auto-remediation test passed**: error detected, pod restarted within ~10s, offsets cleaned, and streaming resumed without manual intervention.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 7. After I shipped to production, asking Amazon Q to verify again
&lt;/h2&gt;

&lt;p&gt;With confidence from staging, I deployed to production. Q helped me validate the production pod with &lt;code&gt;kubectl describe&lt;/code&gt; and logs, confirming the probe was present and the pod was healthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8. Asking Amazon Q to summarize the process for the team
&lt;/h2&gt;

&lt;p&gt;Finally, I asked Q to produce a summary I could share internally as a knowledge base entry. The summary captured the purpose, steps, and indicators of success in a concise way. I asked it to output in Confluence format, so then I just copy-pasted it and did some tunings to match the team atmosphere.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is still in progress
&lt;/h2&gt;

&lt;p&gt;The long term solution is still under investigation. This issue touches MongoDB change stream history, Debezium offsets, and operational safeguards. The automated remediation already saved a large number of events and dramatically reduced business impact, while also saving many human hours during incidents.&lt;/p&gt;




&lt;h1&gt;
  
  
  Lessons learned
&lt;/h1&gt;

&lt;h2&gt;
  
  
  A small safeguard makes a big difference
&lt;/h2&gt;

&lt;p&gt;A small, targeted probe saved us a lot of data and hours. Incident response is a team sport, and AI can be a productive teammate when you use it intentionally. We kept the remediation simple and observable, tested it by simulating the real failure in staging, and delivered it through the standard CD path. That discipline gave us confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Work With AI in an Incident
&lt;/h2&gt;

&lt;p&gt;One important takeaway is that you cannot just tell an AI assistant ״solve the incident for me.״ You need to structure the thinking and let it walk with you step by step. For this incident, I first let Q investigate the cluster alone, even though I already knew what had happened. That gave him context and allowed his remediation suggestions to be sharper because he had ״felt״ the problem himself. Then I asked him for solutions, reviewed them, and deliberately chose the one that fit best. I pushed further by asking him to simulate the failure in staging before touching production, and I asked him to verify every deployment along the way. This orchestration of investigation, proposal, simulation, and verification turned the interaction into a holistic ecosystem. With the right thinking cues, AI goes the extra mile and becomes a true partner when dealing with production critical issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating Amazon Q as a Kubernetes CLI learning companion
&lt;/h2&gt;

&lt;p&gt;One thing that stood out to me in this incident is how natural it felt to use Amazon Q, or really any AI CLI tool, as an interface to Kubernetes. When you are dropped into a new cluster you are not familiar with, it can be hard to know what command to run or even how to authenticate. Q bridged that gap: it helped me install the GCP CLI, log in to the right project, and fetch kubeconfig for the correct GKE cluster. From there, it navigated the cluster to find the exact issue without me having to recall every &lt;code&gt;kubectl&lt;/code&gt; nuance.  &lt;/p&gt;

&lt;p&gt;A small but important detail is that Q always pauses before running a command. By default, you must approve each one, unless you explicitly type &lt;code&gt;t&lt;/code&gt; to grant temporary trust for the session. I personally do not give it blanket approval. There are two reasons: first, I want to learn the commands myself and understand the thinking process; second, I want to be sure nothing breaks. Even though I told Q upfront ״only read, don’t change anything,״ I treat this as advisory rather than guaranteed enforcement. Keeping explicit control allowed me to learn new Kubernetes command practices while staying safe.  &lt;/p&gt;

&lt;h2&gt;
  
  
  My perspective on relying on AI
&lt;/h2&gt;

&lt;p&gt;As long as complex systems still require human intervention for edge cases, and their developers do not make a clear claim that the system can self-heal under failure, we will continue to play an active role in the feedback loop with AI tools.  &lt;/p&gt;

&lt;p&gt;For example, Debezium’s integration with MongoDB Atlas CDC - the case I described - faced an unusual failure. It was likely caused by a third party, outside the Kubernetes cluster. But the key point is this: the way I guided the AI on how to assist me was essential. Without that structured interaction, the problem would not have been solved in practice.  &lt;/p&gt;

&lt;p&gt;I highly recommend watching &lt;a href="https://youtu.be/LCEmiRjPEtQ" rel="noopener noreferrer"&gt;this great talk&lt;/a&gt; - where Andrej Karpathy, a founding member of OpenAI, explains how software is changing (again) and how important is the "Human-in-the-Loop" factor.&lt;/p&gt;

&lt;p&gt;Until the next big leap arrives, which may not be far away, we must keep involving our own thinking in AI-driven processes, alternating between human judgment and AI automation.  &lt;/p&gt;

</description>
      <category>ai</category>
      <category>kubernetes</category>
      <category>data</category>
      <category>sre</category>
    </item>
    <item>
      <title>From Step Functions to Temporal on EKS: Durable Workflows at Scale Without Breaking the Bank</title>
      <dc:creator>Yedidya Schwartz</dc:creator>
      <pubDate>Thu, 03 Apr 2025 19:11:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/from-step-functions-to-temporal-on-eks-durable-workflows-at-scale-without-breaking-the-bank-3cdf</link>
      <guid>https://dev.to/aws-builders/from-step-functions-to-temporal-on-eks-durable-workflows-at-scale-without-breaking-the-bank-3cdf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Lesson learned: Always tie your architecture decisions to your business needs. We started by chasing stability — and ended up chasing cost, once scale kicked in. Don’t be afraid to shift your mindset from using external managed services to running self-hosted infrastructure. At a certain scale, this shift is critical to keep costs manageable and sustainable.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We Needed
&lt;/h2&gt;

&lt;p&gt;Trigger a &lt;strong&gt;timer&lt;/strong&gt;,&lt;br&gt;&lt;br&gt;
Wait &lt;strong&gt;X seconds&lt;/strong&gt;,&lt;br&gt;&lt;br&gt;
Then &lt;strong&gt;run a function with a specific payload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Seems simple—until you're doing it millions of times per day, across two cloud providers, with retry logic, second-level delay precision, and full observability.&lt;/p&gt;


&lt;h2&gt;
  
  
  Our Journey: Azure Durable Functions → AWS Step Functions → Temporal Cloud → Temporal Self-Hosted
&lt;/h2&gt;

&lt;p&gt;We've been through all the usual options:&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ Azure Durable Functions
&lt;/h3&gt;

&lt;p&gt;Worked—until traffic spiked. Then: dropped messages, hung workflows, and zero transparency. Not built for our scale.&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ AWS Step Functions
&lt;/h3&gt;

&lt;p&gt;Solid and stable. We liked it. But the more we used it, the more it cost us. Delay-heavy flows with retries added up fast. It wasn't sustainable.&lt;/p&gt;
&lt;h3&gt;
  
  
  ❌ AWS Step Functions Express
&lt;/h3&gt;

&lt;p&gt;Total execution time is capped at &lt;strong&gt;5 minutes&lt;/strong&gt;. Our flows often wait far longer than that. Not an option.&lt;/p&gt;
&lt;h3&gt;
  
  
  ❌ EventBridge
&lt;/h3&gt;

&lt;p&gt;We considered using EventBridge for scheduled triggers. But the &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt; admit to ~1-minute precision. We needed 1–2 second precision.&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ Temporal Cloud
&lt;/h3&gt;

&lt;p&gt;We did a full POC. It worked beautifully. But the pricing model, based on &lt;strong&gt;actions&lt;/strong&gt;, didn’t fit our use case. At ~200M workflows/month (each including delay, retries, and an activity), which equates to 400M actions/month in our case, we were looking at an extremely high bill.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Cost-Saving Move: Self-Hosted Temporal on EKS
&lt;/h2&gt;

&lt;p&gt;After a long period of embracing managed services wherever possible, we realized that, in this case, going self-hosted was the only way to support the business at scale without letting cost become a blocker. So we deployed Temporal on our own infrastructure (EKS) using the official &lt;a href="https://github.com/temporalio/helm-charts" rel="noopener noreferrer"&gt;Helm charts&lt;/a&gt;. With some tuning, it worked smoothly.&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ ~80% cost savings
&lt;/h3&gt;
&lt;h3&gt;
  
  
  ✅ Full infrastructure and application-level control
&lt;/h3&gt;
&lt;h3&gt;
  
  
  ✅ Scale without pricing surprises
&lt;/h3&gt;

&lt;p&gt;Even better: our cost doesn't scale with traffic.&lt;br&gt;&lt;br&gt;
We configured EKS node groups and Aurora Postgres to support multiple times our current load, all for around &lt;strong&gt;$1,500/month&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Most of our infra is either autoscaled or manually tuned, and our cost curve is flat.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step Functions Implementation: The Starting Point
&lt;/h2&gt;

&lt;p&gt;Before diving into Temporal, here’s what our implementation looked like using &lt;strong&gt;AWS Step Functions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The purpose of sharing this is to show a side-by-side with our Temporal setup later—so you can clearly see the evolution and differences in approach.&lt;/p&gt;

&lt;p&gt;In Step Functions, you must first &lt;strong&gt;define your State Machine ahead of time&lt;/strong&gt;, including all steps, transitions, and timers. This pre-declaration requirement introduces a different kind of workflow lifecycle—configuration first, execution second.&lt;/p&gt;

&lt;p&gt;Here’s how we structured the creation of a timer-based workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Workflow&lt;/span&gt; &lt;span class="n"&gt;CreateTimerWorkflow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;waitTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;httpBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Workflow&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_timerStateMachineArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TimerRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpCallRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;WaitTimeSeconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;waitTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpCallRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MicroserviceRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RequestMetadata&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;RequestId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;RequestContent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpBody&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;Url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;AppCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetServiceAuthCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To trigger this workflow, we used a runner with built-in retry logic and AWS SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartExecutionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;StartExecutionRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;StateMachineArn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateMachineArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt; &lt;span class="c1"&gt;// I used requestId as execution name to make it easy to debug later&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s what the &lt;strong&gt;State Machine definition&lt;/strong&gt; looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A description of my state machine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="nl"&gt;"Wait"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"InputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"SecondsPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.waitTimeSeconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Call third-party API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.data"&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 class="nl"&gt;"Call third-party API"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::http:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="nl"&gt;"Method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"RequestBody.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authentication"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="nl"&gt;"ConnectionArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONNECTION_ARN_PLACEHOLDER"&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 class="nl"&gt;"ApiEndpoint.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"QueryParameters.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.appCode"&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 class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&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 class="s2"&gt;"States.ALL"&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 class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"JitterStrategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Main Retrier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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 class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"InputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&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 class="p"&gt;}&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Temporal is Different
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Temporal&lt;/strong&gt;, workflows are implemented entirely in code using the SDK. You don’t need to predefine a workflow structure or declare a state machine. The only setup required is creating a &lt;strong&gt;namespace&lt;/strong&gt;—everything else happens dynamically at runtime.&lt;/p&gt;

&lt;p&gt;This model shifts the ownership of orchestration from infrastructure (like JSON/YAML flow definitions) to application code. It provides flexibility for teams that want to iterate on workflow logic through code deployments and enables versioning, type safety, and testing in the developer's native language.&lt;/p&gt;

&lt;p&gt;Both models — predefined state machines in Step Functions and code-first workflows in Temporal — represent different philosophies in how orchestration can be managed.&lt;/p&gt;

&lt;p&gt;Temporal lets you write workflows as regular code — with built-in durability, retries, and state recovery.&lt;/p&gt;

&lt;p&gt;Our use case was a perfect fit, just like it was with Step Functions: wait, then trigger logic. But instead of using the standard &lt;code&gt;Workflow.Sleep()&lt;/code&gt; pattern, we took it a step further.&lt;/p&gt;




&lt;h2&gt;
  
  
  Saving on Actions: Using &lt;code&gt;StartDelay&lt;/code&gt; Instead of a Timer
&lt;/h2&gt;

&lt;p&gt;The typical Temporal pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that &lt;code&gt;Sleep&lt;/code&gt; adds an extra action. In Temporal Cloud, that would cost more. During our POC with Temporal Cloud, we realized this optimization could significantly reduce cost.&lt;/p&gt;

&lt;p&gt;So instead, we used &lt;code&gt;StartDelay&lt;/code&gt; when starting the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartWorkflowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TemporalCallApiWorkflow&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timerRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WorkflowOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;taskQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTemporalWorkflowOptions&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;TaskQueue&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;StartDelay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timerRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitTimeSeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapped with exponential backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;CommonUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RetryCallWithExponentialBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartWorkflowAsync&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="s"&gt;"TemporalWorkflowExecution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExecutionRetries&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern reduces actions per workflow from 3 to 2, saving ~33% per execution in the cloud model, and even better—on our self-hosted setup, it reduces the performance overhead by 33% per workflow.&lt;/p&gt;

&lt;p&gt;We configure the system with a unified structure that makes environment switching easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TargetHost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temporal-frontend.temporal.svc.cluster.local:7233"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"WebhookBaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://my-app.test.svc.cluster.local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TaskQueue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-api-caller-task-queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsShadowMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Our Infra Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt;: EKS
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Aurora PostgreSQL (persistence + visibility: the two required Temporal DBs)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingress&lt;/strong&gt;: Internal-only access inside AWS, plus an NLB to allow external access to Temporal for triggering workflows.
&lt;a href="https://community.temporal.io/t/exposing-grpc-frontend-via-alb/4994/5" rel="noopener noreferrer"&gt;Exposing the frontend service via ALB didn’t work&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI&lt;/strong&gt;: VPN-only access
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: Datadog dashboard using &lt;a href="https://docs.datadoghq.com/integrations/temporal/" rel="noopener noreferrer"&gt;the official integration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config&lt;/strong&gt;: Git-managed &lt;code&gt;values.yaml&lt;/code&gt; via ArgoCD&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Stress Testing: What Broke, What We Fixed
&lt;/h2&gt;

&lt;p&gt;We used &lt;a href="https://github.com/temporalio/maru" rel="noopener noreferrer"&gt;Maru&lt;/a&gt; to simulate production load, and the &lt;a href="https://temporal.io/blog/scaling-temporal-the-basics" rel="noopener noreferrer"&gt;Scaling Temporal&lt;/a&gt; doc to interpret the results via our Datadog dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  429 "Too Many Requests"
&lt;/h3&gt;

&lt;p&gt;We hit the &lt;code&gt;frontend.namespaceCount&lt;/code&gt; limit early.&lt;br&gt;&lt;br&gt;
→ Fixed using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dynamicConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;frontend.namespaceCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999999&lt;/span&gt;
      &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pod Resource Starvation
&lt;/h3&gt;

&lt;p&gt;Frontend, history, and worker pods couldn’t keep up.&lt;br&gt;&lt;br&gt;
→ Tuned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3Gi&lt;/span&gt;
    &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aurora Lagging
&lt;/h3&gt;

&lt;p&gt;→ Upgraded the instance class. CPU usage remains high under stress, but it’s no longer a bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  History Shards
&lt;/h3&gt;

&lt;p&gt;We're still on the default &lt;code&gt;history.shards&lt;/code&gt; value. Planning to scale it later—even though it requires a DB reset. Since we're still in shadow mode, as I'll explain in the next paragraph, that's acceptable for now.&lt;/p&gt;

&lt;p&gt;Tuning the number of shards is a critical scaling lever in Temporal.&lt;br&gt;&lt;br&gt;
Highly recommend reading: &lt;a href="https://mikhail.io/2021/05/choose-the-number-of-shards-in-temporal-history-service/" rel="noopener noreferrer"&gt;Choose the Number of Shards in Temporal History Service&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During stress testing, we saw shard latency spikes, signaling a need to increase shard count. We're waiting to validate this under real traffic in shadow mode before committing to a reset.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz915dwmnncsfu4pysseq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz915dwmnncsfu4pysseq.png" alt="History shards spike" width="800" height="828"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Strategy: Shadow Mode
&lt;/h2&gt;

&lt;p&gt;We’re not cutting over cold turkey. Instead, we’re shadowing Step Functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Temporal runs the full logic
&lt;/li&gt;
&lt;li&gt;Sends results to a mock endpoint, including the expected trigger timestamp
&lt;/li&gt;
&lt;li&gt;If the delay &amp;gt; 2 seconds, we log it and investigate
&lt;/li&gt;
&lt;li&gt;We improve iteratively and expand the experiment
&lt;/li&gt;
&lt;li&gt;Once confidence is high, we flip the traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero risk. High signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Easy Rollback: Step Functions as a Safety Net
&lt;/h2&gt;

&lt;p&gt;What made the decision easier was knowing we had Step Functions as a fallback.&lt;br&gt;&lt;br&gt;
They were already in place, already battle-tested. So we had no fear testing Temporal in production scenarios—because rollback was simple and safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment Flow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All changes are GitOps-managed (separate &lt;code&gt;gitops&lt;/code&gt; and &lt;code&gt;gitops-production&lt;/code&gt; repos)
&lt;/li&gt;
&lt;li&gt;ArgoCD watches &lt;code&gt;values.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Manual sync for now — we like the control&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Start with stability. Move to cost once you scale.&lt;/p&gt;

&lt;p&gt;Once you decide to go self-hosted, don't fear it—but do it right.&lt;br&gt;&lt;br&gt;
Take time to deeply understand the product’s architecture. Run stress tests. Iterate.&lt;br&gt;&lt;br&gt;
Be ready with a fallback and observability before fully cutting over.&lt;/p&gt;

&lt;p&gt;This approach gave us both confidence and clarity at each step.&lt;/p&gt;




&lt;p&gt;This is an ongoing story — happy to share more details or dive deeper into any part of the article. I'll keep you posted about next steps and some more challenges I had during the process.&lt;br&gt;&lt;br&gt;
Feel free to reach out!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>temporal</category>
      <category>workflows</category>
    </item>
  </channel>
</rss>
