<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Robin's Tech Blog: Exploring Software Engineering, Cloud Technologies, and Open-Source Innovations]]></title><description><![CDATA[Explore reports on software development, cloud technologies, and open-source innovations. Insights from Robin Brämer, a passionate technologist and full-stack developer.]]></description><link>https://robin.cnap.tech</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1741769515940/dbe1108b-2667-4591-80c1-59e55c810073.png</url><title>Robin&apos;s Tech Blog: Exploring Software Engineering, Cloud Technologies, and Open-Source Innovations</title><link>https://robin.cnap.tech</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 25 Apr 2026 06:38:28 GMT</lastBuildDate><atom:link href="https://robin.cnap.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Unlocking Success: Launching CNAP.tech with 95 Developers and Reaching 800K Views]]></title><description><![CDATA[A few years ago, I found myself knee‑deep in YAML, endlessly wrestling with cloud credentials and surprise bills. I realized that if developers could click once and get a fully isolated, production‑grade environment—just like spinning up a sandbox—th...]]></description><link>https://robin.cnap.tech/launching-cnap-mvp</link><guid isPermaLink="true">https://robin.cnap.tech/launching-cnap-mvp</guid><category><![CDATA[mvp]]></category><category><![CDATA[Product Management]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[developer experience]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[stripe]]></category><category><![CDATA[Stripe Integration]]></category><category><![CDATA[helm chart]]></category><category><![CDATA[Monetization]]></category><category><![CDATA[Startups]]></category><category><![CDATA[devex]]></category><category><![CDATA[How to Build a Minimal Viable Product ]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[cloud native]]></category><dc:creator><![CDATA[Robin Brämer]]></dc:creator><pubDate>Sat, 19 Apr 2025 08:11:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745248929930/22f989a9-affc-4fff-be5e-d7f19b38ae79.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few years ago, I found myself knee‑deep in YAML, endlessly wrestling with cloud credentials and surprise bills. I realized that if developers could click once and get a fully isolated, production‑grade environment—just like spinning up a sandbox—they’d spend far more time building and less time configuring. That spark led to my latest venture: <a target="_blank" href="https://docs.cnap.tech/success/build-million-dollar-products"><strong>CNAP</strong></a>, the Cloud‑Native Application Platform.</p>
<hr />
<h2 id="heading-finding-our-tribe-with-datadriven-insights">Finding Our Tribe with Data‑Driven Insights</h2>
<p>Before writing a single line of CNAP code, I set out to meet the people who’d actually use it. Enter the <a target="_blank" href="https://hetzner-value-auctions.cnap.tech/"><strong>Hetzner Value Auctions Browser</strong></a>—a simple UI that scrapes Hetzner’s Serverbörse, layers on real CPU benchmarks, and ranks machines by true cost‑performance. When I <a target="_blank" href="https://www.reddit.com/r/selfhosted/comments/1hszpeu/advanced_server_auctions_browser_for_hetzner/">shared</a> it on r/selfhosted and related subs, it blew up:</p>
<ul>
<li><p><strong>800 000+ views</strong> across Reddit posts</p>
</li>
<li><p><strong>Hundreds</strong> of comments on Reddit and our Discord with in‑depth feedback</p>
</li>
<li><p><strong>95 developers</strong> joining our Discord and waitlist</p>
</li>
</ul>
<p>That tool did more than prove demand. It gave us an audience already primed for self‑service infrastructure—and it taught me how to listen.</p>
<hr />
<h2 id="heading-today-cnap-in-private-pilot">Today: CNAP in Private Pilot</h2>
<p>With our early community rallied, CNAP has entered a closed pilot. We’re inviting <strong>20 developers</strong>—platform owners, indie SaaS builders, and DevOps‑stretched startups—to experience:</p>
<ul>
<li><p><strong>Instant, per‑customer Kubernetes clusters</strong> (powered by k3s) with zero YAML or infra setup</p>
</li>
<li><p><strong>Built‑in usage tracking &amp; billing hooks</strong>, so you can meter your own software consumption</p>
</li>
<li><p><strong>One‑click app installs</strong> via Helm packages—choose from 50 000+ charts or bring your own</p>
</li>
<li><p><strong>Real‑time feedback loops</strong> in Discord and short in‑flow surveys</p>
</li>
</ul>
<p>Even at MVP stage, pilots can see CNAP’s vision in action—and shape it directly.</p>
<hr />
<h2 id="heading-whats-coming-next">What’s Coming Next</h2>
<p>We’re moving fast on features driven by our early adopters’ wish lists:</p>
<ol>
<li><p><strong>CNAP‑CLI</strong></p>
</li>
<li><p><strong>Managed Clusters, but BYOC</strong></p>
</li>
</ol>
<p>You’ll catch previews of each feature in our living roadmap on <a target="_blank" href="https://docs.cnap.tech/">docs.cnap.tech</a>—and pilots get first access.</p>
<hr />
<h2 id="heading-partnership-spotlight-hetzner-cloud-credits">Partnership Spotlight: Hetzner Cloud Credits</h2>
<p>Building CNAP alongside Hetzner means you don’t just get theory—you get cloud credits, too. Every pilot provisioning clusters on Hetzner via CNAP receives free credits that apply automatically. It’s our way of ensuring you can experiment at no cost, and a big vote of confidence from Hetzner’s team.</p>
<hr />
<h2 id="heading-why-you-should-join-our-pilot">Why You Should Join Our Pilot</h2>
<p>CNAP isn’t another managed K8s host. It’s a developer‑first, self‑hosted platform that:</p>
<ul>
<li><p><strong>Automates per‑customer isolation</strong>—perfect for SaaS, regulated industries, or simply keeping noisy neighbors at bay</p>
</li>
<li><p><strong>Bundles billing, metrics, and deployments</strong> into one interface</p>
</li>
<li><p><strong>Scales with you</strong>, from indie side project to full production SaaS</p>
</li>
</ul>
<p>As a pilot, you’ll receive:</p>
<ul>
<li><p><strong>Direct influence</strong> on our features and UX</p>
</li>
<li><p><strong>Bi‑weekly office‑hour calls</strong> with our engineering team</p>
</li>
<li><p><strong>Preferential pilot pricing</strong> and partnership perks</p>
</li>
</ul>
<hr />
<h2 id="heading-roadmap-at-a-glance">Roadmap at a Glance</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>When</td><td>Milestone</td></tr>
</thead>
<tbody>
<tr>
<td><strong>May – Jun ’25</strong></td><td>Onboard first 20 pilots · CNAP‑CLI v0.2 beta · Pilot feedback webinar</td></tr>
<tr>
<td><strong>Jun – Jul ’25</strong></td><td>Deliver autoscaling, backup/restore · Launch referral program</td></tr>
<tr>
<td><strong>Q3 ’25</strong></td><td>Public beta launch · Multi‑cloud support (AWS/GCP) · Web dashboard MVP</td></tr>
</tbody>
</table>
</div><hr />
<p>Building CNAP has been a journey from “What if we could…” to “Here’s your platform, now let us know what you need.” If you’re a developer, platform operator, or technical founder ready to offload infrastructure complexity and focus on your product, <strong>I’d love to have you aboard</strong>.</p>
<p>➡️ <a target="_blank" href="https://discord.gg/B2zuJ4kCmf"><strong>Apply for our private pilot</strong></a><br />💬 <a target="_blank" href="https://discord.gg/B2zuJ4kCmf"><strong>Join the conversation on Discord</strong></a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745248915200/0c4d9fc6-7932-4994-9cb5-a0dabbf19152.png" alt class="image--center mx-auto" /></p>
<p>Let’s make creating hosted digital products delightful—together.</p>
]]></content:encoded></item><item><title><![CDATA[Guide: Using Zitadel SAML as Rancher IdP via Keycloak SAML Auth Provider]]></title><description><![CDATA[Rancher now support Generic OIDC. Only continue reading this article if you like to see how I tricked Rancher.
https://github.com/rancher/rancher-docs/pull/1392
 

As an admin I want to allow users to SSO to Rancher via Zitadel as the central identit...]]></description><link>https://robin.cnap.tech/guide-using-zitadel-saml-as-rancher-idp-via-keycloak-saml-auth-provider</link><guid isPermaLink="true">https://robin.cnap.tech/guide-using-zitadel-saml-as-rancher-idp-via-keycloak-saml-auth-provider</guid><dc:creator><![CDATA[Robin Brämer]]></dc:creator><pubDate>Fri, 28 Mar 2025 16:41:18 GMT</pubDate><content:encoded><![CDATA[<p>Rancher now support Generic OIDC. Only continue reading this article if you like to see how I tricked Rancher.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/rancher/rancher-docs/pull/1392">https://github.com/rancher/rancher-docs/pull/1392</a></div>
<p> </p>
<hr />
<p>As an admin I want to allow users to SSO to Rancher via <a target="_blank" href="https://github.com/zitadel/zitadel">Zitadel</a> as the central identity auth provider.</p>
<p>Since Rancher has no Zitadel or generic OIDC or SAML auth provider, our trick is to use Keycloak SAML, but connect it to Zitadel.</p>
<hr />
<h3 id="heading-create-saml-app-in-zitadel">Create SAML App in Zitadel</h3>
<ol>
<li><p>Download Rancher's SAML Metadata XML from <a target="_blank" href="https://RANCHERHOST/v1-saml/keycloak/saml/metadata"><code>https://RANCHERHOST/v1-saml/keycloak/saml/metadata</code></a></p>
</li>
<li><p>Load it into the Zitadel SAML App as XML file. (may need to add .xml to file)</p>
</li>
</ol>
<p><img src="https://gist.github.com/user-attachments/assets/fa62130b-5567-451b-8f09-e6b9a64821d6" alt="Rancher Metadata XML" /></p>
<hr />
<h3 id="heading-configuring-keycloak-saml-auth-provider-in-rancher">Configuring Keycloak SAML Auth Provider in Rancher</h3>
<ol>
<li><p>Create Keycloak SAML auth provider in Rancher and enter Zitadel's field names as in the screenshot below.</p>
</li>
<li><p>Download Zitadel SAML Metadata from <a target="_blank" href="https://ZITADELHOST/saml/v2/metadata"><code>https://ZITADELHOST/saml/v2/metadata</code></a></p>
</li>
<li><p>Load that into Rancher Metadata XML field</p>
</li>
<li><p>Download the Certificate from Zitadel from <a target="_blank" href="http://ZITADELHOST/saml/v2/certificate"><code>http://ZITADELHOST/saml/v2/certificate</code></a></p>
</li>
<li><p>Load that into the Rancher Certificate field</p>
</li>
<li><p>For the Private Key, follow the <a target="_blank" href="https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/authentication-permissions-and-global-configuration/authentication-config/configure-keycloak-saml">Rancher Docs</a>, you can simply generate a key by running:</p>
</li>
</ol>
<pre><code class="lang-shell">openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout myservice.key -out myservice.cert
</code></pre>
<p><img src="https://gist.github.com/user-attachments/assets/5bd0692a-6a3f-4159-b753-9d0a4d09ee93" alt="Generate Private Key" /></p>
<ol start="7">
<li>Load the <code>myservice.key</code> into the Rancher's Private Key field.</li>
</ol>
<p>It should look like this in the end:</p>
<ul>
<li><p>Note that UID Field I wanted to use Zitadel's <code>UserID</code>, but it could be <code>Email</code> as well.</p>
</li>
<li><p>Note there is no Groups field in Zitadel's SAMLResponse but it works nevertheless. If you need to add the user to Rancher groups see notes below.</p>
</li>
</ul>
<p><img src="https://gist.github.com/user-attachments/assets/33daf89a-6971-419b-ab92-59a03c6818a8" alt="Rancher Configuration" /></p>
<ol start="8">
<li>Click enable in Rancher, and we are done. We see the auth provider is active:</li>
</ol>
<p><img src="https://gist.github.com/user-attachments/assets/5116d37b-e015-45da-8577-bb46abb6e2b3" alt="Auth Provider Active" /></p>
<ol start="9">
<li>Logout, and we should see that we can now also login via our Keycloak provider, which is actually redirecting to our Zitadel SAML app:</li>
</ol>
<p><img src="https://gist.github.com/user-attachments/assets/b3e42e87-fab2-460d-949f-a9eca502cab5" alt="Login via Keycloak" /></p>
<ol start="10">
<li>We see our Zitadel user now in the top right corner of our Rancher profile:</li>
</ol>
<p><img src="https://gist.github.com/user-attachments/assets/e790657a-e6f3-4603-8d35-d098bad7462d" alt="Zitadel User in Rancher" /></p>
<ol start="11">
<li>Rancher logs will update the rancher user resource's <code>principalIds</code> to include <code>keycloak_user://265306122980819188</code>, where the id is the same as the Zitadel user id:</li>
</ol>
<p><img src="https://gist.github.com/user-attachments/assets/06411343-fd75-47f5-a8f0-710518dc3d97" alt="Rancher User Resource" /></p>
<p><img src="https://gist.github.com/user-attachments/assets/0167e212-5c62-45fa-95e9-835ccf8e7649" alt="Principal IDs" /></p>
<p><img src="https://gist.github.com/user-attachments/assets/01ae81e5-bcf5-4324-8bcd-1e4b03708ebf" alt="Rancher User" /></p>
<p><strong>That’s it! We are done!</strong></p>
<hr />
<h1 id="heading-notes-about-saml-groups">Notes about SAML Groups</h1>
<blockquote>
<p>Warning: In my testing it didn't work yet, I need to dig deeper into how Rancher groups work in their docs.</p>
</blockquote>
<p>There is no Groups field in Zitadel's SAMLResponse but it works nevertheless. If you need to add the user to Rancher groups you can create an action to add it.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setCustomAttribute</span>(<span class="hljs-params">ctx, api</span>)</span>{
    api.v1.attributes.setCustomAttribute(<span class="hljs-string">'Groups'</span>, <span class="hljs-string">''</span>, <span class="hljs-string">'settings-manage'</span>)
}
</code></pre>
<p><img src="https://gist.github.com/user-attachments/assets/91d3931e-aa7b-4103-a00e-6e28fe37dd29" alt="image" /></p>
<h2 id="heading-creating-a-new-user">Creating a New User</h2>
<p>Any user that can register and login via Zitadel is able to login to Rancher by default. Zitadel Rancher SAML App can be restricted to only certain Zitadel Users that have project/app access.</p>
<p>Rancher log when signin into as a new Zitadel user first time:</p>
<pre><code class="lang-shell">2024/05/02 10:26:23 [INFO] Creating user for principal keycloak_user://265329911630135303
2024/05/02 10:26:23 [INFO] Creating globalRoleBindings for u-r3tnxufjdy
2024/05/02 10:26:23 [INFO] Creating new GlobalRoleBinding for GlobalRoleBinding grb-qfhdn
2024/05/02 10:26:23 [INFO] [mgmt-auth-grb-controller] Creating clusterRoleBinding for globalRoleBinding grb-qfhdn for user u-r3tnxufjdy with role cattle-globalrole-user
</code></pre>
<hr />
<h3 id="heading-community-links">Community links:</h3>
<ul>
<li><p><a target="_blank" href="https://discord.com/channels/927474939156643850/927866013545025566/1235495550464430091">Me in Zitadel Discord</a></p>
</li>
<li><p><a target="_blank" href="https://rancher-users.slack.com/archives/C3ASABBD1/p1714637758597989">Me in Rancher Slack</a></p>
</li>
</ul>
<h3 id="heading-resources">Resources:</h3>
<ul>
<li><p><a target="_blank" href="https://gist.github.com/PhilipSchmid/506b33cd74ddef4064d30fba50635c5b">GitHub Gist</a></p>
</li>
<li><p><a target="_blank" href="https://rancher.github.io/dashboard/guide/auth-providers#developer-set-up-saml">Rancher Dashboard Auth Providers Guide</a></p>
</li>
<li><p><a target="_blank" href="https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/authentication-permissions-and-global-configuration/authentication-config/configure-keycloak-saml">Rancher Docs - Configure Keycloak SAML</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Handling Errors and Cleanup in Temporal Workflows (TypeScript)]]></title><description><![CDATA[Developing robust Temporal workflows involves anticipating failures and ensuring that any necessary cleanup or compensating actions occur regardless of how a workflow or activity fails. This includes handling activity exceptions, workflow errors, tim...]]></description><link>https://robin.cnap.tech/handling-errors-and-cleanup-in-temporal-workflows-typescript</link><guid isPermaLink="true">https://robin.cnap.tech/handling-errors-and-cleanup-in-temporal-workflows-typescript</guid><dc:creator><![CDATA[Robin Brämer]]></dc:creator><pubDate>Mon, 10 Mar 2025 16:37:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741772129278/094aadd9-06f0-4aea-8ae9-4b7f59252456.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Developing robust Temporal workflows involves anticipating failures and ensuring that any necessary cleanup or compensating actions occur regardless of how a workflow or activity fails. This includes handling activity exceptions, workflow errors, timeouts, and cancellations. Below, we outline best practices for error handling in Temporal (TypeScript), including the Saga pattern for compensating transactions, use of retry policies, and how to trigger cleanup activities on failure.</p>
<h2 id="heading-common-failure-scenarios-in-temporal">Common Failure Scenarios in Temporal</h2>
<p>Temporal workflows can fail in several ways, each requiring proper handling:</p>
<ul>
<li><p><strong>Activity Failures:</strong> If an Activity (the code executed outside the Workflow) throws an exception or times out, the Workflow will receive an <code>ActivityFailure</code> exception. The original error is wrapped inside (accessible via <code>error.cause</code> in the Workflow) (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=,nonRetryableApplicationFailure">Temporal Error Handling In Practice</a>) (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=Errors%20thrown%20in%20activities%20are%3A">Temporal Error Handling In Practice</a>). By default, Temporal will <strong>retry</strong> failed activities based on a retry policy, unless the error is marked as non-retryable (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=must%20account%20for%20this">Temporal Error Handling In Practice</a>). After final retry exhaustion (or if non-retryable), the Workflow must handle the failure (e.g., via a try/catch).</p>
</li>
<li><p><strong>Workflow Failures:</strong> If the Workflow code itself throws an uncaught exception, the Workflow will fail. You should catch exceptions in the Workflow to perform cleanup or compensation logic instead of letting the Workflow fail silently. Any error not caught will terminate the Workflow run.</p>
</li>
<li><p><strong>Cancellation:</strong> A Workflow cancellation (e.g., via <code>handle.cancel()</code>) causes a <code>CancelledFailure</code> to be thrown inside the Workflow and any running activities (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=if%20so%2C%20in%20your%20workflow,and%20run%20the%20cleanup%20activity">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>). Activities detect cancellation through heartbeat mechanisms (Temporal delivers cancellation on the next heartbeat) (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=Cancellation%20is%20delivered%20to%20your,heartbeat%2C%20do%20your%20activities%20heartbeat">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>). Without special handling, cancellation would stop the Workflow immediately, skipping any following steps.</p>
</li>
<li><p><strong>Timeouts:</strong> Temporal supports activity timeouts (start-to-close, etc.) and Workflow execution timeouts. An activity timeout is treated as an activity failure (throwing an exception that can be caught in the Workflow). <strong>However, a Workflow Execution Timeout is essentially a hard terminate of the Workflow</strong> – it will stop execution without giving the Workflow a chance to clean up (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=No,timers%20inside%20the%20workflow%20code">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>). For business logic deadlines, it's recommended to <strong>avoid using hard Workflow timeouts or terminate</strong> and instead use timers or cancellation logic within the Workflow to handle time-based limits (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=The%20recommended%20way%20is%20not,the%20terminate%20on%20a%20timer">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>) (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=No,timers%20inside%20the%20workflow%20code">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>). This way, the Workflow can catch a timeout condition (as a cancellation or error) and perform cleanup.</p>
</li>
</ul>
<p>Understanding these scenarios allows us to design workflows that catch failures and ensure a cleanup Activity or compensating transaction runs in all cases.</p>
<h2 id="heading-compensating-transactions-and-the-saga-pattern">Compensating Transactions and the Saga Pattern</h2>
<p>For workflows that perform multiple steps (especially across external systems) and need <strong>all-or-nothing</strong> semantics, use the <strong>Saga pattern</strong> (compensating transactions). In Temporal, you implement Sagas by pairing each forward operation with a corresponding <em>compensation</em> operation that can undo it (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=The%20compensating%20action%20pattern%20provides,running%20transaction%2C%20another%20service%20or">Saga Compensating Transactions | Temporal</a>) (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=In%20summary%2C%20compensating%20actions%20,If%20you%20have">Saga Compensating Transactions | Temporal</a>). If any step fails, previously completed steps are rolled back by invoking their compensations in reverse order, leaving the overall system in a consistent state (as if the workflow's side effects never happened).</p>
<p>Temporal's TypeScript SDK does not have a built-in Saga helper (unlike the Java SDK’s <code>Saga</code> class), but it's straightforward to implement:</p>
<ol>
<li><p><strong>Perform each activity and record its compensation:</strong> After each successful activity, record a compensation function (e.g., an Activity that reverses that step) in a list. For example, if you create a record in one activity, record a compensating activity to delete that record.</p>
</li>
<li><p><strong>On failure, run compensations in reverse order:</strong> In the Workflow’s <code>catch</code> block, iterate through the recorded compensation functions and call them (typically in reverse order of the original operations). Each compensation should be designed to safely handle the case where the original operation might not have fully completed (idempotent or conditional undo logic).</p>
</li>
<li><p><strong>Optionally, handle compensation failures:</strong> If a compensation action itself fails, log or handle it appropriately (Temporal will by default retry activities, so a failed compensation activity can be retried as well). The workflow should still attempt all compensations even if one of them fails.</p>
</li>
</ol>
<p><strong>TypeScript Workflow example using Saga pattern:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CancellationScope } <span class="hljs-keyword">from</span> <span class="hljs-string">'@temporalio/workflow'</span>;
<span class="hljs-keyword">type</span> Compensation = <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">OrderWorkflow</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  <span class="hljs-keyword">const</span> compensations: Compensation[] = [];
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Step 1: Perform operation and record its compensation</span>
    <span class="hljs-keyword">await</span> createOrder(); 
    compensations.unshift(<span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-keyword">await</span> deleteOrder(); });  <span class="hljs-comment">// compensation for step 1</span>

    <span class="hljs-comment">// Step 2: Perform next operation</span>
    <span class="hljs-keyword">await</span> reserveInventory();
    compensations.unshift(<span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-keyword">await</span> releaseInventory(); }); <span class="hljs-comment">// compensation for step 2</span>

    <span class="hljs-comment">// Step 3: Perform another operation</span>
    <span class="hljs-keyword">await</span> chargePayment();
    compensations.unshift(<span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-keyword">await</span> refundPayment(); });    <span class="hljs-comment">// compensation for step 3</span>

    <span class="hljs-comment">// ... more steps as needed ...</span>

  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-comment">// If any step fails, execute compensating actions for completed steps</span>
    <span class="hljs-keyword">await</span> CancellationScope.nonCancellable(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> compensate <span class="hljs-keyword">of</span> compensations) {
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">await</span> compensate();
        } <span class="hljs-keyword">catch</span> (compErr) {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Compensation failed:"</span>, compErr);
          <span class="hljs-comment">// continue to next compensation even if one fails</span>
        }
      }
    });
    <span class="hljs-keyword">throw</span> err;  <span class="hljs-comment">// rethrow to mark workflow as failed after compensation</span>
  }
}
</code></pre>
<p>In the above example, each successful step pushes a compensating function onto a stack. If a failure occurs, we execute all collected compensations. We wrap the compensation loop in a <strong>non-cancellable scope</strong> to ensure it runs to completion even if the workflow was cancelled (more on this below). This pattern ensures that resources created or actions taken in earlier steps are undone when a later step fails (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>) (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=await%20addMilk%28%29%20,compensations%2C%20compensateInParallel%29%20throw%20err">Saga Compensating Transactions | Temporal</a>). Temporal’s documentation provides a similar example of collecting compensation callbacks in TypeScript (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>).</p>
<p><strong>Best practices for Saga compensations:</strong></p>
<ul>
<li><p><strong>Order and Idempotency:</strong> Invoke compensations in the reverse order of the original actions (LIFO order) since the latest action should be undone first. Make each compensation action idempotent or safe to run even if the original step partially failed or was never performed. For example, a “delete resource” compensation should succeed (or do nothing) even if the resource didn’t exist, as shown by functions like <code>putBowlAwayIfPresent</code> in Temporal's saga example (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>).</p>
</li>
<li><p><strong>Marking Non-Retryable Errors:</strong> If a failure at a certain step is not transient (e.g., a business logic validation), consider throwing it as a non-retryable error (using <code>ApplicationFailure.nonRetryable</code>) from the activity (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=must%20account%20for%20this">Temporal Error Handling In Practice</a>). This prevents Temporal from retrying the activity endlessly and instead fails fast, triggering the compensation logic.</p>
</li>
<li><p><strong>Handling Compensation Failures:</strong> Design compensating activities with their own retry policies – you generally want to retry them on failure as well (since cleanup is crucial). Even if a compensation ultimately fails, the Workflow should catch that error (as in the example above) and continue attempting the remaining compensations. Log these failures for visibility. The Workflow can still be marked failed after compensation, or you might choose to swallow the original error if you consider the saga completion a “graceful” outcome.</p>
</li>
<li><p><strong>Consistency Consideration:</strong> In rare cases, an original activity might <strong>complete after</strong> its compensation has run, due to timing issues (for example, a delayed activity attempt finishing after the Workflow already assumed it failed) (<a target="_blank" href="https://community.temporal.io/t/support-for-saga-compensating-transactions-in-typescript/6392#:~:text=It%20should%20be%20noted%20that,and%20starts%20a%20compensating%20action">Support for Saga compensating transactions in Typescript - Community Support - Temporal</a>). To guard against this, ensure that activities are designed to have no effect if they are cancelled or if a compensating action was performed. This might involve application-level checks (e.g., the activity writes data with a version or token that the compensation invalidates) (<a target="_blank" href="https://community.temporal.io/t/support-for-saga-compensating-transactions-in-typescript/6392#:~:text=For%20completely%20reliable%20compensations%2C%20the,service%20or%20resource%20being%20manipulated">Support for Saga compensating transactions in Typescript - Community Support - Temporal</a>). While such race conditions are uncommon, being aware of them is part of Temporal best practices for absolutely reliable transactions.</p>
</li>
</ul>
<h2 id="heading-triggering-cleanup-activities-on-failure">Triggering Cleanup Activities on Failure</h2>
<p>Not all failure scenarios require a full saga with multiple compensating steps. Often, you just need to run a <strong>single cleanup activity</strong> at the end of a workflow to release resources (delete temporary files, send a compensating notification, etc.) if the workflow fails. Temporal workflows can use standard try/catch/finally logic to ensure cleanup runs:</p>
<ul>
<li><p><strong>Try/Catch in the Workflow:</strong> Wrap your workflow logic in a <code>try { ... } catch (err) { ... }</code>. In the catch block, call a cleanup activity. This catch will execute for any unhandled exception in the try block, whether it's an activity failure or an error thrown by workflow code. For example:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">import</span> { CancellationScope, isCancellation } <span class="hljs-keyword">from</span> <span class="hljs-string">'@temporalio/workflow'</span>;
  <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> activities <span class="hljs-keyword">from</span> <span class="hljs-string">'../activities'</span>;  <span class="hljs-comment">// import activities, including cleanup</span>

  <span class="hljs-keyword">const</span> { cleanupTempFiles } = activities; <span class="hljs-comment">// assume this is an activity to cleanup files</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FileProcessingWorkflow</span>(<span class="hljs-params">input: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> activities.processFile(input);  <span class="hljs-comment">// main activity that might fail</span>
      <span class="hljs-keyword">await</span> activities.otherStep(input);
      <span class="hljs-comment">// ... normal workflow logic ...</span>
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-comment">// Determine failure type and perform cleanup</span>
      <span class="hljs-keyword">if</span> (isCancellation(err)) {
        <span class="hljs-comment">// If workflow was cancelled, ensure cleanup runs in a non-cancellable scope</span>
        <span class="hljs-keyword">await</span> CancellationScope.nonCancellable(<span class="hljs-keyword">async</span> () =&gt; {
          <span class="hljs-keyword">await</span> cleanupTempFiles(input);
        });
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Non-cancellation failure (activity throw or other error)</span>
        <span class="hljs-keyword">await</span> cleanupTempFiles(input);
      }
      <span class="hljs-keyword">throw</span> err;  <span class="hljs-comment">// rethrow to fail the workflow after cleanup</span>
    }
  }
