<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Durable Functions on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</title><link>https://jeppe-spanggaard.dk/tags/durable-functions/</link><description>Recent content in Durable Functions on Jeppe Spanggaard - Software Developer | .NET, Azure &amp; Microsoft 365</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Wed, 05 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jeppe-spanggaard.dk/tags/durable-functions/index.xml" rel="self" type="application/rss+xml"/><item><title>Every Activity Will Run Twice: Idempotency in Durable Functions</title><link>https://jeppe-spanggaard.dk/blogs/durable-functions-idempotent-activities/</link><pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/durable-functions-idempotent-activities/</guid><description>Learn how to make every Durable Functions activity idempotent so retry policies can safely rerun SharePoint and Graph provisioning steps.</description><content:encoded><![CDATA[<p>My provisioning engine creates SharePoint team sites through a Durable Functions orchestration: create the site, activate features, pull content types, apply templates, seed folders. Every activity call in that orchestrator goes through one shared retry policy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Outer safety net: retry any activity that throws a transient error (e.g. throttling</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// that slips past HTTP-level retry). Exponential backoff: 5s, 10s, 20s, ... up to 5min,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 10 attempts total. All activities are idempotent, so retrying the full activity is safe.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> TaskOptions ActivityRetryOptions = <span style="color:#66d9ef">new</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> RetryPolicy(
</span></span><span style="display:flex;"><span>        maxNumberOfAttempts: <span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>        firstRetryInterval: TimeSpan.FromSeconds(<span style="color:#ae81ff">5</span>),
</span></span><span style="display:flex;"><span>        backoffCoefficient: <span style="color:#ae81ff">2.0</span>,
</span></span><span style="display:flex;"><span>        maxRetryInterval: TimeSpan.FromMinutes(<span style="color:#ae81ff">5</span>)));
</span></span></code></pre></div><p>Read that comment again: &ldquo;All activities are idempotent, so retrying the full activity is safe.&rdquo;</p>
<p>That sentence is either the best thing about the whole engine or a lie that duplicates customer sites at 2am. There&rsquo;s no middle ground. The retry policy reruns the <em>entire activity</em>, not the line that failed. If your activity created a site and then died resolving its ID, the retry creates the site again - unless the activity was written to survive being run twice.</p>
<p>So this post is a catalog of how I actually make that sentence true. Five patterns, all from real provisioning code.</p>
<h2 id="pattern-1-check-before-create">Pattern 1: Check Before Create</h2>
<p>The site creation activity is the highest-stakes one. If a previous attempt created the site but failed a moment later (say, while resolving the Graph site ID), the retry must not create it again. So it probes first:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#75715e">// Idempotency: if the site already exists (e.g. previous attempt created it but</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// failed during Graph ID resolution), skip creation and go straight to Graph lookup.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">await</span> SiteExistsAsync(newSiteUrl))
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    logger.LogInformation(<span style="color:#e6db74">&#34;Site already exists at {SiteUrl}, skipping creation.&#34;</span>, newSiteUrl);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> CreateSiteAsync(parsedType, site, siteAlias, newSiteUrl);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Note what makes the probe possible in the first place: the site URL is deterministic, built from a prefix and alias in the schema. No timestamps, no random suffixes. If the URL contained <code>Guid.NewGuid()</code>, attempt two would probe a different URL, find nothing, and create a second site. Deterministic naming is the quiet prerequisite for check-before-create.</p>
<h2 id="pattern-2-already-exists-is-success">Pattern 2: &ldquo;Already Exists&rdquo; Is Success</h2>
<p>Sometimes you can&rsquo;t probe cheaply, so you attempt the operation and translate the failure. Activating a site feature that&rsquo;s already active comes back from SharePoint as an error - but it means the activity already did its job on a previous run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">catch</span> (InvalidOperationException ex) when (
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// &#34;Feature already activated&#34; comes back as HTTP 500 with odata.error.code</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// &#34;System.Data.DuplicateNameException&#34; - locale-independent, unlike the human message.</span>
</span></span><span style="display:flex;"><span>    ex.Message.Contains(<span style="color:#e6db74">&#34;System.Data.DuplicateNameException&#34;</span>, StringComparison.OrdinalIgnoreCase))
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    logger.LogInformation(<span style="color:#e6db74">&#34;{Feature} feature is already active, skipping.&#34;</span>, displayName);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The comment is a scar. My first version matched on the English error text, which works great until the code runs against a tenant in another language. Match on error <em>codes</em>, not error <em>messages</em>. The OData error code survives localization; &ldquo;Feature &hellip; is already activated&rdquo; does not.</p>
<p>Graph makes this pattern cleaner because it gives you a real status code. Copying folder structures into the new site:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">catch</span> (ODataError ex) when (ex.ResponseStatusCode == <span style="color:#ae81ff">409</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Item already exists in target - idempotent on retry</span>
</span></span><span style="display:flex;"><span>    logger.LogInformation(<span style="color:#e6db74">&#34;Item &#39;{ItemName}&#39; already exists in target, skipping copy.&#34;</span>, itemName);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>409 Conflict on a create is not a failure. It&rsquo;s a receipt from your previous attempt.</p>
<h2 id="pattern-3-upsert-by-nature">Pattern 3: Upsert by Nature</h2>
<p>The best idempotency is the kind you get for free by choosing the right API. The registration activity writes the new site&rsquo;s URL and ID back to an inventory list, and it does so with SharePoint&rsquo;s <code>ValidateUpdateListItem</code> against a known item ID. Setting the same fields on the same item to the same values twice is indistinguishable from doing it once. There&rsquo;s nothing to guard because the operation has no failure mode for repetition.</p>
<p>When you&rsquo;re designing an activity and you get to pick between &ldquo;add a row&rdquo; and &ldquo;set fields on item X&rdquo;, pick the second. Every <code>Add</code> needs a guard; a keyed <code>Set</code> guards itself.</p>
<h2 id="pattern-4-verify-after-write">Pattern 4: Verify After Write</h2>
<p>The dark twin of idempotency: some SharePoint writes report success and then don&rsquo;t stick. Property bag values on a freshly created site are notorious for this. The activity&rsquo;s answer is to re-read everything it just wrote and throw if reality disagrees:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (mismatches.Count &gt; <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> InvalidOperationException(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">$&#34;Property bag verification failed on {siteUrl}: {string.Join(&#34;</span>; <span style="color:#e6db74">&#34;, mismatches)}. &#34;</span> +
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Values did not persist to the server; retrying the activity.&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li>After writing the property bag values, the activity loads <code>Web.AllProperties</code> fresh from the server.</li>
<li>Each expected key/value is compared against what actually persisted.</li>
<li>Any mismatch throws a plain transient exception, which hands the problem to the orchestrator&rsquo;s retry policy.</li>
<li>The rerun is safe precisely because setting a property bag key is an upsert - pattern 3 again. Verification and idempotency are two halves of the same loop: writes are repeatable, so failed verification can simply demand a repeat.</li>
</ol>
<p>This inverts the usual relationship with retries. Instead of retries being something inflicted on the activity, the activity uses a throw to <em>request</em> one.</p>
<h2 id="pattern-5-idempotent-deletes-too">Pattern 5: Idempotent Deletes Too</h2>
<p>Removal flows have the same problem in the other direction. Removing a user from a site&rsquo;s members group when they&rsquo;re already gone throws a <code>ServerException</code>, and treating that as failure would wedge every cleanup retry:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">catch</span> (ServerException ex)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    logger.LogWarning(ex, <span style="color:#e6db74">&#34;Could not remove {LoginName} from {SiteUrl} ({ErrorType}); treating as already removed.&#34;</span>,
</span></span><span style="display:flex;"><span>        loginName, siteUrl, ex.ServerErrorTypeName);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Desired state: user is not in the group. User is not in the group. Done. Idempotency is about converging on a state, not performing an action.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>One non-idempotent activity poisons the whole pipeline.</strong> I had a security activity that created SharePoint groups without checking for existing ones. Every retry of that step risked duplicate groups, which meant retries weren&rsquo;t safe, which meant the retry policy comment was a lie for the whole orchestration. It&rsquo;s disabled until it earns its way back in. Idempotency is all-or-nothing per pipeline.</li>
<li><strong>The failure window is between success and checkpoint.</strong> The orchestrator records completed steps, but a crash can land after the activity finished and before the record was written. The completed-steps list narrows how often reruns happen; idempotency is what makes the remaining reruns harmless. You need both.</li>
<li><strong><code>catch</code> the narrowest thing you can.</strong> The 409 handler above catches <code>ODataError</code> with status 409, nothing else. A broad <code>catch { return; }</code> also &ldquo;makes retries pass&rdquo;, by swallowing real failures. Idempotency guards should be precise enough that a genuinely broken call still throws.</li>
<li><strong>Test by running it twice, literally.</strong> My smoke test for a new activity is: run the provisioning, then immediately queue the exact same provisioning again. Zero errors and zero duplicates or it doesn&rsquo;t ship.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>A Durable Functions retry policy is a contract: it promises to rerun your activities, and your activities promise not to care. Check before create, read &ldquo;already exists&rdquo; as success, prefer upserts, verify writes that lie. Write every activity as if it will run twice, because on a long enough timeline, it will.</p>
<p>If retry policies and replay are new to you, <a href="https://jeppe-spanggaard.dk/blogs/what-are-durable-functions/">Durable Functions: A Function That Sleeps for a Week</a> covers what an orchestration actually is, and why &ldquo;it will run twice&rdquo; is a feature rather than a bug.</p>
]]></content:encoded></item><item><title>Provision Forward, Never Backward: Checkpointing Durable Functions</title><link>https://jeppe-spanggaard.dk/blogs/durable-functions-provisioning-checkpoints/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/durable-functions-provisioning-checkpoints/</guid><description>Learn how to checkpoint a Durable Functions orchestration so a failed SharePoint provisioning run resumes where it stopped instead of starting over.</description><content:encoded><![CDATA[<p>I built a provisioning engine that creates a SharePoint team site for every new customer. Create the site, activate features, apply a template, seed a folder structure, add groups, register the site in an inventory list. Roughly ten steps, several minutes end to end, talking to SharePoint and Microsoft Graph the whole way.</p>
<p>The first time step seven failed, I got to watch my engine try to create a site that already existed.</p>
<p>That run taught me the rule this post is about: in a long provisioning flow, you don&rsquo;t roll back when something fails. You checkpoint, and you resume forward.</p>
<p>If Durable Functions are new to you, start with my intro to <a href="https://jeppe-spanggaard.dk/blogs/what-are-durable-functions/">what they are and what they can do</a> - this post builds on it.</p>
<h2 id="why-rollback-is-the-wrong-instinct">Why Rollback Is the Wrong Instinct</h2>
<p>When provisioning fails at step seven, you have a half-built site. The textbook answer is a compensation saga: undo steps six through one in reverse order. Delete the folders, detach the template, deactivate the features, delete the site, then recreate everything from scratch on the next attempt.</p>
<p>I started sketching that and stopped halfway through the list, because every line was either dangerous or absurd. Deleting a site collection to work around a failed navigation tweak is using a crane to hang a picture. And some steps don&rsquo;t even have an undo - you can&rsquo;t meaningfully &ldquo;unapply&rdquo; a provisioning template that merged fields into existing lists.</p>
<p>Here&rsquo;s the thing rollback ignores: the six completed steps aren&rsquo;t damage. They&rsquo;re progress. The site is fine; what&rsquo;s missing is the steps that haven&rsquo;t run yet. So the only recovery that makes sense is forward: figure out where the run stopped, and continue from there.</p>
<p>That reframes the problem completely. I don&rsquo;t need compensation logic. I need to know, reliably, which steps finished.</p>
<h2 id="carry-the-progress-in-the-state">Carry the Progress in the State</h2>
<p>Durable Functions makes this natural, because an orchestrator already passes state to each activity and gets state back. The trick is to make &ldquo;what&rsquo;s done&rdquo; part of that state. My orchestration state looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">record</span> <span style="color:#a6e22e">SiteSetupState</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> CustomerId,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span> SiteUrl,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">string</span>[] CompletedSteps
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>And the orchestrator walks its steps like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> state = context.GetInput&lt;SiteSetupState&gt;() ?? <span style="color:#66d9ef">await</span> CreateSite(context, customerId);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">string</span>[] pipeline =
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">[
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    nameof(ActivateFeatures),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    nameof(ApplyTemplate),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    nameof(SeedFolders),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    nameof(AddMemberGroups),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">    nameof(RegisterSite),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">]</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> (<span style="color:#66d9ef">var</span> step <span style="color:#66d9ef">in</span> pipeline)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (state.CompletedSteps.Contains(step))
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        logger.LogInformation(<span style="color:#e6db74">&#34;{Step} already completed, skipping.&#34;</span>, step);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(step, state, retryOptions);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    state = state with { CompletedSteps = [.. state.CompletedSteps, step] };
</span></span><span style="display:flex;"><span>    context.SetCustomStatus(state);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li>The pipeline is just an ordered list of activity names. Nothing clever, and that&rsquo;s the point - the interesting machinery is around the calls, not in them.</li>
<li>Before each step, the orchestrator checks whether this step already ran. On a fresh run the array is empty and nothing is skipped. On a resumed run, this check is what fast-forwards past the finished work.</li>
<li>After each successful step, the state is copied with the step name appended. An immutable <code>with</code> copy, so nothing mutates in place, which keeps replays honest.</li>
<li><code>SetCustomStatus</code> publishes the updated state on the orchestration instance. This is the checkpoint. It costs one line.</li>
</ol>
<p>That last line is doing more work than it looks like. Custom status is readable from <em>outside</em> the orchestration - through the management API, without touching the orchestration history. So the same call gives you two things: anyone polling the instance sees live progress (&ldquo;three of six steps done&rdquo;), and if the run fails, the last published state is sitting right there, telling you exactly where it stopped.</p>
<h2 id="resume-is-just-input">Resume Is Just Input</h2>
<p>Because the checkpoint is a plain serializable record, resuming a failed run doesn&rsquo;t need any special framework support. You read the failed instance&rsquo;s custom status, and you start a <em>new</em> orchestration with that state as input:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> failed = <span style="color:#66d9ef">await</span> client.GetInstanceAsync(failedInstanceId, getInputsAndOutputs: <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> checkpoint = failed.ReadCustomStatusAs&lt;SiteSetupState&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> client.ScheduleNewOrchestrationInstanceAsync(
</span></span><span style="display:flex;"><span>    nameof(SiteSetupOrchestrator),
</span></span><span style="display:flex;"><span>    checkpoint);
</span></span></code></pre></div><p>The new run enters the same loop, finds five steps in <code>CompletedSteps</code>, skips them in about a millisecond, and picks up at step six. No site deletion, no re-creation, no duplicate template apply. The half-built site becomes a five-sixths-built site, then a finished one.</p>
<p>I like how little there is to this. The &ldquo;resume feature&rdquo; is the skip-check in the loop plus the fact that the input type and the checkpoint type are the same type. That&rsquo;s it.</p>
<h2 id="the-gap-that-idempotency-covers">The Gap That Idempotency Covers</h2>
<p>One honest caveat. The checkpoint is written <em>after</em> the activity succeeds, so there&rsquo;s a window: the activity finishes, the process dies before the checkpoint lands. On resume, that step&rsquo;s name isn&rsquo;t in <code>CompletedSteps</code>, and it runs again.</p>
<p>You can&rsquo;t close that window - it&rsquo;s inherent to doing the work and recording the work as two operations. What you do instead is make every activity safe to run twice: check before create, treat &ldquo;already exists&rdquo; as success, write with upserts. That&rsquo;s a full topic on its own, but the division of labor is worth stating plainly: <strong>the checkpoint decides how often steps rerun, idempotency decides whether reruns hurt.</strong> You need both. The checkpoint alone has a crash window; idempotency alone means re-executing ten minutes of finished work on every hiccup.</p>
<p>And never checkpoint <em>before</em> the call to close the window from the other side - then a crashed step gets skipped on resume, which is far worse. A step that runs twice is a wasted minute; a step that runs zero times is a broken site that says &ldquo;Completed&rdquo;. Ask me how I know.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>Use the replay-safe logger.</strong> Orchestrator code replays from history every time the instance wakes up. <code>context.CreateReplaySafeLogger(...)</code> keeps your logs from repeating every completed step on each replay. A regular <code>ILogger</code> in an orchestrator will gaslight you.</li>
<li><strong>No clocks, no GUIDs in the orchestrator.</strong> <code>DateTime.UtcNow</code>, <code>Guid.NewGuid()</code>, and <code>Random</code> produce different values on replay and corrupt the history. Anything nondeterministic belongs inside an activity, including generated names and timestamps you want in the state.</li>
<li><strong>Custom status has a size limit</strong> (16 KB of JSON). A record with a customer ID, a URL, and an array of step names fits hundreds of times over, but don&rsquo;t stuff a whole template or file manifest in there. Checkpoint the <em>position</em>, not the <em>payload</em>.</li>
<li><strong>Step names are a contract.</strong> The moment <code>CompletedSteps</code> is persisted anywhere - a failed instance you might resume next week - renaming an activity breaks the match and the step silently reruns (fine, if idempotent) or the resume misbehaves (not fine). Rename with the same care you&rsquo;d give a database column.</li>
<li><strong>Report progress somewhere humans look.</strong> Custom status is great for machines; my engine also writes the current step name to a status column in the site inventory list, inside a try/catch that logs and swallows. A cosmetic status write must never kill a provisioning run.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Long provisioning flows fail in the middle, so design for resuming instead of undoing: carry a list of completed steps in the orchestration state, publish it as custom status after every step, skip completed steps on rerun, and feed the saved state back in to resume. Rollback is for databases. Provisioning goes forward.</p>
]]></content:encoded></item><item><title>Durable Functions: A Function That Sleeps for a Week</title><link>https://jeppe-spanggaard.dk/blogs/what-are-durable-functions/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://jeppe-spanggaard.dk/blogs/what-are-durable-functions/</guid><description>Learn what Azure Durable Functions are and what they can do, using a SharePoint site provisioning workflow as a real-world example.</description><content:encoded><![CDATA[<p>I had to build a provisioning engine. Every time a new customer lands in the business system, it creates a SharePoint team site for them: create the site, activate features, apply a template, seed a folder structure, add member groups, register the site in an inventory list. Roughly ten steps, several minutes end to end, and every single step talks to an API that can throttle, hiccup, or time out.</p>
<p>My first instinct was a plain Azure Function. One trigger, one method, ten steps in a row.</p>
<p>That instinct survived about a day. A plain function has a timeout measured in minutes. It holds its state in local variables, so when the process restarts mid-run (deployments, scale-in, Azure having a Tuesday), everything it knew is gone. Step six fails and your options are &ldquo;run all ten steps again&rdquo; or &ldquo;reconstruct what happened from logs like a crime scene&rdquo;.</p>
<p>And it&rsquo;s not like the step-sequencing part was new to me. I&rsquo;d already built <a href="https://github.com/Pinksky-ApS/provisioning-pipeline-dotnet">ProvisioningPipeline</a>, an open-source .NET library for composing provisioning workflows from middleware-style steps, with hooks and parallel execution. It solves the &ldquo;run these steps in order, cleanly&rdquo; problem well. What no in-process pipeline can solve is the other half: a pipeline in memory still dies with the process, and all its progress dies with it.</p>
<p>What I actually needed was a function that can run for a long time, remember how far it got, and pick up where it left off. That&rsquo;s not a pattern you bolt onto a normal function. It&rsquo;s a different tool, and Azure ships it: Durable Functions.</p>
<h2 id="three-functions-one-flow">Three Functions, One Flow</h2>
<p>A durable workflow is made of three kinds of functions, and the separation is the whole idea.</p>
<p>The <strong>client</strong> is a normal function with a normal trigger - HTTP, queue, timer, whatever starts your process. Its only durable job is to schedule an orchestration and get out of the way:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Function(nameof(StartProvisioning))]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task Run(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [ServiceBusTrigger(&#34;provisioning&#34;)]</span> ProvisioningMessage message,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [DurableClient]</span> DurableTaskClient client)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> client.ScheduleNewOrchestrationInstanceAsync(
</span></span><span style="display:flex;"><span>        nameof(SiteSetupOrchestrator),
</span></span><span style="display:flex;"><span>        message.ToSetupRequest());
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <strong>orchestrator</strong> is the recipe. It calls the steps in order and holds the workflow logic, but does no real work itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Function(nameof(SiteSetupOrchestrator))]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task RunOrchestrator(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [OrchestrationTrigger]</span> TaskOrchestrationContext context)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> request = context.GetInput&lt;SiteSetupRequest&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> site = <span style="color:#66d9ef">await</span> context.CallActivityAsync&lt;SiteInfo&gt;(nameof(CreateSite), request);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(ActivateFeatures), site);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(ApplyTemplate), site);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(SeedFolders), site);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(AddMemberGroups), site);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(RegisterSite), site);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <strong>activities</strong> are where the actual SharePoint and Graph calls live. Each one is a small, self-contained function: <code>CreateSite</code> creates a site, <code>ApplyTemplate</code> applies a template, and so on.</p>
<p><strong>What&rsquo;s happening here?</strong></p>
<ol>
<li>The client schedules an orchestration instance and returns immediately. Nothing waits around; the run now has a life of its own, with an instance ID you can use to check on it later.</li>
<li>The orchestrator reads like a script: six awaited steps, top to bottom. This is the part that would be a state machine diagram in Logic Apps or a pile of queues in a hand-rolled solution. Here it&rsquo;s just code, with loops and try/catch available when you need them.</li>
<li>Every <code>CallActivityAsync</code> is a checkpoint boundary. The runtime records that the step completed, and what it returned, before moving on.</li>
<li>Activities take input, do I/O, return output. They don&rsquo;t know they&rsquo;re part of a workflow, which keeps them easy to write and easy to test.</li>
</ol>
<h2 id="the-durable-part-is-a-history-table">The &ldquo;Durable&rdquo; Part Is a History Table</h2>
<p>Here&rsquo;s what separates this from a normal async method: the orchestrator&rsquo;s progress doesn&rsquo;t live in memory. Every scheduled activity and every result gets written to a history table in storage as an event.</p>
<p>When the process dies mid-run - and over a multi-minute workflow across enough customers, it will - the runtime picks the orchestration up on another instance and <em>replays</em> it. The orchestrator code runs again from the top, but every activity that already completed doesn&rsquo;t re-execute; its recorded result comes straight back from history. The replay fast-forwards through the finished steps in milliseconds and resumes real work at the first step without a recorded result.</p>
<p>It&rsquo;s a save file in a video game. The console losing power doesn&rsquo;t send you back to level one; you reload and continue from the last save point. The orchestrator saves after every activity, and it never plays a level twice.</p>
<p>That same mechanism is why an orchestration can wait for days without costing you anything. Awaiting a durable timer or an external event just means &ldquo;write the wait to history and unload from memory&rdquo;. No process is alive, no compute is billed, and when the timer fires next week, replay reconstructs the state and continues. A function that sleeps for a week, literally.</p>
<h2 id="what-else-is-in-the-box">What Else Is in the Box</h2>
<p>Sequential steps are the basics. A few more capabilities, through the provisioning lens:</p>
<p><strong>Per-step retries.</strong> Every activity call can carry a retry policy, and this is the built-in retry logic worth knowing about. Mine looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">readonly</span> TaskOptions ActivityRetryOptions = <span style="color:#66d9ef">new</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">new</span> RetryPolicy(
</span></span><span style="display:flex;"><span>        maxNumberOfAttempts: <span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>        firstRetryInterval: TimeSpan.FromSeconds(<span style="color:#ae81ff">5</span>),
</span></span><span style="display:flex;"><span>        backoffCoefficient: <span style="color:#ae81ff">2.0</span>,
</span></span><span style="display:flex;"><span>        maxRetryInterval: TimeSpan.FromMinutes(<span style="color:#ae81ff">5</span>)));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> context.CallActivityAsync(nameof(ApplyTemplate), site, ActivityRetryOptions);
</span></span></code></pre></div><p>If <code>ApplyTemplate</code> throws because SharePoint returned a 429, the runtime waits 5 seconds and reruns the activity. Then 10, then 20, backing off exponentially up to 5 minutes, for up to 10 attempts. The orchestration just sits there durably waiting between attempts. For workflows built on APIs that throttle, this one feature pays for the whole ticket.</p>
<p><strong>Fan-out/fan-in.</strong> Steps that don&rsquo;t depend on each other can run as parallel activities and be awaited together with <code>Task.WhenAll</code>. Seeding twelve folders one by one is a for-loop; seeding them as twelve parallel activities is the same loop without the awaits until the end.</p>
<p><strong>Durable timers.</strong> <code>context.CreateTimer(...)</code> gives you delays and polling loops that survive restarts. Waiting 30 seconds for SharePoint to finish creating a site is a timer, not a <code>Task.Delay</code> and a prayer.</p>
<p><strong>External events.</strong> An orchestration can pause until something outside signals it - <code>WaitForExternalEvent</code> is how you build &ldquo;wait for an admin to approve, then continue&rdquo;, even if the admin takes four days.</p>
<p><strong>Status from the outside.</strong> Every instance can be queried by ID: is it running, what step is it on, did it fail and why. My provisioning engine exposes this so the business system can show &ldquo;site is being created&rdquo; instead of a shrug.</p>
<h2 id="what-it-doesnt-do-for-you">What It Doesn&rsquo;t Do for You</h2>
<p>An honest intro should include the bill.</p>
<p><strong>Retries are per step, not per run.</strong> The retry policy reruns a failing <em>activity</em>. If an activity exhausts all ten attempts, the orchestration fails, and a failed run stays failed - nothing in the box restarts it from where it stopped. Resuming a dead run is a pattern you build yourself on top of these primitives.</p>
<p><strong>Retried activities rerun whole.</strong> When attempt two starts, it starts from the activity&rsquo;s first line, not from where attempt one died. An activity that created a site and then failed will try to create the site again. Every activity has to be written to survive running twice, and that&rsquo;s on you.</p>
<p><strong>Orchestrator code lives under house rules.</strong> Because the orchestrator replays, it must be deterministic: same history in, same decisions out. No <code>DateTime.UtcNow</code>, no <code>Guid.NewGuid()</code>, no HTTP calls, no reading config that might change between replays. Anything nondeterministic belongs in an activity. The rules are simple but unforgiving, and the failure mode is a corrupted run, not a compile error.</p>
<p>None of these are dealbreakers. They&rsquo;re the shape of the tool: the runtime guarantees your progress is never lost, and in exchange you write steps that tolerate being repeated.</p>
<h2 id="gotchas">Gotchas</h2>
<ul>
<li><strong>Use the replay-safe logger in orchestrators.</strong> <code>context.CreateReplaySafeLogger(...)</code> suppresses log lines during replay. A regular <code>ILogger</code> logs every completed step again on every replay, and the duplicated log stream will have you debugging problems you don&rsquo;t have.</li>
<li><strong>Keep activities small and single-purpose.</strong> One activity per step means one checkpoint per step. A mega-activity that does five things gives you one checkpoint for five steps, and a failure at thing four reruns things one through three.</li>
<li><strong>The retry policy can&rsquo;t tell a 429 from a typo.</strong> It retries every exception the same way, including the ones that will never succeed, like a broken config value. Distinguishing &ldquo;try again&rdquo; from &ldquo;stop, a human needs to fix this&rdquo; takes extra work.</li>
<li><strong>Don&rsquo;t sneak I/O into the orchestrator.</strong> It compiles, it works on the happy path, and it breaks determinism the first time a replay gets a different answer. If it touches the network or the clock, it&rsquo;s an activity.</li>
</ul>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>A Durable Function is a workflow that remembers where it was: an orchestrator sequences the steps, activities do the work, and the runtime checkpoints every step so crashes, restarts, and week-long waits don&rsquo;t lose progress. If your process has numbered steps, talks to flaky APIs, and takes longer than a request, it&rsquo;s a fit - provisioning was mine.</p>
]]></content:encoded></item></channel></rss>