</code></pre>
<p>  In this example, if any activity throws an error or the workflow is cancelled, the catch block triggers the <code>cleanupTempFiles</code> activity. We use <code>CancellationScope.nonCancellable</code> when the error is a cancellation to <strong>shield the cleanup step from being cancelled</strong> (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>). This is important because when a workflow cancellation is requested, the <em>root cancellation scope</em> of the workflow is cancelled, which would normally cancel all subsequent activity invocations. Running the cleanup in a non-cancellable scope ensures the cleanup activity is started and completed even if the workflow was cancelled mid-execution (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=To%20ensure%20the%20activity%20will,Legacy%20documentation%20for%20Temporal%20SDKs">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>). Temporal's documentation shows this pattern: detecting a cancellation with <code>isCancellation(error)</code> and then running the cleanup logic in a non-cancellable scope (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>).</p>
</li>
<li><p><strong>Using Finally:</strong> You can also use a <code>finally</code> block to schedule cleanup logic that should run regardless of success or failure. However, be mindful that if the workflow is cancelled, you still need the non-cancellable scope trick. Often, the <code>catch</code> approach with rethrow (as above) is sufficient, since you typically only want to cleanup on failure scenarios. If you need to do something on <strong>both success and failure</strong>, you might call that in finally, and still handle cancellation as shown.</p>
</li>
<li><p><strong>Cleanup Activity Implementation:</strong> The cleanup itself is just another activity. It can be a simple call to delete files, rollback a database change, send a compensating event, etc. Ensure the cleanup activity is <strong>idempotent</strong> or safe to run multiple times, since in failure scenarios Temporal might retry it if it fails, or a workflow might be retried/restarted and attempt the cleanup again. For example, attempting to delete a file that’s already deleted should not error. This makes the cleanup robust.</p>
</li>
<li><p><strong>Workflow Cancellation vs Termination:</strong> Always prefer cancellation over termination when you want workflows to do cleanup. <strong>Cancellation</strong> triggers the cancellation exception inside the workflow (which you can catch as shown above to run compensations/cleanup) (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=if%20so%2C%20in%20your%20workflow,and%20run%20the%20cleanup%20activity">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>). In contrast, a <strong>termination</strong> (or an execution timeout expiring) will immediately stop the workflow without giving it a chance to handle the event (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=No,timers%20inside%20the%20workflow%20code">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>). Thus, for scenarios where you might externally stop a workflow and still need cleanup, use <code>WorkflowHandle.cancel()</code> rather than <code>WorkflowHandle.terminate()</code>. The Temporal team specifically advises using workflow cancellation (which is graceful) or internal timers for timeouts, instead of hard timeouts that the workflow cannot intercept (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=The%20recommended%20way%20is%20not,the%20terminate%20on%20a%20timer">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>) (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=No,timers%20inside%20the%20workflow%20code">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>).</p>
</li>
</ul>
<h2 id="heading-retry-policies-and-timeout-handling">Retry Policies and Timeout Handling</h2>
<p>Temporal’s built-in <strong>retry policies</strong> and timeout mechanisms play a role in how you handle failures and trigger cleanups:</p>
<ul>
<li><p><strong>Activity Retry Policy:</strong> By default, Temporal will retry an activity that fails or times out, using an exponential backoff strategy. You can configure the retry policy on each activity (max attempts, interval, etc.) or disable retries. Best practice is to allow retries for transient errors so that the workflow can succeed without human intervention. Only bypass retries for errors that are definitively not recoverable (e.g., validation errors). In those cases, throw an <code>ApplicationFailure.nonRetryable()</code> from the activity (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=must%20account%20for%20this">Temporal Error Handling In Practice</a>) so the workflow catches it immediately and can perform compensation. For example, if <code>chargePayment</code> activity in a saga gets a decline response (business rule fail), you might throw a non-retryable failure to trigger an immediate rollback instead of retrying the charge. Conversely, if an activity simply times out due to a network issue, letting Temporal retry it a few times is wise; only after final failure would the workflow enter the compensation logic.</p>
</li>
<li><p><strong>Workflow Retry Policy:</strong> You can also configure retries at the workflow level (for the whole workflow run). If a workflow run fails, Temporal Server can automatically start a new run of the workflow (often used for cron or recurring scenarios). However, if you are implementing compensation <strong>inside</strong> the workflow, you typically will not want the entire workflow retried from scratch on failure, as that could duplicate work. In most cases, leave Workflow retries disabled when using manual compensation logic, or handle idempotency carefully so that a retried workflow run doesn’t repeat side effects in an inconsistent way. (This is a more advanced scenario; often it's simpler to not rely on workflow retries for saga-style workflows and instead handle all cleanup in one run.)</p>
</li>
<li><p><strong>Heartbeats for long-running Activities:</strong> If an activity performs a lengthy operation, use heartbeats (<code>Activity.Context.heartbeat</code> in the activity code) with a heartbeat timeout. This makes the Temporal server aware that the activity is alive and allows you to cancel it faster if needed. If you cancel a workflow while an activity is running, the activity will only receive a cancellation signal on its next heartbeat (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=Cancellation%20is%20delivered%20to%20your,heartbeat%2C%20do%20your%20activities%20heartbeat">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>). A well-behaved activity should periodically heartbeat and also handle cancellation internally (e.g., by catching a <code>CanceledError</code> in activity code or checking <code>Activity.Context.cancelled</code>). Proper heartbeat usage ensures timely cancellation and thus timely execution of your cleanup logic in the workflow.</p>
</li>
<li><p><strong>Handling Workflow Timeouts Gracefully:</strong> As noted, a <strong>Workflow Execution Timeout</strong> is akin to a kill switch with no chance to run cleanup code. To ensure cleanup runs, do not solely rely on execution timeouts. Instead, implement a <strong>timer inside the workflow</strong> (e.g., using Temporal’s <code>sleep</code> or <code>Schedule</code> APIs) to enforce a deadline. If the timer fires, you can decide to throw a controlled exception or cancel the workflow from within (which triggers the cancellation flow you can handle). Another pattern is to use a parent workflow to start a child with a shorter timeout: if the child workflow times out and fails, the parent workflow catches that failure and can run a cleanup activity or compensation in the parent context (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=We%20solved%20this%20issue%20by,error%20and%20handle%20it%20properly">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>). This “parent-child” approach effectively turns an external timeout into a catchable error in the parent workflow.</p>
</li>
</ul>
<h2 id="heading-putting-it-all-together-best-practices">Putting It All Together: Best Practices</h2>
<p>To ensure a cleanup activity runs in <strong>all failure cases</strong>, combine the above techniques:</p>
<ul>
<li><p><strong>Always catch errors in the Workflow:</strong> Surround your critical workflow steps with a try/catch. In the catch, invoke cleanup or compensations. This covers activity exceptions, activity timeouts (which surface as exceptions), and even cancellation (which throws a <code>CancelledFailure</code>). Use <code>isCancellation(error)</code> to detect cancellations and run cleanup in a non-cancellable scope if needed (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>). This guarantees that even if the workflow is cancelled mid-way, your cleanup activity (or compensating actions) will be executed before the workflow truly terminates.</p>
</li>
<li><p><strong>Use CancellationScope.nonCancellable for cleanup steps:</strong> This is a crucial Temporal API to prevent a cancellation from aborting the cleanup. Any Activities or workflow code inside a non-cancellable scope will ignore cancellation requests from parent scopes (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=,timeoutMs%2C%20fn%29%3A%20If%20a%20timeout">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>) (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>). In practice, wrap your cleanup activity call or compensation loop in <code>CancellationScope.nonCancellable(...)</code> whenever you call them from a catch/finally. Temporal samples demonstrate this pattern for running cleanup after a cancellation event (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>).</p>
</li>
<li><p><strong>Leverage Saga pattern for multi-step workflows:</strong> If your workflow does several distinct operations that need rollback, structure your code to collect compensations for each step. The example above and Temporal’s saga tutorial code show how to do this in TypeScript (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>). This ensures that <em>any</em> failure at <em>any</em> point triggers the appropriate cleanup of all prior successful steps.</p>
</li>
<li><p><strong>Prefer workflow cancellation over termination/timeouts:</strong> Design your system to request cancellations when you need to stop a workflow early. This gives the workflow a chance to perform its cleanup. Avoid terminating workflows except in truly unrecoverable situations, as it will not run any workflow code thereafter (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=No,timers%20inside%20the%20workflow%20code">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>).</p>
</li>
<li><p><strong>Configure retries thoughtfully:</strong> Let Temporal handle transient failures with retries, but mark permanent errors as non-retryable to fall out to your compensation logic. Also consider the retry policy on your cleanup activity – you may want it to retry on failure (so the cleanup itself is robust).</p>
</li>
<li><p><strong>Consult Temporal documentation:</strong> Temporal’s docs and community resources have extensive discussions on failure handling. For example, the Temporal docs on cancellation and scopes provide guidance on using <code>CancellationScope</code> and checking for <code>CancelledFailure</code> (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>), and the Temporal blog covers the Saga pattern with examples in multiple languages (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>) (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=await%20addMilk%28%29%20,compensations%2C%20compensateInParallel%29%20throw%20err">Saga Compensating Transactions | Temporal</a>). These can provide additional context and examples.</p>
</li>
</ul>
<p>By following these practices, you can ensure that no matter how a workflow fails – whether an activity crashes, a third-party service call times out, or a cancellation request comes in – your Temporal workflow will reliably execute the necessary cleanup or rollback logic before completing. This leads to more resilient, correct applications that gracefully handle errors in complex long-running processes.</p>
<p><strong>Sources:</strong></p>
<ol>
<li><p>Temporal Community Forum – <em>Handling workflow cancellation and ensuring cleanup</em> (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=if%20so%2C%20in%20your%20workflow,and%20run%20the%20cleanup%20activity">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>) (<a target="_blank" href="https://community.temporal.io/t/how-to-define-cleanup-for-workflow-cancellations-typescript-sdk/6840#:~:text=To%20ensure%20the%20activity%20will,Legacy%20documentation%20for%20Temporal%20SDKs">How to define cleanup for workflow cancellations (typescript-sdk) - Community Support - Temporal</a>)</p>
</li>
<li><p>Temporal Documentation – <em>Workflow Cancellation and Scopes (TypeScript)</em> (<a target="_blank" href="https://docs.temporal.io/develop/typescript/cancellation#:~:text=%2F%2F%20Cleanup%20logic%20must%20be,Fail%20the%20Workflow">Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation</a>)</p>
</li>
<li><p>Temporal Blog – <em>Saga Pattern (Compensating Transactions) in TypeScript</em> (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=export%20async%20function%20breakfastWorkflow,unshift%28putCerealBackInBoxIfPresent%29%20await%20addCereal%28%29%20await%20addMilk">Saga Compensating Transactions | Temporal</a>) (<a target="_blank" href="https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas#:~:text=await%20addMilk%28%29%20,compensations%2C%20compensateInParallel%29%20throw%20err">Saga Compensating Transactions | Temporal</a>)</p>
</li>
<li><p>Temporal Community Forum – <em>Saga compensations and activity completion considerations</em> (<a target="_blank" href="https://community.temporal.io/t/support-for-saga-compensating-transactions-in-typescript/6392#:~:text=It%20should%20be%20noted%20that,and%20starts%20a%20compensating%20action">Support for Saga compensating transactions in Typescript - Community Support - Temporal</a>) (<a target="_blank" href="https://community.temporal.io/t/support-for-saga-compensating-transactions-in-typescript/6392#:~:text=For%20completely%20reliable%20compensations%2C%20the,service%20or%20resource%20being%20manipulated">Support for Saga compensating transactions in Typescript - Community Support - Temporal</a>)</p>
</li>
<li><p>Flightcontrol Blog – <em>Temporal Error Handling (error wrapping and non-retryable errors)</em> (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=,nonRetryableApplicationFailure">Temporal Error Handling In Practice</a>) (<a target="_blank" href="https://www.flightcontrol.dev/blog/temporal-error-handling-in-practice#:~:text=must%20account%20for%20this">Temporal Error Handling In Practice</a>)</p>
</li>
<li><p>Temporal Community Forum – <em>Workflow timeouts vs. cleanup best practices</em> (<a target="_blank" href="https://community.temporal.io/t/recommended-way-for-running-cleanup-activity-on-workflow-timeout/6455#:~:text=The%20recommended%20way%20is%20not,the%20terminate%20on%20a%20timer">Recommended way for running cleanup activity on workflow timeout - Community Support - Temporal</a>)</p>
</li>
</ol>
]]></content:encoded></item></channel></rss>