<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://keithtenzer.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://keithtenzer.com/" rel="alternate" type="text/html" /><updated>2025-08-22T18:44:32+00:00</updated><id>https://keithtenzer.com/feed.xml</id><title type="html">Keith Tenzer’s Blog</title><subtitle></subtitle><author><name>Keith Tenzer</name></author><entry><title type="html">webAI: Guide to Building Custom Elements</title><link href="https://keithtenzer.com/ai/WebAI-Custom-Elements-Advanced/" rel="alternate" type="text/html" title="webAI: Guide to Building Custom Elements" /><published>2025-08-22T00:00:00+00:00</published><updated>2025-08-22T00:00:00+00:00</updated><id>https://keithtenzer.com/ai/WebAI-Custom-Elements-Advanced</id><content type="html" xml:base="https://keithtenzer.com/ai/WebAI-Custom-Elements-Advanced/"><![CDATA[<p><img src="/assets/2025-05-23/WebAI.png" alt="My Image" width="100" height="75" /></p>

<h2 id="overview">Overview</h2>
<p>In the webAI platform, an Element is the basic building block of an AI pipeline. Think of it as a modular unit that either generates data, transforms it, or routes it further down the workflow. Elements can emit frames, process text, run models, or even orchestrate I/O across multiple data streams. They enable organizations to solve real business problems such as automating customer support, enriching knowledge management systems, accelerating content generation, or integrating AI-driven insights directly into business applications. For example, an Element might connect to a CRM to streamline sales workflows, ingest enterprise documents for retrieval-augmented generation, or process video feeds in real time for compliance and monitoring.</p>

<p>To standardize how developers create, configure, and connect these elements, the Element SDK provides a lightweight but powerful scaffolding layer. The ElementSDK provides the necessary boilerplate for making your custom code usable in the webAI platform.</p>

<h2 id="what-is-an-element">What is an Element?</h2>
<p>An Element is a building block used to create workflows or Flows in Navigator. There are two types of Elements: Built-in and Custom. Built-in Elements are provided through the official Element Registry and are part of the webAI platform. Custom elements are built using python and the ElementSDK.</p>

<p>Elements are Python packages that define the following:
Inputs
Outputs
Settings
Requirements
Run Function</p>

<p>An Element can have an input, an output, both, or neither.
I/O
Example
Input
Receives data from another element
Output
Sends data to another element
Input &amp; Output
Processes input, performs a transformation, and passes the result to another element
None
Stand-alone element (e.g., for model training or dataset generation)</p>

<h3 id="element-scaffolding">Element Scaffolding</h3>
<p>Every element follows a predictable project structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root-folder/
├── element_name/
│   ├── __init__.py   
│   ├── element.py   
├── setup.py          
└── requirements.txt  
</code></pre></div></div>

<h3 id="element-settings">Element Settings</h3>
<p>Element settings are configurable parameters users can adjust at runtime in Navigator. The SDK supports a variety of settings: NumberSetting, TextSetting, BoolSetting.</p>

<p>Example:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Settings</span><span class="p">(</span><span class="n">ElementSettings</span><span class="p">):</span>
    <span class="n">color</span> <span class="o">=</span> <span class="n">TextSetting</span><span class="p">(...)</span>
    <span class="n">delay</span> <span class="o">=</span> <span class="n">NumberSetting</span><span class="p">[</span><span class="nb">int</span><span class="p">](...)</span>
</code></pre></div></div>

<h4 id="boolsetting">BoolSetting</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool_setting</span> <span class="o">=</span> <span class="n">BoolSetting</span><span class="p">(</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"bool_setting"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"A Dropdown Toggle"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"Whether Dropdown should be displayed"</span><span class="p">,</span>
   <span class="n">required</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>
<h4 id="numbersetting">NumberSetting</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">number_setting</span> <span class="o">=</span> <span class="n">NumberSetting</span><span class="p">[</span><span class="nb">int</span><span class="p">](</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"number"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"A Number"</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"This is a number"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
   <span class="n">min_value</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
   <span class="n">step</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="dropdown-textsetting">Dropdown TextSetting</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dropdown_setting</span> <span class="o">=</span> <span class="n">TextSetting</span><span class="p">(</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"dropdown_setting"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"Dropdown Setting"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="s">"item1"</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"An Item"</span><span class="p">,</span>
   <span class="n">valid_values</span><span class="o">=</span><span class="p">[</span>
      <span class="s">"item1"</span><span class="p">,</span>
      <span class="s">"item2"</span><span class="p">,</span>
      <span class="s">"item3"</span><span class="p">,</span>
   <span class="p">],</span>
   <span class="n">hints</span><span class="o">=</span><span class="p">[</span><span class="s">"dropdown"</span><span class="p">],</span>
   <span class="n">required</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="hints">Hints</h3>
<p>Some settings support hints that modify their rendered appearance in Navigator:</p>

<table>
  <thead>
    <tr>
      <th>Class</th>
      <th>Hint</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>TextSetting</td>
      <td>dropdown</td>
      <td>Renders a select box populated with valid_values</td>
    </tr>
    <tr>
      <td>TextSetting</td>
      <td>folder_path</td>
      <td>Renders a folder picker</td>
    </tr>
  </tbody>
</table>

<h3 id="element-inputs-and-outputs">Element Inputs and Outputs</h3>
<p>Elements communicate through strongly-typed slots. Types include Frame, TextFrame, ImageFrame, MLXFrame, and Preview.</p>

<p>Example:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Inputs</span><span class="p">(</span><span class="n">ElementInputs</span><span class="p">):</span>
    <span class="n">default</span> <span class="o">=</span> <span class="n">Input</span><span class="p">[</span><span class="n">Frame</span><span class="p">]()</span>

<span class="k">class</span> <span class="nc">Outputs</span><span class="p">(</span><span class="n">ElementOutputs</span><span class="p">):</span>
    <span class="n">default</span> <span class="o">=</span> <span class="n">Output</span><span class="p">[</span><span class="n">Frame</span><span class="p">]()</span>
</code></pre></div></div>

<h3 id="element-run-function">Element Run Function</h3>
<p>At the core of every element lies its run function, an asynchronous coroutine that drives the element’s execution. The Element SDK is designed around a continuous streaming model, meaning elements remain active and process data in real time. If your workflow requires batching or sequential execution, the recommended approach is to keep all elements running until the final element completes its task. Once any element terminates, the WebAI Runtime will automatically shut down the remaining connected elements to ensure a clean and consistent pipeline state.</p>

<p>Example:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">process</span><span class="p">:</span> <span class="n">Process</span><span class="p">):</span>
    <span class="n">settings</span><span class="p">,</span> <span class="n">outputs</span> <span class="o">=</span> <span class="n">process</span><span class="p">.</span><span class="n">settings</span><span class="p">,</span> <span class="n">process</span><span class="p">.</span><span class="n">outputs</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">reset_frame</span><span class="p">(</span><span class="n">settings</span><span class="p">.</span><span class="n">color</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">delay</span> <span class="o">=</span> <span class="n">settings</span><span class="p">.</span><span class="n">delay</span><span class="p">.</span><span class="n">value</span>

    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">outputs</span><span class="p">.</span><span class="n">default</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">frame</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">delay</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="input-handling">Input Handling</h3>
<p>When your element consumes data, you need a frame receiver function. Example relay:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">frame_receiver</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">frame</span><span class="p">:</span> <span class="n">Frame</span><span class="p">):</span>
    <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">frame_queue</span><span class="p">.</span><span class="n">put</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">process</span><span class="p">:</span> <span class="n">Process</span><span class="p">):</span>
    <span class="n">outputs</span> <span class="o">=</span> <span class="n">process</span><span class="p">.</span><span class="n">outputs</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">frame</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">frame_queue</span><span class="p">.</span><span class="n">get</span><span class="p">()</span>
        <span class="k">await</span> <span class="n">outputs</span><span class="p">.</span><span class="n">default</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">frame</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">frame_queue</span><span class="p">.</span><span class="n">task_done</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="create-helloworld-element">Create Helloworld Element</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hello-world/
├── hello_world/
│   ├── __init__.py         
│   └── element.py       
├── requirements.txt
└── setup.py
</code></pre></div></div>

<h3 id="directory-structure">Directory Structure</h3>
<p>webAI elements require a defined directory structure.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">mkdir</span> <span class="nt">-p</span> helloworld/helloworld
</code></pre></div></div>

<h3 id="element-definition">Element Definition</h3>
<p>Elements are defined by creating a setup.py under the element director where both element name and version are required. Optionally, if you are using poetry you can create a pyproject.toml.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">vi</span> <span class="n">helloworld</span><span class="o">/</span><span class="n">setup</span><span class="p">.</span><span class="n">py</span>
<span class="kn">from</span> <span class="nn">setuptools</span> <span class="kn">import</span> <span class="n">find_packages</span><span class="p">,</span> <span class="n">setup</span>

<span class="n">setup</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"hello_world"</span><span class="p">,</span>
    <span class="n">version</span><span class="o">=</span><span class="s">"0.1.0"</span><span class="p">,</span>
    <span class="n">packages</span><span class="o">=</span><span class="n">find_packages</span><span class="p">(),</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="element-dependencies">Element Dependencies</h3>
<p>Dependencies can be managed using a requirements.txt file.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">vi</span> <span class="n">helloworld</span><span class="o">/</span><span class="n">requirements</span><span class="p">.</span><span class="n">txt</span>
<span class="o">--</span><span class="n">extra</span><span class="o">-</span><span class="n">index</span><span class="o">-</span><span class="n">url</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">gitlab</span><span class="p">.</span><span class="n">com</span><span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">v4</span><span class="o">/</span><span class="n">projects</span><span class="o">/</span><span class="mi">49121232</span><span class="o">/</span><span class="n">packages</span><span class="o">/</span><span class="n">pypi</span><span class="o">/</span><span class="n">simple</span>
<span class="n">webai_element_sdk</span><span class="o">==</span><span class="mf">0.10</span><span class="p">.</span><span class="o">*</span>
</code></pre></div></div>

<h3 id="element-boilerplate">Element Boilerplate</h3>
<p>Element settings goes in the element/element/element.py.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">vi</span> <span class="n">helloworld</span><span class="o">/</span><span class="n">helloworld</span><span class="o">/</span><span class="n">element</span><span class="p">.</span><span class="n">py</span>

<span class="kn">from</span> <span class="nn">webai_element_sdk.element.settings</span> <span class="kn">import</span> <span class="n">ElementSettings</span><span class="p">,</span> <span class="n">TextSetting</span>

<span class="k">class</span> <span class="nc">Settings</span><span class="p">(</span><span class="n">ElementSettings</span><span class="p">):</span>
    <span class="n">greeting</span> <span class="o">=</span> <span class="n">TextSetting</span><span class="p">(</span>
        <span class="n">name</span><span class="o">=</span><span class="s">"greeting"</span><span class="p">,</span>
        <span class="n">display_name</span><span class="o">=</span><span class="s">"Greeting"</span><span class="p">,</span>
        <span class="n">default</span><span class="o">=</span><span class="s">"Hello, World!"</span><span class="p">,</span>
        <span class="n">character_limit</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Element code goes in the element/element/<strong>init</strong>.py.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">vi</span> <span class="n">helloworld</span><span class="o">/</span><span class="n">helloworld</span><span class="o">/</span><span class="n">__init__</span><span class="p">.</span><span class="n">py</span>

<span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">from</span> <span class="nn">webai_element_sdk.element</span> <span class="kn">import</span> <span class="n">CreateElement</span>
<span class="kn">from</span> <span class="nn">webai_element_sdk.process</span> <span class="kn">import</span> <span class="n">Process</span><span class="p">,</span> <span class="n">ProcessMetadata</span>

<span class="kn">from</span> <span class="nn">.element</span> <span class="kn">import</span> <span class="n">Settings</span>

<span class="k">class</span> <span class="nc">HelloWorld</span><span class="p">:</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">process</span><span class="p">:</span> <span class="n">Process</span><span class="p">):</span>
        <span class="n">settings</span> <span class="o">=</span> <span class="n">process</span><span class="p">.</span><span class="n">settings</span>
        <span class="k">await</span> <span class="n">process</span><span class="p">.</span><span class="n">agent_comms</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="s">"HelloWorld element started"</span><span class="p">)</span>

        <span class="k">try</span><span class="p">:</span>
            <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
                <span class="k">await</span> <span class="n">process</span><span class="p">.</span><span class="n">agent_comms</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">settings</span><span class="p">.</span><span class="n">greeting</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
                <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">CancelledError</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">process</span><span class="p">.</span><span class="n">agent_comms</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="s">"HelloWorld element shutting down"</span><span class="p">)</span>
            <span class="k">raise</span>


<span class="n">hello_world</span> <span class="o">=</span> <span class="n">HelloWorld</span><span class="p">()</span>

<span class="n">process</span> <span class="o">=</span> <span class="n">CreateElement</span><span class="p">(</span>
    <span class="n">Process</span><span class="p">(</span>
        <span class="n">settings</span><span class="o">=</span><span class="n">Settings</span><span class="p">(),</span>
        <span class="n">metadata</span><span class="o">=</span><span class="n">ProcessMetadata</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"helloworld-0001"</span><span class="p">,</span>
            <span class="n">name</span><span class="o">=</span><span class="s">"hello_world"</span><span class="p">,</span>
            <span class="n">displayName</span><span class="o">=</span><span class="s">"Hello World"</span><span class="p">,</span>
            <span class="n">version</span><span class="o">=</span><span class="s">"0.1.0"</span><span class="p">,</span>
            <span class="n">description</span><span class="o">=</span><span class="s">"Simplest element: logs a configurable greeting every second."</span><span class="p">,</span>
        <span class="p">),</span>
        <span class="n">run_func</span><span class="o">=</span><span class="n">hello_world</span><span class="p">.</span><span class="n">run</span><span class="p">,</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="validate-python-environment">Validate Python Environment</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd </span>helloworld
python <span class="nt">-c</span> <span class="s2">"import sys; print(sys.executable)"</span>
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/Users/ktenzer/python/webai-element-helloworld/helloworld/venv/bin/python
</code></pre></div></div>

<p>Set Interpreter in Visual Studio Code</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="s">"python.analysis.extraPaths"</span><span class="p">:</span> <span class="p">[</span>
    <span class="s">"/Users/ktenzer/python/helloworld/helloworld/venv/lib/python3.12/site-packages"</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="import-custom-element">Import Custom Element</h3>
<p>Once we have put together our custom element, we can import it into Navigator.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>/Applications/Navigator.app/Contents/Resources/support/builder import helloworld

Jul 31 01:02:50.206 INF Using data directory <span class="nv">path</span><span class="o">=</span>/Users/keith.tenzer/.webai
Jul 31 01:02:50.206 INF Packaging <span class="nb">source </span>folder into .zip...
Jul 31 01:02:50.206 INF Generating publish.json file <span class="k">for </span>element...
Jul 31 01:02:50.477 INF Python setup <span class="nv">status</span><span class="o">=</span>success
Jul 31 01:02:50.565 INF running <span class="nb">command </span><span class="nv">commandLabel</span><span class="o">=</span>generate <span class="nb">command</span><span class="o">=</span><span class="s2">"[/Users/keith.tenzer/.webai/elements/generator/.venv/bin/python3 -um webai_element_sdk generate --path /Users/keith.tenzer/python/elements/helloworld]"</span>
Jul 31 01:02:50.735 INF Generated publish.json successfully.
Jul 31 01:02:50.745 INF Importing element .zip package...
Jul 31 01:02:50.751 INF Successfully imported element variant <span class="nb">id</span><span class="o">=</span>de8288be-f0bb-563d-a299-1e81d329744a
Jul 31 01:02:50.751 INF Cleaning up .zip package...
Jul 31 01:02:50.751 INF Element imported successfully.
</code></pre></div></div>

<h2 id="element-versioning">Element Versioning</h2>
<p>Elements are versioned by bumping the version in the <code class="language-plaintext highlighter-rouge">__init__.py</code> and setup.py followed by side-loading using the import command.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">process</span> <span class="o">=</span> <span class="n">CreateElement</span><span class="p">(</span>
    <span class="n">Process</span><span class="p">(</span>
        <span class="n">settings</span><span class="o">=</span><span class="n">Settings</span><span class="p">(),</span>
        <span class="n">metadata</span><span class="o">=</span><span class="n">ProcessMetadata</span><span class="p">(</span>
            <span class="nb">id</span><span class="o">=</span><span class="s">"helloworld-0001"</span><span class="p">,</span>
            <span class="n">name</span><span class="o">=</span><span class="s">"hello_world"</span><span class="p">,</span>
            <span class="n">displayName</span><span class="o">=</span><span class="s">"Hello World"</span><span class="p">,</span>
            <span class="n">version</span><span class="o">=</span><span class="s">"0.1.1"</span><span class="p">,</span>
            <span class="n">description</span><span class="o">=</span><span class="s">"Simplest element: logs a configurable greeting every second."</span><span class="p">,</span>
        <span class="p">),</span>
        <span class="n">run_func</span><span class="o">=</span><span class="n">hello_world</span><span class="p">.</span><span class="n">run</span><span class="p">,</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/Applications/Navigator.app/Contents/Resources/support/builder import helloworld
</code></pre></div></div>

<h2 id="configure-custom-element">Configure Custom Element</h2>
<p>Drop element into a Canvas in Navigator.
<img src="/assets/2025-08-22/navigator.png" alt="Helloworld" /></p>

<p><em>Tip</em>: Use the Initiator element to create test frames and validate your element.</p>

<p>Once a Custom Element is added to the canvas in Navigator it will show up as a package under ~/.webai/elements/elements folder.</p>

<p><em>Tip</em>: It can be helpful to debug and test your Custom Elements directly inside the deployed package.</p>

<h2 id="testing">Testing</h2>
<p>Using the webAI CLI tail log for debugging and testing elements.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">tail</span> <span class="nt">-f</span> ~/.webai/logs/runtime.log
Aug 21 11:13:51.381 INF installer/environment.go:157 Installing element dependencies <span class="nv">label</span><span class="o">=</span>helloworld:0.1.0 <span class="nv">variantID</span><span class="o">=</span>7cf75c48-3475-5fc6-b89c-837b5c9a986e
Aug 21 11:13:55.003 INF installer/environment.go:176 Installing element <span class="nv">label</span><span class="o">=</span>helloworld:0.1.0 <span class="nv">variantID</span><span class="o">=</span>7cf75c48-3475-5fc6-b89c-837b5c9a986e
</code></pre></div></div>

<p><em>Tip</em>: To explore deployed WebAI elements, import your ~/.webai folder into VS Code. Inside elements/elements, you’ll find the source code for any element you’ve deployed to a Navigator Canvas. Just deploy an element, open its <code class="language-plaintext highlighter-rouge">__init__.py</code>, and you’ll have example code ready to use.</p>

<h2 id="next-steps">Next Steps</h2>
<p>Now that you have completed helloworld see if you can integrate a Custom Element into a webAI Flow it is time to connect multiple Elements and build AI pipelines. In the below example Github repo, you will see how to create a MultiModal RAG inference and ingest pipeline using the Element SDK and webAI platform. Simply follow the steps outlined in the repo and explore the code.</p>

<p><a href="https://github.com/ktenzer/webai-multimodal-rag">https://github.com/ktenzer/webai-multimodal-rag</a></p>

<p>(c) 2025 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="AI" /><category term="AI" /><category term="Elements" /><category term="webAI" /><category term="Navigator" /><category term="Companion" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">webAI: Intro to Building Custom Elements</title><link href="https://keithtenzer.com/ai/WebAI-Custom-Elements/" rel="alternate" type="text/html" title="webAI: Intro to Building Custom Elements" /><published>2025-06-04T00:00:00+00:00</published><updated>2025-06-04T00:00:00+00:00</updated><id>https://keithtenzer.com/ai/WebAI-Custom-Elements</id><content type="html" xml:base="https://keithtenzer.com/ai/WebAI-Custom-Elements/"><![CDATA[<p><img src="/assets/2025-05-23/WebAI.png" alt="My Image" width="100" height="75" /></p>

<p><em>Note</em>: This article covers the older ElementSDK 0.9.x. The newer webAI ElementSDK is 0.10.x and recommended which is documented <a href="https://keithtenzer.com/ai/WebAI-Custom-Elements-Advanced/">here</a></p>

<h2 id="overview">Overview</h2>
<p>webAI is an on-device AI platform that enables organizations to build and deploy AI models in a distributed fashion using commodity hardware. Its key benefits are <strong>privacy</strong>, <strong>time-to-market</strong>, <strong>cost-efficiency</strong>, and <strong>accuracy</strong>. I previously wrote a <a href="https://keithtenzer.com/ai/AI_on_the_Edge/">blog</a> that explores the platform in more depth.</p>

<p>In this article, we focus on integrating external tools, services, and APIs into webAI Navigator by creating <strong>custom elements</strong>.</p>

<h2 id="custom-element">Custom Element</h2>
<p>An element is a building block used to create workflows in Navigator. There are many pre-packaged elements (with more added regularly), but the true power comes from customizing or even building your own.</p>

<p>Elements are Python packages that define the following:</p>
<ul>
  <li><strong>Inputs</strong></li>
  <li><strong>Outputs</strong></li>
  <li><strong>Settings</strong></li>
  <li><strong>Decorators</strong></li>
</ul>

<p>An element can have an input, an output, both, or neither.</p>

<table>
  <thead>
    <tr>
      <th>I/O</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Input</strong></td>
      <td>Element that inspects or otherwise processes incoming data</td>
    </tr>
    <tr>
      <td><strong>Output</strong></td>
      <td>Element that ingests data and forwards it to another element</td>
    </tr>
    <tr>
      <td><strong>Input &amp; Output</strong></td>
      <td>Element that processes input, performs a transformation, and passes the result to another element</td>
    </tr>
    <tr>
      <td><strong>None</strong></td>
      <td>Stand-alone element (e.g., for model training or dataset generation)</td>
    </tr>
  </tbody>
</table>

<h2 id="element-inputs-and-outputs">Element Inputs and Outputs</h2>
<p>Data is passed between elements in what is called a <strong>Frame</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Frame</span><span class="p">(</span>
      <span class="n">ndframe</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
      <span class="n">rois</span><span class="o">=</span><span class="p">[],</span>
      <span class="n">frame_id</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
      <span class="n">headers</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
      <span class="n">other_data</span><span class="o">=</span><span class="p">{</span><span class="s">"message"</span><span class="p">:</span> <span class="s">"Hello"</span><span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Depending on an element’s purpose, different parts of the Frame matter most:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ndframe</code> for tabular data or pre-processing</li>
  <li><code class="language-plaintext highlighter-rouge">rois</code> for regions of interest in vision use cases</li>
  <li><code class="language-plaintext highlighter-rouge">other_data</code> for miscellaneous payloads</li>
</ul>

<h2 id="element-definition">Element Definition</h2>
<p>An element definition specifies its ID, metadata, settings, and I/O:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">element</span><span class="p">:</span> <span class="n">Element</span> <span class="o">=</span> <span class="n">Element</span><span class="p">(</span>
    <span class="nb">id</span><span class="o">=</span><span class="n">UUID</span><span class="p">(</span><span class="s">"c382bdae-6680-4c64-a647-4b89fcba860b"</span><span class="p">),</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"helloworld"</span><span class="p">,</span>
    <span class="n">display_name</span><span class="o">=</span><span class="s">"helloworld"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">""</span><span class="p">,</span>
    <span class="n">version</span><span class="o">=</span><span class="s">"0.1.0"</span><span class="p">,</span>
    <span class="n">settings</span><span class="o">=</span><span class="n">Settings</span><span class="p">(),</span>
    <span class="n">inputs</span><span class="o">=</span><span class="n">Inputs</span><span class="p">(),</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="element-settings">Element Settings</h2>
<p>Element settings enable dynamic user interaction. Three setting types are available: <code class="language-plaintext highlighter-rouge">TextSetting</code>, <code class="language-plaintext highlighter-rouge">BoolSetting</code>, and <code class="language-plaintext highlighter-rouge">NumberSetting</code>.</p>

<h3 id="textsetting">TextSetting</h3>
<p>The TextSetting enables a input text-box in the Element UI:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">text_setting</span> <span class="o">=</span> <span class="n">TextSetting</span><span class="p">(</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"hellowolrd"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"HelloWorld"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="s">"HelloWorld"</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="boolsetting">BoolSetting</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool_setting</span> <span class="o">=</span> <span class="n">BoolSetting</span><span class="p">(</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"bool_setting"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"A Dropdown Toggle"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"Whether Dropdown should be displayed"</span><span class="p">,</span>
   <span class="n">required</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="numbersetting">NumberSetting</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">number_setting</span> <span class="o">=</span> <span class="n">NumberSetting</span><span class="p">[</span><span class="nb">int</span><span class="p">](</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"number"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"A Number"</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"This is a number"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
   <span class="n">min_value</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
   <span class="n">step</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="hints">Hints</h3>
<p>Some settings support <strong>hints</strong> that modify their rendered appearance:</p>

<table>
  <thead>
    <tr>
      <th>Class</th>
      <th>Hint</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TextSetting</code></td>
      <td><code class="language-plaintext highlighter-rouge">"dropdown"</code></td>
      <td>Renders a select box populated with <code class="language-plaintext highlighter-rouge">valid_values</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TextSetting</code></td>
      <td><code class="language-plaintext highlighter-rouge">"folder_path"</code></td>
      <td>Renders a folder picker</td>
    </tr>
  </tbody>
</table>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dropdown_setting</span> <span class="o">=</span> <span class="n">TextSetting</span><span class="p">(</span>
   <span class="n">name</span><span class="o">=</span><span class="s">"dropdown_setting"</span><span class="p">,</span>
   <span class="n">display_name</span><span class="o">=</span><span class="s">"Dropdown Setting"</span><span class="p">,</span>
   <span class="n">default</span><span class="o">=</span><span class="s">"item1"</span><span class="p">,</span>
   <span class="n">description</span><span class="o">=</span><span class="s">"An Item"</span><span class="p">,</span>
   <span class="n">valid_values</span><span class="o">=</span><span class="p">[</span>
      <span class="s">"item1"</span><span class="p">,</span>
      <span class="s">"item2"</span><span class="p">,</span>
      <span class="s">"item3"</span><span class="p">,</span>
   <span class="p">],</span>
   <span class="n">hints</span><span class="o">=</span><span class="p">[</span><span class="s">"dropdown"</span><span class="p">],</span>
   <span class="n">required</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="element-decorators">Element Decorators</h2>
<p>Elements have three decorators which need to be implemented: <code class="language-plaintext highlighter-rouge">startup</code>, <code class="language-plaintext highlighter-rouge">shutdown</code>, and <code class="language-plaintext highlighter-rouge">run</code>.</p>

<h3 id="startup-decorator">Startup Decorator</h3>
<p>Instructs what should occur when the element is started.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">element</span><span class="p">.</span><span class="n">startup</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">startup</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">Context</span><span class="p">[</span><span class="n">Inputs</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="n">Settings</span><span class="p">]):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"***** HelloWorld Element Startup *****"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="shutdown-decorator">Shutdown Decorator</h3>
<p>Instructs what should occur when the element is shutdown.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">element</span><span class="p">.</span><span class="n">shutdown</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">shutdown</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">Context</span><span class="p">[</span><span class="n">Inputs</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="n">Settings</span><span class="p">]):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"***** HelloWorld Element Shutdown *****"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="run-decorator">Run Decorator</h3>
<p>Instructs what should occur when the element is run (most code goes here).</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">element</span><span class="p">.</span><span class="n">executor</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">Context</span><span class="p">[</span><span class="n">Inputs</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="n">Settings</span><span class="p">]):</span>
   <span class="n">input_frame</span> <span class="o">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">inputs</span><span class="p">.</span><span class="nb">input</span><span class="p">.</span><span class="n">value</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Frame: </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">frame_id</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">headers</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">content_type</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">as_text</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">ndframe</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">input_frame</span><span class="p">.</span><span class="n">other_data</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="create-element">Create Element</h2>
<p>The WebAI CLI can scaffold, version, and manage your elements.</p>

<p>Install latest cli</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew tap thewebAI/webai
<span class="nv">$ </span>brew <span class="nb">install </span>webai
</code></pre></div></div>

<p>Initialize element</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>webai element init

✔ Enter a display name <span class="k">for </span>the new element: … helloworld
✔ Select boilerplate verbosity level: › Guided - Step-by-step examples with detailed explanations <span class="o">(</span>recommended <span class="k">for </span>first <span class="nb">time users</span><span class="o">)</span>
✔ Directory structure created
✔ Python files generated
✔ Dependencies installed successfully

✨ Success! Created new element at ./helloworld

Next steps:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Navigate to the project directory:
   <span class="nb">cd </span>helloworld

2. Activate the virtual environment:
   <span class="nb">source </span>venv/bin/activate

3. Add dependencies to requirements.txt
   pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt

4. Start developing your element <span class="k">in </span>__init__.py
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
</code></pre></div></div>

<p>Validate python:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$cd</span> hellowolrd
python <span class="nt">-c</span> <span class="s2">"import sys; print(sys.executable)"</span>
/Users/ktenzer/python/webai-element-helloworld/helloworld/venv/bin/python
</code></pre></div></div>

<p>Set Interpreter in VS Code:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"python.analysis.extraPaths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"/Users/ktenzer/python/webai-element-helloworld/helloworld/venv/lib/python3.12/site-packages"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="import-element">Import Element</h2>
<p>Once we have put together our element, we can import it into Navigator.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd </span>helloworld
<span class="nv">$ </span>webai element import <span class="nb">.</span>

✔ Element directory found
⚠ Notice: An element with version 0.1.3 has already been imported.
✔ What would you like to <span class="k">do</span>? › Bump version and retry
✔ Current version: 0.1.3
✔ Which part of the version would you like to increment? › Patch <span class="o">(</span>0.1.4<span class="o">)</span> - Bug fixes <span class="o">(</span>recommended<span class="o">)</span>
✔ Updated version <span class="k">in </span>setup.py
✔ Updated version <span class="k">in </span>__init__.py
ℹ publish.json not found, generating it...
✔ Element directory found
✔ Element packaged successfully

✨ Success! Packaged element is 2fa338eb-6e3d-5706-8627-95ced48a03c8.zip

Next step: Import your element with webai element import helloworld
✔ Generated publish.json with new version
✔ Successfully bumped version from 0.1.3 to 0.1.4
✔ Element directory found
✔ Element imported successfully

✨ Success! Imported element <span class="s2">"."</span> successfully.
</code></pre></div></div>

<h1 id="configure-element">Configure Element</h1>
<p>Once element is imported, drop element into a canvas in Navigator.</p>
<blockquote>
  <p><strong>Pro tip:</strong> Use the <em>Initiator</em> element to create test frames and validate your element.</p>
</blockquote>

<p><img src="/assets/2025-05-23/helloworld.png" alt="Helloworld configure step1" /></p>

<p>Once an element is added to the canvas in Navigator it will show up as a package under <code class="language-plaintext highlighter-rouge">~/.webai/elements/packages</code> folder.</p>
<blockquote>
  <p><strong>Pro tip:</strong> It can be helpful to debug and test directly inside the deployed package.</p>
</blockquote>

<h1 id="testing">Testing</h1>
<p>Using the webAI CLI tail log for debugging and testing elements.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>webai <span class="nb">tail
</span>13:40:44 INFO Frame: d4be9a4b-6276-4cd2-832a-37148aa8bd42 <span class="o">{</span><span class="s1">'baggage'</span>: <span class="s1">'baggage=baggage'</span><span class="o">}</span> cv2-frame &lt;<span class="k">function </span>Frame.as_text at 0x104296a70&gt; <span class="o">[[[</span>0 0 0]]] <span class="o">{</span><span class="s1">'message'</span>: <span class="s1">'start'</span><span class="o">}</span> <span class="nv">commandLabel</span><span class="o">=</span>helloworld-0.1.6
</code></pre></div></div>

<h2 id="summary">Summary</h2>
<p>We explored how to extend WebAI by creating custom elements. An element is simply a Python class that defines inputs, outputs, settings, and decorators (<code class="language-plaintext highlighter-rouge">startup</code>, <code class="language-plaintext highlighter-rouge">shutdown</code>, <code class="language-plaintext highlighter-rouge">run</code>). With the WebAI CLI, you can scaffold, version, and import elements into Navigator, then deploy them onto a canvas for seamless testing and debugging.</p>

<p>© 2025 Keith Tenzer</p>

<p>(c) 2025 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="AI" /><category term="AI" /><category term="Elements" /><category term="webAI" /><category term="Navigator" /><category term="Companion" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Agentic AI Frameworks</title><link href="https://keithtenzer.com/ai/Agentic_AI_Frameworks/" rel="alternate" type="text/html" title="Agentic AI Frameworks" /><published>2025-05-16T00:00:00+00:00</published><updated>2025-05-16T00:00:00+00:00</updated><id>https://keithtenzer.com/ai/Agentic_AI_Frameworks</id><content type="html" xml:base="https://keithtenzer.com/ai/Agentic_AI_Frameworks/"><![CDATA[<p><img src="/assets/2025-05-16/agent.png" alt="Agent" /></p>

<h2 id="overview">Overview</h2>
<p>The future of applications is Agentic and that future is here, now. AI is transforming how we build and interact with software. Large Language Models (LLMs) are being integrated into applications to increase capabilities and enable autonomous task execution. It’s no longer just about question-and-answer systems.</p>

<p>Agentic AI combines an LLM with tools—external applications, services, data sources, and more. At its core, an Agentic system is a framework that connects an LLM to a business process, effectively making it a workflow.</p>

<p>An AI Agent must handle the following:</p>
<ul>
  <li>Managing conversations between the LLM and user</li>
  <li>Configuring the LLM with appropriate roles (system, user, assistant, tool), plans, and multi-shot examples</li>
  <li>Executing tools (e.g., API calls to services, databases, other LLMs, agents, RAG, inference, etc.)</li>
  <li>Storing conversation history and managing prompting</li>
</ul>

<p>That’s a substantial amount of responsibility.</p>

<h2 id="technology-stack">Technology Stack</h2>
<p>When selecting a technology stack for an AI Agent, some decisions are more straightforward than others. Here are key considerations:</p>

<ul>
  <li>Do you plan to use a single frontier model provider and are comfortable with the associated vendor lock-in?</li>
  <li>Does your agent require durability? Is the use case mission-critical?</li>
  <li>Is the agent’s workflow complex, involving multiple steps, branches, or business logic?</li>
  <li>What level of scalability is required?</li>
  <li>Will the workflow involve interactive steps, such as user approvals?</li>
  <li>Does the agent require long-running conversations over extended periods?</li>
  <li>How much data will be passed into the LLM in each conversation turn?</li>
  <li>Do you require token-level streaming between the LLM and the user, or can responses be delivered all at once?</li>
</ul>

<p>Below is a comparison of leading Agentic AI frameworks, highlighting their strengths and limitations.</p>

<table>
  <thead>
    <tr>
      <th>Framework</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Temporal</strong></td>
      <td>Durable, fault-tolerant workflows</td>
      <td>Steeper learning curve</td>
    </tr>
    <tr>
      <td> </td>
      <td>Polyglot SDKs (Go, Java, Python, .NET)</td>
      <td>Operational overhead</td>
    </tr>
    <tr>
      <td> </td>
      <td>Scales to thousands of concurrent workflows</td>
      <td>May be overkill for simple agents</td>
    </tr>
    <tr>
      <td> </td>
      <td>Supports user interaction within workflows</td>
      <td>Limited payload size &amp; streaming support</td>
    </tr>
    <tr>
      <td> </td>
      <td>Rich observability (history view, metrics, tracing)</td>
      <td> </td>
    </tr>
    <tr>
      <td><strong>LangGraph</strong></td>
      <td>Graph-based orchestration model</td>
      <td>Early-stage project with a smaller community</td>
    </tr>
    <tr>
      <td> </td>
      <td>Tight integration with LLMs like ChatGPT and Claude</td>
      <td>Limited built-in durability</td>
    </tr>
    <tr>
      <td> </td>
      <td>Lightweight: no infrastructure required</td>
      <td>Minimal UI and monitoring tools</td>
    </tr>
    <tr>
      <td> </td>
      <td>Extensible nodes and tool integrations</td>
      <td> </td>
    </tr>
    <tr>
      <td><strong>CrewAI</strong></td>
      <td>Fully managed, low-code platform with drag-and-drop design</td>
      <td>Lack of state management</td>
    </tr>
    <tr>
      <td> </td>
      <td>Hosted infrastructure (zero ops)</td>
      <td>Less flexible, static workflows</td>
    </tr>
    <tr>
      <td> </td>
      <td>Built-in analytics and monitoring</td>
      <td>Limited control over execution</td>
    </tr>
    <tr>
      <td> </td>
      <td>Enterprise features (RBAC, audit logs, SLAs)</td>
      <td>Lacks built-in durability and retries</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>No support for updates or feedback loops</td>
    </tr>
    <tr>
      <td><strong>OpenAI</strong></td>
      <td>Native integration with OpenAI LLMs</td>
      <td>No built-in durability or retry mechanism</td>
    </tr>
    <tr>
      <td> </td>
      <td>Can leverage non-OAI models via LiteLLM</td>
      <td>Limited observability and monitoring</td>
    </tr>
    <tr>
      <td> </td>
      <td>Simplified, code-first agent development</td>
      <td>Vendor lock-in concerns even w/LiteLLM</td>
    </tr>
    <tr>
      <td> </td>
      <td>Automated tool planning and execution</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>Low operational overhead</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h2 id="agentic-airline-assistant">Agentic Airline Assistant</h2>
<p>I built an <a href="https://github.com/ktenzer/airline-ai-agent">Airline AI Agent</a> using Temporal, LangGraph, and CrewAI to evaluate each framework’s strengths and weaknesses.</p>

<p>This AI Airline Agent specializes in searching and booking flights from Los Angeles to select destinations. It handles conversations using an LLM—configured with GPT-4o-mini, though any LLM can be used. It also calls tools such as <code class="language-plaintext highlighter-rouge">find_flights</code> and <code class="language-plaintext highlighter-rouge">book_flight</code>. All examples use a simple ChatUI built with <a href="https://www.gradio.app/">Gradio</a> and <a href="https://www.langchain.com/">LangChain</a> for LLM interaction, configuration, and prompting.</p>

<p>These examples allow you to explore each framework’s capabilities and trade-offs. I’ve aimed to remain as objective as possible.</p>

<h2 id="recommendations">Recommendations</h2>
<p>Based on my experience working with these Agentic frameworks, here are some practical recommendations:</p>

<ul>
  <li><strong>OpenAI Agent SDK</strong>: Best if you’re using OpenAI as main frontier LLM and are comfortable with some vendor lock-in.</li>
  <li><strong>Temporal</strong>: Ideal for any agent, especially if they require durability, fault-tolerance, scalability, and production-grade features. Also supports polyglot development.</li>
  <li><strong>CrewAI</strong>: A good option for simpler agents where durability isn’t critical and operational overhead must be minimal.</li>
  <li><strong>LangGraph</strong>: Suitable for lightweight, code-first agents with tight LLM integration and limited durability needs.</li>
</ul>

<h2 id="summary">Summary</h2>
<p>This article explored Agentic AI Agents, their requirements, and opportunities. We reviewed leading frameworks and compared their strengths and limitations. A simple AI Airline Assistant example demonstrated how each framework can be applied. Finally, practical recommendations were provided to guide framework selection.</p>

<p>(c) 2025 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="AI" /><category term="AI" /><category term="Agentic" /><category term="LLM" /><category term="Workflows" /><category term="Framework" /><category term="Temporal" /><category term="CrewAI" /><category term="LangGraph" /><category term="LangChain" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">AI: The Age of Agentic</title><link href="https://keithtenzer.com/ai/AI_The_Age_of_Agentic/" rel="alternate" type="text/html" title="AI: The Age of Agentic" /><published>2025-04-04T00:00:00+00:00</published><updated>2025-04-04T00:00:00+00:00</updated><id>https://keithtenzer.com/ai/AI_The_Age_of_Agentic</id><content type="html" xml:base="https://keithtenzer.com/ai/AI_The_Age_of_Agentic/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>When I think about AI, I can’t resist and imagine robots doing work for humans. A robot is just hardware, but its brain is software, more specifically a large collection of Agentic AIs - acting autonomously, on behalf of a human, carrying out various tasks with limited or no intervention. If we agree then the age of robots powered by Agentic is here!</p>

<p>Agentic is a broad category within GenAI that can be broken into sub-catagories: interaction vs non-interaction. Interaction involves conversation with a human, while non-interaction can happen without a human even being aware. The key to both and what makes it Agentic, is the use of GenAI coupled with acting on behalf of a human.</p>

<h2 id="agentic-requirements">Agentic Requirements</h2>
<p>Agentic AI requires new approaches and architecture. Message or event-driven architectures don’t really fit well. In addition, new ways of connecting Agentic to data are required. One such standard from Anthropic, called Model Context Protocol <a href="https://www.anthropic.com/news/model-context-protocol">MCP</a> has already started to gain traction. On one hand, Agentic requires focus on a conversation-driven approach, where the clear boundaries and definitions provided by a message or event-driven architecture, simply do not exist. On the other hand, it also couples business processes with GenAI, all of which need durable orchestration to be reliable. These are two distinct approaches and both are somewhat interwoven.</p>

<h3 id="conversation-driven-approach">Conversation-Driven Approach</h3>
<p>Conversations are very verbose and happen in real-time. To handle a conversation of unlimited size over unknown duration, a layer between the human and LLM which can effectively stream tokens is required. In addition, the conversation must be persisted, so that it isn’t lost or forgotten.</p>

<h3 id="non-conservation-driven-approach">Non-Conservation-Driven Approach</h3>
<p>An LLM is a generalist and will always require specialist tools to complete tasks. These tools could be other LLMs/SLMs, Inference, Retrieval-Augmented-Generation, even sub-agents. A tool will often be used in the background, without direction of a human. In addition, such tools will be coupled with business processes to achieve meaningful outcomes.</p>

<p>Lets consider a high-level architecture.
<img src="/assets/2025-04-02/AI_Agentic_Architecture.png" alt="Agentic architecture" /></p>

<h2 id="agentic-with-temporal">Agentic with Temporal</h2>
<p>Choosing the right technology to build Agentic upon is crucial. Orchestration is important, but only orchestration that can support long-running (infinite), while providing state management and durability. Temporal becomes a natural fit for orchestrating Human-to-AI delegation.</p>

<p>Temporal Advantages for Agentic</p>
<ul>
  <li>Support Long-running</li>
  <li>State Management</li>
  <li>Durability</li>
  <li>Resource Throttling/Locking (GPUs)</li>
  <li>Scale</li>
  <li>Visibility</li>
  <li>Programming Model</li>
</ul>

<p>As already discussed there are two aspects to Agentic: Conversation and Tool. Using Temporal, both can be modelled as Workflows.</p>

<p><img src="/assets/2025-04-02/AI_Agentic_with_Temporal.png" alt="Agentic with Temporal" /></p>

<h3 id="conversation">Conversation</h3>
<p>The conversation Workflow will not only keep state (through any kind of failures), but also coordinate sending prompts to LLM, storing conversation text chunks and returning output tokens to the human. Pointers that map to the tokens are used, allowing for storing of the conversation state, directly within the Workflow. In addition, since Temporal Workflows support interaction through Signals or Updates, implementing human-approval for certain LLM related tasks is trivial in an Agentic Workflow.</p>

<h3 id="tool">Tool</h3>
<p>The tool Workflow represents a tool that can accomplish a specific task with a defined spec. Tools of course can be flaky or have low throttling, but Temporal will automatically handle retries. Temporal Workflows are code and can be written in Python using the Temporal SDK. The programming model provides an elegant abstraction for “durable execution” of code and enables developers to leverage any python AI frameworks or libraries, such as LangChain.</p>

<h2 id="summary">Summary</h2>
<p>Agentic AI is an incredibly exciting and rapidly growing space. We are probably only 12-24 months away from personal AI assistants which will help us carry out not only daily tasks, but improve our own capability, in private life and the workplace. Temporal is a great technology choice for building Agentic AI. It provides not only the orchestration capabilities for long-running, but also critical durability needed to ensure Agentic AI is reliable, and can be counted on just like its human counterparts.</p>

<p>(c) 2025 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="AI" /><category term="AI" /><category term="Agentic" /><category term="Temporal" /><category term="LLM" /><category term="MCP" /><summary type="html"><![CDATA[Overview When I think about AI, I can’t resist and imagine robots doing work for humans. A robot is just hardware, but its brain is software, more specifically a large collection of Agentic AIs - acting autonomously, on behalf of a human, carrying out various tasks with limited or no intervention. If we agree then the age of robots powered by Agentic is here!]]></summary></entry><entry><title type="html">webAI: AI on the Edge</title><link href="https://keithtenzer.com/ai/AI_on_the_Edge/" rel="alternate" type="text/html" title="webAI: AI on the Edge" /><published>2025-03-30T00:00:00+00:00</published><updated>2025-03-30T00:00:00+00:00</updated><id>https://keithtenzer.com/ai/AI_on_the_Edge</id><content type="html" xml:base="https://keithtenzer.com/ai/AI_on_the_Edge/"><![CDATA[<h2 id="overview">Overview</h2>
<p>AI has exploded, and if software was eating the world, AI is swallowing software. The assumption has been that AI requires cloud, with vast amounts of Nvidia GPUs. While that is true to build general purpose LLMs, what if those LLMs became smaller built-for-purpose (SLMs), and what if we could leverage AI using edge devices, such as laptops, phones or tablets? What if we could bring AI closer to the human?</p>

<h2 id="advantages-of-edge-ai">Advantages of Edge AI</h2>
<p>Edge AI is exactly that, using general-purpose off-the-shelf hardware, and focusing on specialist or domain-specific SLMs. After all, if I am trying to understand a defect of an airplane part, in the field, I probably don’t need the AI to also know about sports trivia, right?</p>

<p>Where cloud excels at building general-purpose LLMs that are pretty good at everything, it struggles with making it exceptionally good at something very specific and this is where I think Edge AI can really excel. Below are some of the advantages of Edge AI:</p>

<ul>
  <li>Cost Efficiency / Increased ROI</li>
  <li>Enhanced Data Privacy</li>
  <li>Real-time data processing</li>
  <li>Reduced network congestion</li>
</ul>

<h2 id="introducing-webai">Introducing webAI</h2>
<p>As I discovered recently, there is a company focusing on Edge AI, <a href="https://www.webai.com/">webAI</a>. Their approach is to focus on edge devices, laptops, phones, tablets, and bring AI to these devices, without the need to connect to the cloud. WebAI has focused on Apple silicon, and that bet has paid off with recent advancements in the M4 chip, enabling AI, without the complexity of running complex infrastructure, requiring cloud.</p>

<p>WebAI provides a platform for training, inference, and building AI apps, that all run on consumer-grade Apple hardware: phones, tablets, or laptops. WebAI provides a platform with several components: Navigator, Companion, and webAI CLI.</p>

<h3 id="navigator">Navigator</h3>
<p>Navigator provides a drag-and-drop interface, connecting inputs such as cameras, OCR (Optical Character Recognition), OD (Object Detection), allowing users to train AI models and ultimately build specialist AI assistants that are domain specific.</p>

<p>Building an OD application on AI becomes as simple as clicking together a few boxes using navigator.</p>

<p><img src="/assets/2025-03-30/navigator.png" alt="Navigator" /></p>

<p>Clicking open under output preview shows results.
<img src="/assets/2025-03-30/od_output.png" alt="OD Video Output" /></p>

<h4 id="elements">Elements</h4>
<p>Elements are the building blocks of Navigator. An element is a packaging of code, meant to represent a specific function. There are many <a href="https://support.webai.com/hc/en-us/articles/33767303884947-Element-Registry">built-in</a> elements, but one can also create custom elements as well, for virtually anything.</p>

<h4 id="flow">Flow</h4>
<p>A flow is simple a workflow or sequence of elements to perform a trained function and produce an output. Usually it would represent an input, inference element and an output but many different flows can be imagined.</p>

<h4 id="clusters">Clusters</h4>
<p>Clusters are groups of devices on your network running the webAI CLI (worker). These can be any device, as long as it is running on Apple silicon. Clusters allows for pooling together resources for a common purpose.</p>

<h4 id="deployments">Deployments</h4>
<p>A deployment assigns a flow to a cluster or group of devices, so that it is decoupled from running in Navigator, on your local machine. Development or prototyping is done inside navigator and then deployed to a cluster or set of device(s) when ready for further testing and eventually production.</p>

<h3 id="companion">Companion</h3>
<p>Companion allows you to connect to your deployments, running on a cluster, and interface through an AI assistant chat.</p>

<p><img src="/assets/2025-03-30/companion.png" alt="Companion" /></p>

<h3 id="cli">CLI</h3>
<p>The webAI cli is used to run the worker. This is a lightweight process that allows a device to connect to the webAI platform and ultimately receive deployments.</p>

<h2 id="ai-assistant-example">AI Assistant Example</h2>
<p>Lets put everything together, in a real-world use case, and build a specialized assistant, that will be able to answer very domain specific questions, that a generalized LLM may struggle with.</p>

<p>First, I want to make my general-purpose LLM more accurate, providing detailed documentation that will be used in inference. First, a RAG Inference Pipeline will be built in Navigator to generate vector indexing so detailed and specific documentation can be used by the LLM.</p>

<h3 id="rag-inference-pipeline">Rag Inference Pipeline</h3>
<p>To build the RAG inference pipeline, we will need to use four elements from Navigator: OCR, Chunking, Embedding and Vector Indexing.</p>

<p><img src="/assets/2025-03-30/rag_inference_pipeline.png" alt="RAG Inference Pipeline" /></p>

<h4 id="ocr-element">OCR Element</h4>
<p>The OCR element allows for reading our text documents, we can go into settings and define the directory where all the documents we want exist. Here we configured our docs folder.</p>

<h4 id="chunking-element">Chunking Element</h4>
<p>The Chunking element lets us break up the text into smaller pieces, for efficient downstream processing. We didn’t change any defaults.</p>

<h4 id="embedding-element">Embedding Element</h4>
<p>The embedding element creates a mathematical representation of the text, for storing in a vector index. We didn’t change any of the defaults.</p>

<h4 id="vector-indexing-element">Vector Indexing Element</h4>
<p>The Vector Indexing element writes the index into a vector database. Here we simply specified the folder path for writing the vector index. Each run will generate a new folder with that runs vector database.</p>

<h3 id="llm-chat-assistant">LLM Chat Assistant</h3>
<p>Now that we have vector representation of our text data, we can build our AI assistant. To do this we will need to use the following elements from Navigator: API, Embedding, Vector Retrieval, Prompt Templating and LLM Chat.</p>

<p><img src="/assets/2025-03-30/ai_assistant.png" alt="AI Specialist Assistant" /></p>

<h4 id="api-element">API Element</h4>
<p>The API elements communicates with LLM and the user. It takes user input and sends a prompt back to the user. We didn’t change and defaults.</p>

<h4 id="embedding-element-1">Embedding Element</h4>
<p>The Embedding element as mentioned earlier, creates mathematical representation of the data, in this case, the user input. We didn’t change any defaults.</p>

<h4 id="vector-retrieval-element">Vector Retrieval Element</h4>
<p>The Vector Retrieval gets result from vector database. Here we simply provided the folder to where our vector database was created, with the RAG Inference Pipeline.</p>

<h4 id="prompt-templating-element">Prompt Templating Element</h4>
<p>The Prompt Templating puts our retrieved contexts into the prompt template. We didn’t change any defaults.</p>

<h4 id="llm-chat-element">LLM Chat Element</h4>
<p>The LLM Chat element displays a chat interface, allowing user to interact with the pre-configured LLM. You can choose many different types of LLMs. Llama is the default LLM. We didn’t change any defaults.</p>

<h2 id="observations">Observations</h2>
<p>Using Navigator, we can open a chat interface, and start using our specialized assistant. In this case, we uploaded several documents to RAG pipeline. Those documents go into detail about how to setup observability for a Temporal worker. Below I asked a very specific question about observability of a Temporal worker.</p>

<p><img src="/assets/2025-03-30/webai.png" alt="webAI Example" /></p>

<p>As we can see the AI returned a response to our question which was correct, but also cited one of the documents that were part of our RAG Inference Pipeline, as its source of truth.</p>

<p>Now lets compare the same question with ChatGPT 4o. The answer seems very convincing, but it is in fact wrong and a hallucination.</p>

<p><img src="/assets/2025-03-30/chatgpt.png" alt="ChatGPT Example" /></p>

<h2 id="summary">Summary</h2>
<p>Edge AI is a very quickly emerging space in the AI ecosystem. WebAI is at the forefront, enabling not only new use-cases, but democratizing the way we build and ship AI capabilities with its Navigator platform, bringing AI to the edge, using off-the-shelf consumer-grade hardware instead of incredibly expensive cloud-based GPUs. This should greatly increase the speed and impact we get from AI in our daily lives, bringing AI closer, where it can have the most impact. I am incredibly excited to see where Edge AI goes next!</p>

<p>(c) 2025 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="AI" /><category term="Edge AI" /><category term="webAI" /><summary type="html"><![CDATA[Overview AI has exploded, and if software was eating the world, AI is swallowing software. The assumption has been that AI requires cloud, with vast amounts of Nvidia GPUs. While that is true to build general purpose LLMs, what if those LLMs became smaller built-for-purpose (SLMs), and what if we could leverage AI using edge devices, such as laptops, phones or tablets? What if we could bring AI closer to the human?]]></summary></entry><entry><title type="html">Temporal Fundamentals Part VI: Workers</title><link href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers/" rel="alternate" type="text/html" title="Temporal Fundamentals Part VI: Workers" /><published>2025-01-22T00:00:00+00:00</published><updated>2025-01-22T00:00:00+00:00</updated><id>https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers</id><content type="html" xml:base="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.</p>

<ul>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Basics">Temporal Fundamentals Part I: Basics</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Concepts/">Temporal Fundamentals Part II: Concepts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Timeouts/">Temporal Fundamentals Part III: Timeouts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows/">Temporal Fundamentals Part IV: Workflows</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/">Temporal Fundamentals Part V: Workflow Patterns</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers/">Temporal Fundamentals Part VI: Workers</a></li>
</ul>

<h2 id="worker-architecture">Worker Architecture</h2>
<p>Temporal Workers are responsible for Workflow execution. A Workflow is broken down into Workflow and Activity tasks. Workflows and Activities are registered with a Worker and written using the Temporal SDK, in the programming language of choice.</p>

<p><img src="/assets/2025-01-22/worker.png" alt="Temporal Worker" /></p>

<p>Temporal Workers long poll the Temporal service for tasks, perform execution and return results to the Temporal service. Temporal Workers must poll on a specific Temporal Namespace and TaskQueue. You can have N number of Workers and they can be scaled dynamically.</p>

<p><img src="/assets/2025-01-22/worker-service.png" alt="Worker Polling" /></p>

<p>Temporal Workers have a default polling timeout of around 60 seconds. If the timeout is reached, the poll is unsuccessful and a new poll is started. A successful poll, on the other hand, results in a task being matched to a Worker from the matching service.</p>

<h2 id="workflow-and-activity-registration">Workflow and Activity Registration</h2>
<p>Up until this point, we have focused on developing Workflows and Activities. Once our Workflows and Activities are developed, they need to be registered to a Worker. A few things to keep in mind:</p>
<ul>
  <li>Workers can only poll a single TaskQueue and Namespace</li>
  <li>Workers can handle Workflow tasks, Activity tasks or both</li>
  <li>Client connection when using development server or self-host, differs to that of Temporal Cloud, which requires MTLS certificates or API keys.</li>
</ul>

<p>The below code demonstrates how to create a Worker, setting the TaskQueue, as well as registering Workflow and Activity classes. The client can be switch between localhost and Temporal Cloud, depending on environment settings.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">AccountTransferWorker</span> <span class="o">{</span>

    <span class="nd">@SuppressWarnings</span><span class="o">(</span><span class="s">"CatchAndPrintStackTrace"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="kd">final</span> <span class="nc">String</span> <span class="no">TASK_QUEUE</span> <span class="o">=</span> <span class="s">"AccountTransferTaskQueue"</span><span class="o">;</span>

        <span class="nc">WorkerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">WorkerFactory</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="nc">TemporalClient</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>

        <span class="nc">Worker</span> <span class="n">worker</span> <span class="o">=</span> <span class="n">factory</span><span class="o">.</span><span class="na">newWorker</span><span class="o">(</span><span class="no">TASK_QUEUE</span><span class="o">);</span>
        <span class="n">worker</span><span class="o">.</span><span class="na">registerWorkflowImplementationTypes</span><span class="o">(</span><span class="nc">AccountTransferWorkflowImpl</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">worker</span><span class="o">.</span><span class="na">registerWorkflowImplementationTypes</span><span class="o">(</span><span class="nc">AccountTransferWorkflowScenarios</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">worker</span><span class="o">.</span><span class="na">registerActivitiesImplementations</span><span class="o">(</span><span class="k">new</span> <span class="nc">AccountTransferActivitiesImpl</span><span class="o">());</span>

        <span class="n">factory</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Worker started for task queue: "</span> <span class="o">+</span> <span class="no">TASK_QUEUE</span><span class="o">);</span>
    <span class="o">}</span>    
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TemporalClient</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">WorkflowServiceStubs</span> <span class="nf">getWorkflowServiceStubs</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">FileNotFoundException</span><span class="o">,</span> <span class="nc">SSLException</span> <span class="o">{</span>
        <span class="nc">WorkflowServiceStubsOptions</span><span class="o">.</span><span class="na">Builder</span> <span class="n">workflowServiceStubsOptionsBuilder</span> <span class="o">=</span>
                <span class="nc">WorkflowServiceStubsOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">();</span>

        <span class="k">if</span> <span class="o">(!</span><span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getCertPath</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">""</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="s">""</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getKeyPath</span><span class="o">()))</span> <span class="o">{</span>
            <span class="nc">InputStream</span> <span class="n">clientCert</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FileInputStream</span><span class="o">(</span><span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getCertPath</span><span class="o">());</span>
            <span class="nc">InputStream</span> <span class="n">clientKey</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FileInputStream</span><span class="o">(</span><span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getKeyPath</span><span class="o">());</span>
            <span class="n">workflowServiceStubsOptionsBuilder</span><span class="o">.</span><span class="na">setSslContext</span><span class="o">(</span>
                    <span class="nc">SimpleSslContextBuilder</span><span class="o">.</span><span class="na">forPKCS8</span><span class="o">(</span><span class="n">clientCert</span><span class="o">,</span> <span class="n">clientKey</span><span class="o">).</span><span class="na">build</span><span class="o">()</span>
            <span class="o">);</span>
        <span class="o">}</span>

        <span class="nc">String</span> <span class="n">targetEndpoint</span> <span class="o">=</span> <span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getAddress</span><span class="o">();</span>
        <span class="n">workflowServiceStubsOptionsBuilder</span><span class="o">.</span><span class="na">setTarget</span><span class="o">(</span><span class="n">targetEndpoint</span><span class="o">);</span>
        <span class="nc">WorkflowServiceStubs</span> <span class="n">service</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

        <span class="k">if</span> <span class="o">(!</span><span class="nc">ServerInfo</span><span class="o">.</span><span class="na">getAddress</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"localhost:7233"</span><span class="o">))</span> <span class="o">{</span>
            <span class="c1">// if not local server, then use the workflowServiceStubsOptionsBuilder</span>
            <span class="n">service</span> <span class="o">=</span> <span class="nc">WorkflowServiceStubs</span><span class="o">.</span><span class="na">newServiceStubs</span><span class="o">(</span><span class="n">workflowServiceStubsOptionsBuilder</span><span class="o">.</span><span class="na">build</span><span class="o">());</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">service</span> <span class="o">=</span> <span class="nc">WorkflowServiceStubs</span><span class="o">.</span><span class="na">newLocalServiceStubs</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">service</span><span class="o">;</span>
    <span class="o">}</span>
</code></pre></div></div>

<h2 id="deployment">Deployment</h2>
<p>Each Worker is a single process. Typically for Kubernetes, each Worker process would run in its own container/pod. Scaling is done bu increasing replicas, thus creating more pods and worker processes. A single machine can of course run multiple Worker processes. This is also possible within the same pod in Kubernetes.</p>

<p>For Autoscaling Workers, we can use the built-in <a href="https://docs.temporal.io/develop/worker-performance?_gl=1*184hy9r*_gcl_au*MjQwMDcwODI3LjE3Mjk2MTU3ODk.*_ga*MTc3NDEyMDM5Ni4xNzA4OTcwNzU4*_ga_R90Q9SJD3D*MTczMjA1MDg5OS40NzIuMS4xNzMyMDUwOTE3LjAuMC4w#task-queue-metrics">Autoscaler</a> based off TaskQueue metrics or create our own Autoscaler using SDK metrics such as, Workflow/Activity ScheduleToStart latencies.</p>

<p>Example Dockerfile for Java Worker</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use an official Gradle image from the Docker Hub</span>
FROM <span class="nt">--platform</span><span class="o">=</span>linux/amd64 gradle:jdk17-jammy AS build

<span class="c"># Set the working directory</span>
WORKDIR /home/gradle/project

<span class="c"># Copy the Gradle configuration files first for leveraging Docker cache</span>
<span class="c"># and avoid the downloading of dependencies on each build</span>
COPY build.gradle settings.gradle gradlew ./

<span class="c"># Copy the gradle wrapper JAR and properties files</span>
COPY gradle ./gradle

<span class="c"># Copy the source code</span>
COPY ./core ./core

<span class="c"># Now run gradle assemble to download dependencies and build the application</span>
RUN <span class="nb">chmod</span> +x ./gradlew
RUN ./gradlew build

<span class="c"># Use a JDK base image for running the gradle task</span>
FROM amazoncorretto:17-al2-native-headless
WORKDIR /app

<span class="c"># Copy the build output from the builder stage</span>
COPY <span class="nt">--from</span><span class="o">=</span>build /home/gradle/project/build /app/build

<span class="c"># Copy the source code</span>
COPY <span class="nt">--from</span><span class="o">=</span>build /home/gradle/project/core /app/core

<span class="c"># Copy the gradlew and settings files</span>
COPY <span class="nt">--from</span><span class="o">=</span>build /home/gradle/project/gradlew /home/gradle/project/settings.gradle /home/gradle/project/build.gradle /app/
COPY <span class="nt">--from</span><span class="o">=</span>build /home/gradle/project/gradle /app/gradle

<span class="c"># Run the specified task</span>
CMD <span class="o">[</span><span class="s2">"sh"</span>, <span class="s2">"-c"</span>, <span class="s2">"./gradlew -q execute -PmainClass=io.temporal.samples.moneytransfer.AccountTransferWorker"</span><span class="o">]</span>
</code></pre></div></div>

<p>Below shows example of Kubernetes Worker deployment.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-money-transfer-java-worker</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-money-transfer-java-worker</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-money-transfer-java-worker</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-money-transfer-java-worker</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">cicd</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">worker-workflow</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-money-transfer-java-worker:latest</span>
          <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ENCRYPT_PAYLOADS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">ktenzer-test-1.sdvdw.tmprl.cloud:7233</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">ktenzer-test-1.sdvdw</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_MONEYTRANSFER_TASKQUEUE</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">MoneyTransferSampleJava-WaaS</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
              <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
          <span class="na">secret</span><span class="pi">:</span>
            <span class="na">defaultMode</span><span class="pi">:</span> <span class="m">420</span>
            <span class="na">secretName</span><span class="pi">:</span> <span class="s">temporal-tls</span>
</code></pre></div></div>

<h2 id="sdk-metrics">SDK Metrics</h2>
<p>Temporal Workers, if configured, emit important metrics for understanding performance and performing fine-tuning of workers. Workers can be configured to support Prometheus, StatsD and even M3.</p>

<p>An example configuration for Prometheus in Java can be found <a href="https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/metrics/MetricsWorker.java">here</a>.</p>

<p>In Kubernetes, Temporal workers can be scrapped by simply defining a service to expose the metrics endpoint and a service monitor to scrape periodically into Prometheus.</p>

<p>Metrics Service</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-metrics-worker</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">metrics</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">internalTrafficPolicy</span><span class="pi">:</span> <span class="s">Cluster</span>
  <span class="na">ipFamilies</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">IPv4</span>
  <span class="na">ipFamilyPolicy</span><span class="pi">:</span> <span class="s">SingleStack</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">metrics</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8077</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8077</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-metrics-worker</span>
  <span class="na">sessionAffinity</span><span class="pi">:</span> <span class="s">None</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
<span class="na">status</span><span class="pi">:</span>
  <span class="na">loadBalancer</span><span class="pi">:</span> <span class="pi">{}</span>
</code></pre></div></div>

<p>Service Monitor</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">monitoring.coreos.com/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceMonitor</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-metrics-worker</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">metrics-monitor</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">endpoints</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="s">metrics</span>
    <span class="na">interval</span><span class="pi">:</span> <span class="s">15s</span>
    <span class="na">scrapeTimeout</span><span class="pi">:</span> <span class="s">14s</span>
  <span class="na">namespaceSelector</span><span class="pi">:</span>
    <span class="na">matchNames</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">temporal-metrics</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">temporal-metrics-worker</span>
</code></pre></div></div>

<h2 id="worker-tuning">Worker Tuning</h2>
<p>Having Worker and Temporal service (server or Cloud) metrics is a pre-requisite to any tuning or optimization. Before jumping into Worker metrics, it is good to look at Worker Sync/Match Rate. This metric is emitted for the Temporal Service. While it doesn’t show if a worker is well tuned, it does show if workers are able to keep up with the rate of work being generated by the Temporal service. It is recommended to have &gt;= 95% Sync/Match rate. Having a poor Sync/Match rate is an indication that Worker tuning may be required.</p>

<p>Temporal Workers have resource constraints: CPU, Memory and IO.</p>

<h3 id="cpu">CPU</h3>
<p>Workers execute Workflow and Activity tasks concurrently. More work means more CPU is required. First we should identify how much work a Worker should do. Just because we can do more, doesn’t mean that is preferred, for example protecting a downstream service. How much work a Worker can do is a function of CPU resources assigned to worker and the Worker settings:</p>
<ul>
  <li>MaxConcurrentWorkflowTaskExecutionSize</li>
  <li>MaxConcurrentActivityTaskExecutionSize</li>
</ul>

<p>To determine the right settings, we should aim to utilize as much of the CPU as possible without going above 80% utilization. However, when doing so we need to ensure metrics schedule-to-start latencies (time tasks are waiting to be executed) remain low.</p>
<ul>
  <li>activity_schedule_to_start_latency</li>
  <li>workflow_task_schedule_to_start_latency</li>
</ul>

<p>In addition we need to ensure that our Workflow and Activity task slots (TaskExecutionSize) don’t get close to 0 by monitoring the task slots metrics bucket.</p>
<ul>
  <li>worker_task_slots_available {worker_type = WorkflowWorker}</li>
  <li>worker_task_slots_available {worker_type = ActivityWorker}</li>
</ul>

<p>Finally, measuring the task execution latency, can help identify problems with our code or downstream dependencies, especially for CPU intensive operations.</p>
<ul>
  <li>workflow_task_execution_latency</li>
  <li>activity_task_execution_latency</li>
</ul>

<h3 id="memory">Memory</h3>
<p>Workers cache Workflow event histories as an optimization so that Workflow replay (which can be expensive) only happens when there is a failure (Worker dies, etc). However, not having enough memory or cache slots can mean Worker cache is evicted which then causes additional, unneeded Workflow replays. This is controlled by the setting:</p>
<ul>
  <li>MaxCachedWorkflows/StickyWorkflowCacheSize</li>
</ul>

<p>To determine if memory is optimized for caching of Workflows we can look into the cache metrics:</p>
<ul>
  <li>sticky_cache_total_forced_eviction</li>
  <li>sticky_cache_size</li>
  <li>sticky_cache_hit</li>
  <li>sticky_cache_miss</li>
</ul>

<p>These metrics show the size of our cache, the hit/miss ratio and when forced evictions happen. If we see forced evictions and lots of misses, we can increase the cache size, after verifying sufficient free memory. We should aim to stay below 80% memory utilization.</p>

<p>If we are seeing lots of cache misses, looking at replay latency would be recommended.</p>
<ul>
  <li>workflow_task_replay_latency</li>
</ul>

<p>Workflow replay requires downloading the Workflow event history to Worker and then replaying it. This can be improved by tuning CPU but also by reducing event history size, for example by utilizing continue-as-new or segmenting Workflow using Child Workflows.</p>

<h3 id="io">IO</h3>
<p>Workers poll for Workflow and Activity tasks. Each has its own set of pollers. How much work a Worker can grab at any one point, is function of how many pollers are defined in Worker settings and overall network IO:</p>
<ul>
  <li>MaxConcurrentWorkflowTaskPollers</li>
  <li>MaxConcurrentActivityTaskPollers</li>
</ul>

<p>Optimizing requires looking at the following metrics buckets:</p>
<ul>
  <li>num_pollers {poller_type = workflow_task}</li>
  <li>num_pollers {poller_type = activity_task}</li>
  <li>request_latency {namespace, operation}</li>
</ul>

<p>Additional documentation for both SDK (Worker) and Service (Server/Cloud) metrics can be found <a href="https://docs.temporal.io/cloud/metrics/">here</a>.</p>

<h3 id="auto-tuning">Auto Tuning</h3>
<p>The Temporal Worker supports auto-tuning. You can do auto-tuning of task slots based on fixed slot size, resources or even custom. The resource based auto-tuner will for example provide additional slots until it reaches the set limit for CPU and Memory.</p>

<p>Example of resource based auto-tuning for Java Worker setting CPU and Memory utilization to 80%.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WorkerOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">setWorkerTuner</span><span class="o">(</span>
        <span class="nc">ResourceBasedTuner</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">setControllerOptions</span><span class="o">(</span>
                <span class="nc">ResourceBasedControllerOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">(</span><span class="mf">0.8</span><span class="o">,</span> <span class="mf">0.8</span><span class="o">).</span><span class="na">build</span><span class="o">())</span>
            <span class="o">.</span><span class="na">build</span><span class="o">())</span>
    <span class="o">.</span><span class="na">build</span><span class="o">())</span>
</code></pre></div></div>

<p>Additional documentation on Worker performance can be found <a href="https://docs.temporal.io/develop/worker-performance">here</a></p>

<h2 id="data-converter">Data Converter</h2>
<p>The Data Converter is a component of the Temporal SDK, handling serialization and encoding of data from your application, to payloads sent to the Temporal service. It has several use cases:</p>
<ul>
  <li>Encryption / Decryption</li>
  <li>Compression</li>
  <li>Custom Serialization/Deserialization</li>
</ul>

<p><img src="/assets/2025-01-22/data_converter.png" alt="Data Converter" /></p>

<p>The Data Converter is widely used to ensure sensitive data sent into payloads via Workflow or Activity inputs is secure. This is accomplished by allowing developers to define their own encryption codec, using their own keys. The Temporal service never accesses a Worker directly, nor has access to the keys or encryption codec. As such all payloads sent/received to/from the Temporal service are encrypted/decrypted within the Worker. This provides a clean security model with clear areas of responsibility and control.</p>

<p>The Data Converter is configured in the Worker WorkflowClientOptions.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WorkflowClient</span> <span class="n">client</span> <span class="o">=</span>
    <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span>
        <span class="n">service</span><span class="o">,</span>
        <span class="nc">WorkflowClientOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">setDataConverter</span><span class="o">(</span>
                <span class="k">new</span> <span class="nf">CodecDataConverter</span><span class="o">(</span>
                    <span class="nc">DefaultDataConverter</span><span class="o">.</span><span class="na">newDefaultInstance</span><span class="o">(),</span>
                    <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="k">new</span> <span class="nc">CryptCodec</span><span class="o">())))</span>
            <span class="o">.</span><span class="na">build</span><span class="o">());</span>
</code></pre></div></div>

<h2 id="summary">Summary</h2>
<p>The Temporal Worker is where Workflows and Activities are executed. In this article we reviewed the Temporal Worker, including configuration and deployment example for kubernetes. We demonstrated how to configure metrics and approach Worker tuning. Finally, we discussed the Data Converter and how it can be used to secure sensitive payloads.</p>

<p>(c) 2024 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="Temporal" /><category term="Worker" /><category term="Temporal" /><category term="Workflow" /><summary type="html"><![CDATA[Overview This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.]]></summary></entry><entry><title type="html">Temporal Fundamentals Part V: Workflow Patterns</title><link href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/" rel="alternate" type="text/html" title="Temporal Fundamentals Part V: Workflow Patterns" /><published>2024-11-18T00:00:00+00:00</published><updated>2024-11-18T00:00:00+00:00</updated><id>https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns</id><content type="html" xml:base="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.</p>

<ul>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Basics">Temporal Fundamentals Part I: Basics</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Concepts/">Temporal Fundamentals Part II: Concepts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Timeouts/">Temporal Fundamentals Part III: Timeouts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows/">Temporal Fundamentals Part IV: Workflows</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/">Temporal Fundamentals Part V: Workflow Patterns</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers/">Temporal Fundamentals Part VI: Workers</a></li>
</ul>

<p>This article will explore some of the more common Temporal design patterns. There are many reusable solutions within Temporal Workflows that will help speedup development, ensure problems are avoided, improve code readability and get the most out of leveraging Temporal.</p>

<h2 id="workflow-patterns">Workflow Patterns</h2>
<p>Below is a list of patterns covered:</p>
<ul>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#workflow-and-activity-composition">Workflow and Activity Compensation</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#saga">SAGA</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#polling">Polling</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#batch-processing">Batch Processing</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#actor-workflows">Actor Workflows</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#request-and-response">Request and Response</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#retry-and-error-handling">Retry and Error Handling</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#activity-timeouts-and-heartbeats">Activity Timeouts and Heartbeats</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#versioning">Versioning</a></li>
  <li><a href="/temporal/Temporal_Fundamentals_Workflow_Patterns/#cancellation-and-compensation">Cancellation and Compensation</a></li>
</ul>

<p>As we look at specific patterns, we will show both Workflow and Activity code.</p>

<h2 id="workflow-and-activity-composition">Workflow and Activity Composition</h2>
<p>Temporal enables breaking business logic down, into smaller reusable pieces. In Temporal, Activities are components that perform concrete tasks. More specifically, anything that can fail, needs to be inside Activity. Workflows orchestrate Activities, managing their sequence and directing the overall business process to completion.</p>

<p>The below code shows relationship between Workflow and Activity.</p>

<h3 id="activity">Activity</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">GreetingActivities</span> <span class="o">{</span>
    <span class="nd">@ActivityMethod</span>
    <span class="nc">String</span> <span class="nf">composeGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">greeting</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">GreetingActivitiesImpl</span> <span class="kd">implements</span> <span class="nc">GreetingActivities</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">composeGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">greeting</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">greeting</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">name</span> <span class="o">+</span> <span class="s">"!"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="workflow">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">GreetingWorkflow</span> <span class="o">{</span>
    <span class="nd">@WorkflowMethod</span>
    <span class="nc">String</span> <span class="nf">getGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">GreetingWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">GreetingWorkflow</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">GreetingActivities</span> <span class="n">activities</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span><span class="nc">GreetingActivities</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Call an activity within the workflow</span>
        <span class="k">return</span> <span class="n">activities</span><span class="o">.</span><span class="na">composeGreeting</span><span class="o">(</span><span class="s">"Hello"</span><span class="o">,</span> <span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="saga">SAGA</h2>
<p>Distributed SAGAs in Temporal are trivial, and amount to a simple try/catch with compensation. Since Temporal can recover, and resume execution from any failure, we only need to concern ourselves with business logic. This is a big reason why, so many organizations rely on Temporal for mission critical business processes, such as bookings, orders, payments, loans, subscriptions and more.</p>

<p>The below code <a href="https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/bookingsaga">sample</a>, shows how to create a SAGA, and run compensation using a multi-step booking as example.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">bookVacation</span><span class="o">(</span><span class="nc">BookingInfo</span> <span class="n">info</span><span class="o">)</span> <span class="o">{</span>
   <span class="nc">Saga</span> <span class="n">saga</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Saga</span><span class="o">(</span><span class="k">new</span> <span class="nc">Saga</span><span class="o">.</span><span class="na">Options</span><span class="o">.</span><span class="na">Builder</span><span class="o">().</span><span class="na">build</span><span class="o">());</span>
   <span class="k">try</span> <span class="o">{</span>
       <span class="n">saga</span><span class="o">.</span><span class="na">addCompensation</span><span class="o">(</span><span class="nl">activities:</span><span class="o">:</span><span class="n">cancelHotel</span><span class="o">,</span> <span class="n">info</span><span class="o">.</span><span class="na">getClientId</span><span class="o">());</span>
       <span class="n">activities</span><span class="o">.</span><span class="na">bookHotel</span><span class="o">(</span><span class="n">info</span><span class="o">);</span>

       <span class="n">saga</span><span class="o">.</span><span class="na">addCompensation</span><span class="o">(</span><span class="nl">activities:</span><span class="o">:</span><span class="n">cancelFlight</span><span class="o">,</span> <span class="n">info</span><span class="o">.</span><span class="na">getClientId</span><span class="o">());</span>
       <span class="n">activities</span><span class="o">.</span><span class="na">bookFlight</span><span class="o">(</span><span class="n">info</span><span class="o">);</span>

       <span class="n">saga</span><span class="o">.</span><span class="na">addCompensation</span><span class="o">(</span><span class="nl">activities:</span><span class="o">:</span><span class="n">cancelExcursion</span><span class="o">,</span> 
                            <span class="n">info</span><span class="o">.</span><span class="na">getClientId</span><span class="o">());</span>
       <span class="n">activities</span><span class="o">.</span><span class="na">bookExcursion</span><span class="o">(</span><span class="n">info</span><span class="o">);</span>
   <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">TemporalFailure</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
       <span class="n">saga</span><span class="o">.</span><span class="na">compensate</span><span class="o">();</span>
       <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
   <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="polling">Polling</h2>
<p>It is often the case, that Workflows may need to wait for external processing. There different types of polling: frequent, infrequent or even periodic. Each have different implementations.</p>

<h3 id="frequent">Frequent</h3>
<p>Frequent polling should be done inside the Activity, it is very important to Heartbeat as this will be a long running Activity.</p>

<p>The below code <a href="https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/polling/frequent">sample</a>, shows how to implement polling inside an Activity.</p>

<h4 id="activity-1">Activity</h4>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">PollingActivities</span> <span class="o">{</span>
  <span class="nc">String</span> <span class="nf">doPoll</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FrequentPollingActivityImpl</span> <span class="kd">implements</span> <span class="nc">PollingActivities</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TestService</span> <span class="n">service</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">POLL_DURATION_SECONDS</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>

  <span class="kd">public</span> <span class="nf">FrequentPollingActivityImpl</span><span class="o">(</span><span class="nc">TestService</span> <span class="n">service</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">this</span><span class="o">.</span><span class="na">service</span> <span class="o">=</span> <span class="n">service</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">String</span> <span class="nf">doPoll</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">ActivityExecutionContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">Activity</span><span class="o">.</span><span class="na">getExecutionContext</span><span class="o">();</span>

    <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">try</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">service</span><span class="o">.</span><span class="na">getServiceResult</span><span class="o">();</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">TestService</span><span class="o">.</span><span class="na">TestServiceException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// service "down" we can log</span>
      <span class="o">}</span>

      <span class="c1">// heart beat and sleep for the poll duration</span>
      <span class="k">try</span> <span class="o">{</span>
        <span class="n">context</span><span class="o">.</span><span class="na">heartbeat</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ActivityCompletionException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// activity was either cancelled or workflow was completed or worker shut down</span>
        <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
      <span class="o">}</span>
      <span class="n">sleep</span><span class="o">(</span><span class="no">POLL_DURATION_SECONDS</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="kt">void</span> <span class="nf">sleep</span><span class="o">(</span><span class="kt">int</span> <span class="n">seconds</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
      <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">.</span><span class="na">toMillis</span><span class="o">(</span><span class="n">seconds</span><span class="o">));</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">interrupt</span><span class="o">();</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="infrequent">Infrequent</h3>
<p>For infrequent polling, we can simply use Activity retries and the configurable backoff coefficient, increasing time between retries, configured at the Workflow level.</p>

<p>The below code <a href="https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/polling/infrequent">sample</a>, shows how to configure polling using Activity retry options.</p>

<h4 id="workflow-1">Workflow</h4>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">PollingWorkflow</span> <span class="o">{</span>
  <span class="nd">@WorkflowMethod</span>
  <span class="nc">String</span> <span class="nf">exec</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">InfrequentPollingWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">PollingWorkflow</span> <span class="o">{</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">String</span> <span class="nf">exec</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">ActivityOptions</span> <span class="n">options</span> <span class="o">=</span>
        <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
            <span class="c1">// Set activity StartToClose timeout (single activity exec), does not include retries</span>
            <span class="o">.</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">))</span>
            <span class="o">.</span><span class="na">setRetryOptions</span><span class="o">(</span>
                <span class="nc">RetryOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setBackoffCoefficient</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">setInitialInterval</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">60</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">())</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="c1">// create our activities stub and start activity execution</span>
    <span class="nc">PollingActivities</span> <span class="n">activities</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span><span class="nc">PollingActivities</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">options</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">activities</span><span class="o">.</span><span class="na">doPoll</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="batch-processing">Batch Processing</h2>
<p>In Temporal, batch processing is a pattern for executing background operations over a large set of Workflows, all at once.</p>

<p>Typical use cases are:</p>
<ul>
  <li>Bulk Signals/Cancellations/Terminations of Workflows</li>
  <li>Data Analysis or transforming raw data into something useful for modelling</li>
  <li>Data Processing for reporting, warehousing, payroll processing and more</li>
</ul>

<p>Batch processing usually involves grouping data together, or a sliding window. For a large dataset, we would recommend fan-out, using a Child Workflow for every group, and within each Workflow, every item would be processed by an activity. If items can share same retry policy, compensation logic, failure handling, it might make sense to group multiple items into same Activity.</p>

<p>Batch processing can be implemented as a Heartbeating Activity, Iterator or or even Sliding Window.</p>

<p>The below code <a href="https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/batch/iterator">sample</a>, shows a iterator batch use case, where batching is done using Child Workflows.</p>

<h3 id="activity-2">Activity</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">BatchActivities</span> <span class="o">{</span>
    <span class="nd">@ActivityMethod</span>
    <span class="kt">void</span> <span class="nf">processData</span><span class="o">(</span><span class="nc">String</span> <span class="n">data</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">BatchActivitiesImpl</span> <span class="kd">implements</span> <span class="nc">BatchActivities</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processData</span><span class="o">(</span><span class="nc">String</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Processing data: "</span> <span class="o">+</span> <span class="n">data</span><span class="o">);</span>
        <span class="c1">// Add business logic here</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="workflow-2">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">IteratorBatchWorkflow</span> <span class="o">{</span>
  <span class="nd">@WorkflowMethod</span>
  <span class="kt">int</span> <span class="nf">processBatch</span><span class="o">(</span><span class="kt">int</span> <span class="n">pageSize</span><span class="o">,</span> <span class="kt">int</span> <span class="n">offset</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">IteratorBatchWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">IteratorBatchWorkflow</span> <span class="o">{</span>

  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RecordLoader</span> <span class="n">recordLoader</span> <span class="o">=</span>
      <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span>
          <span class="nc">RecordLoader</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
          <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">().</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">5</span><span class="o">)).</span><span class="na">build</span><span class="o">());</span>

  <span class="cm">/** Stub used to continue-as-new. */</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">IteratorBatchWorkflow</span> <span class="n">nextRun</span> <span class="o">=</span>
      <span class="nc">Workflow</span><span class="o">.</span><span class="na">newContinueAsNewStub</span><span class="o">(</span><span class="nc">IteratorBatchWorkflow</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">processBatch</span><span class="o">(</span><span class="kt">int</span> <span class="n">pageSize</span><span class="o">,</span> <span class="kt">int</span> <span class="n">offset</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// Loads a page of records</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">SingleRecord</span><span class="o">&gt;</span> <span class="n">records</span> <span class="o">=</span> <span class="n">recordLoader</span><span class="o">.</span><span class="na">getRecords</span><span class="o">(</span><span class="n">pageSize</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span>
    <span class="c1">// Starts a child per record asynchrnously.</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Promise</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;&gt;</span> <span class="n">results</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="n">records</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">SingleRecord</span> <span class="n">record</span> <span class="o">:</span> <span class="n">records</span><span class="o">)</span> <span class="o">{</span>
      <span class="c1">// Uses human friendly child id.</span>
      <span class="nc">String</span> <span class="n">childId</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">getInfo</span><span class="o">().</span><span class="na">getWorkflowId</span><span class="o">()</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">record</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>
      <span class="nc">RecordProcessorWorkflow</span> <span class="n">processor</span> <span class="o">=</span>
          <span class="nc">Workflow</span><span class="o">.</span><span class="na">newChildWorkflowStub</span><span class="o">(</span>
              <span class="nc">RecordProcessorWorkflow</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
              <span class="nc">ChildWorkflowOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">().</span><span class="na">setWorkflowId</span><span class="o">(</span><span class="n">childId</span><span class="o">).</span><span class="na">build</span><span class="o">());</span>
      <span class="nc">Promise</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="nc">Async</span><span class="o">.</span><span class="na">procedure</span><span class="o">(</span><span class="nl">processor:</span><span class="o">:</span><span class="n">processRecord</span><span class="o">,</span> <span class="n">record</span><span class="o">);</span>
      <span class="n">results</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">result</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="c1">// Waits for all children to complete.</span>
    <span class="nc">Promise</span><span class="o">.</span><span class="na">allOf</span><span class="o">(</span><span class="n">results</span><span class="o">).</span><span class="na">get</span><span class="o">();</span>

    <span class="c1">// No more records in the dataset. Completes the workflow.</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">records</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
      <span class="k">return</span> <span class="n">offset</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// Continues as new with the increased offset.</span>
    <span class="k">return</span> <span class="n">nextRun</span><span class="o">.</span><span class="na">processBatch</span><span class="o">(</span><span class="n">pageSize</span><span class="o">,</span> <span class="n">offset</span> <span class="o">+</span> <span class="n">records</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="actor-workflows">Actor Workflows</h2>
<p>In Temporal there is no constraint of time and as such, Workflows can live forever. Workflows can even encapsulate an entities behavior. Actor Workflows can send/receive messages, maintain state or even create new Actors.</p>

<p>Typical use cases are:</p>
<ul>
  <li>Subscriptions</li>
  <li>Users</li>
  <li>Orders</li>
  <li>Shipments</li>
  <li>And much more…</li>
</ul>

<p>The below code <a href="https://github.com/temporalio/subscription-workflow-project-template-java/tree/main">sample</a>, shows a example of leveraging the Actor pattern to build a subscription Workflow.</p>

<h3 id="workflow-3">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">SubscriptionWorkflow</span> <span class="o">{</span>
  <span class="nd">@WorkflowMethod</span>
  <span class="kt">void</span> <span class="nf">startSubscription</span><span class="o">(</span><span class="nc">Customer</span> <span class="n">customer</span><span class="o">);</span>

  <span class="nd">@SignalMethod</span>
  <span class="kt">void</span> <span class="nf">cancelSubscription</span><span class="o">();</span>

  <span class="nd">@SignalMethod</span>
  <span class="kt">void</span> <span class="nf">updateBillingPeriodChargeAmount</span><span class="o">(</span><span class="kt">int</span> <span class="n">billingPeriodChargeAmount</span><span class="o">);</span>

  <span class="nd">@QueryMethod</span>
  <span class="nc">String</span> <span class="nf">queryCustomerId</span><span class="o">();</span>

  <span class="nd">@QueryMethod</span>
  <span class="kt">int</span> <span class="nf">queryBillingPeriodNumber</span><span class="o">();</span>

  <span class="nd">@QueryMethod</span>
  <span class="kt">int</span> <span class="nf">queryBillingPeriodChargeAmount</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SubscriptionWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">SubscriptionWorkflow</span> <span class="o">{</span>

  <span class="kd">private</span> <span class="kt">int</span> <span class="n">billingPeriodNum</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">subscriptionCancelled</span><span class="o">;</span>
  <span class="kd">private</span> <span class="nc">Customer</span> <span class="n">customer</span><span class="o">;</span>

  <span class="c1">// Define our Activity options: setStartToCloseTimeout: maximum Activity Execution time after it was sent to a Worker</span>
   
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ActivityOptions</span> <span class="n">activityOptions</span> <span class="o">=</span>
      <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">().</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">5</span><span class="o">)).</span><span class="na">build</span><span class="o">();</span>

  <span class="c1">// Define subscription Activities stub</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SubscriptionActivities</span> <span class="n">activities</span> <span class="o">=</span>
      <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span><span class="nc">SubscriptionActivities</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">activityOptions</span><span class="o">);</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">startSubscription</span><span class="o">(</span><span class="nc">Customer</span> <span class="n">customer</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// Set the Workflow customer</span>
    <span class="k">this</span><span class="o">.</span><span class="na">customer</span> <span class="o">=</span> <span class="n">customer</span><span class="o">;</span>

    <span class="c1">// Send welcome email to customer</span>
    <span class="n">activities</span><span class="o">.</span><span class="na">sendWelcomeEmail</span><span class="o">(</span><span class="n">customer</span><span class="o">);</span>

    <span class="c1">// Start the free trial period. User can still cancel subscription during this time</span>
    <span class="nc">Workflow</span><span class="o">.</span><span class="na">await</span><span class="o">(</span><span class="n">customer</span><span class="o">.</span><span class="na">getSubscription</span><span class="o">().</span><span class="na">getTrialPeriod</span><span class="o">(),</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="n">subscriptionCancelled</span><span class="o">);</span>

    <span class="c1">// If customer cancelled their subscription during trial period, send notification email</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">subscriptionCancelled</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">activities</span><span class="o">.</span><span class="na">sendCancellationEmailDuringTrialPeriod</span><span class="o">(</span><span class="n">customer</span><span class="o">);</span>
      <span class="c1">// We have completed subscription for this customer.</span>
      <span class="c1">// Finishing Workflow Execution</span>
      <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// Trial period is over, start billing until</span>
    <span class="c1">// we reach the max billing periods for the subscription</span>
    <span class="c1">// or sub has been cancelled</span>
    <span class="k">while</span> <span class="o">(</span><span class="n">billingPeriodNum</span> <span class="o">&lt;</span> <span class="n">customer</span><span class="o">.</span><span class="na">getSubscription</span><span class="o">().</span><span class="na">getMaxBillingPeriods</span><span class="o">())</span> <span class="o">{</span>

      <span class="c1">// Charge customer for the billing period</span>
      <span class="n">activities</span><span class="o">.</span><span class="na">chargeCustomerForBillingPeriod</span><span class="o">(</span><span class="n">customer</span><span class="o">,</span> <span class="n">billingPeriodNum</span><span class="o">);</span>

      <span class="c1">// Wait 1 billing period to charge customer or if they cancel subscription</span>
      <span class="c1">// whichever comes first</span>
      <span class="nc">Workflow</span><span class="o">.</span><span class="na">await</span><span class="o">(</span><span class="n">customer</span><span class="o">.</span><span class="na">getSubscription</span><span class="o">().</span><span class="na">getBillingPeriod</span><span class="o">(),</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="n">subscriptionCancelled</span><span class="o">);</span>

      <span class="c1">// If customer cancelled their subscription send notification email</span>
      <span class="k">if</span> <span class="o">(</span><span class="n">subscriptionCancelled</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">activities</span><span class="o">.</span><span class="na">sendCancellationEmailDuringActiveSubscription</span><span class="o">(</span><span class="n">customer</span><span class="o">);</span>

        <span class="c1">// We have completed subscription for this customer.</span>
        <span class="c1">// Finishing Workflow Execution</span>
        <span class="k">break</span><span class="o">;</span>
      <span class="o">}</span>

      <span class="n">billingPeriodNum</span><span class="o">++;</span>
    <span class="o">}</span>

    <span class="c1">// if we get here the subscription period is over</span>
    <span class="c1">// notify the customer to buy a new subscription</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">subscriptionCancelled</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">activities</span><span class="o">.</span><span class="na">sendSubscriptionOverEmail</span><span class="o">(</span><span class="n">customer</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">cancelSubscription</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">subscriptionCancelled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateBillingPeriodChargeAmount</span><span class="o">(</span><span class="kt">int</span> <span class="n">billingPeriodChargeAmount</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">customer</span><span class="o">.</span><span class="na">getSubscription</span><span class="o">().</span><span class="na">setBillingPeriodCharge</span><span class="o">(</span><span class="n">billingPeriodChargeAmount</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="nc">String</span> <span class="nf">queryCustomerId</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">customer</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">queryBillingPeriodNumber</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">billingPeriodNum</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">queryBillingPeriodChargeAmount</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">customer</span><span class="o">.</span><span class="na">getSubscription</span><span class="o">().</span><span class="na">getBillingPeriodCharge</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="client">Client</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">SubscriptionWorkflowStarter</span> <span class="o">{</span>

  <span class="c1">// Task Queue name</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TASK_QUEUE</span> <span class="o">=</span> <span class="s">"SubscriptionsTaskQueue"</span><span class="o">;</span>
  <span class="c1">// Base Id for all subscription Workflow Ids</span>
  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">WORKFLOW_ID_BASE</span> <span class="o">=</span> <span class="s">"SubscriptionsWorkflow"</span><span class="o">;</span>

  <span class="c1">// Define our Subscription, Let's say we have a trial period of 10 seconds and a billing period of 10 seconds.In real life this would be much longer. We also set the max billing periods to 24, and the billing cycle charge to 120</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="nc">Subscription</span> <span class="n">subscription</span> <span class="o">=</span>
      <span class="k">new</span> <span class="nf">Subscription</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">),</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">),</span> <span class="mi">24</span><span class="o">,</span> <span class="mi">120</span><span class="o">);</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">WorkflowServiceStubs</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">WorkflowServiceStubs</span><span class="o">.</span><span class="na">newLocalServiceStubs</span><span class="o">();</span>
    <span class="nc">WorkflowClient</span> <span class="n">client</span> <span class="o">=</span> <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">service</span><span class="o">);</span>
    <span class="nc">WorkerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">WorkerFactory</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">client</span><span class="o">);</span>
    <span class="nc">Worker</span> <span class="n">worker</span> <span class="o">=</span> <span class="n">factory</span><span class="o">.</span><span class="na">newWorker</span><span class="o">(</span><span class="no">TASK_QUEUE</span><span class="o">);</span>
 
    <span class="n">worker</span><span class="o">.</span><span class="na">registerWorkflowImplementationTypes</span><span class="o">(</span><span class="nc">SubscriptionWorkflowImpl</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="n">worker</span><span class="o">.</span><span class="na">registerActivitiesImplementations</span><span class="o">(</span><span class="k">new</span> <span class="nc">SubscriptionActivitiesImpl</span><span class="o">());</span>

    <span class="c1">// Start all the Workers registered for a specific Task Queue.</span>
    <span class="n">factory</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>

    <span class="c1">// List of our example customers</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Customer</span><span class="o">&gt;</span> <span class="n">customers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>

    <span class="c1">// Create example customers</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
      <span class="nc">Customer</span> <span class="n">customer</span> <span class="o">=</span>
          <span class="k">new</span> <span class="nf">Customer</span><span class="o">(</span><span class="s">"First Name"</span> <span class="o">+</span> <span class="n">i</span><span class="o">,</span> <span class="s">"Last Name"</span> <span class="o">+</span> <span class="n">i</span><span class="o">,</span> <span class="s">"Id-"</span> <span class="o">+</span> <span class="n">i</span><span class="o">,</span> <span class="s">"Email"</span> <span class="o">+</span> <span class="n">i</span><span class="o">,</span> <span class="n">subscription</span><span class="o">);</span>
      <span class="n">customers</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">customer</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// Create and start a new subscription Workflow for each of the example customers</span>

    <span class="n">customers</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span>
        <span class="n">customer</span> <span class="o">-&gt;</span> <span class="o">{</span>
          <span class="nc">SubscriptionWorkflow</span> <span class="n">workflow</span> <span class="o">=</span>
              <span class="n">client</span><span class="o">.</span><span class="na">newWorkflowStub</span><span class="o">(</span>
                  <span class="nc">SubscriptionWorkflow</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                  <span class="nc">WorkflowOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                      <span class="o">.</span><span class="na">setTaskQueue</span><span class="o">(</span><span class="no">TASK_QUEUE</span><span class="o">)</span>
                      <span class="o">.</span><span class="na">setWorkflowId</span><span class="o">(</span><span class="no">WORKFLOW_ID_BASE</span> <span class="o">+</span> <span class="n">customer</span><span class="o">.</span><span class="na">getId</span><span class="o">())</span>
                      <span class="o">.</span><span class="na">setWorkflowRunTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">5</span><span class="o">))</span>
                      <span class="o">.</span><span class="na">build</span><span class="o">());</span>

          <span class="c1">// Start Workflow Execution (async)</span>
          <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="nl">workflow:</span><span class="o">:</span><span class="n">startSubscription</span><span class="o">,</span> <span class="n">customer</span><span class="o">);</span>
        <span class="o">});</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="request-and-response">Request and Response</h2>
<p>A common pattern in software development is request/response. Temporal provides the Workflow Update primitive, which can not only return data back to the caller but also handle validation. The primitive also supports UpdateWithStart so that a Workflow can be started, do some validation or processing, return back to caller and then continue with anything else asynchronously.</p>

<p>The below code <a href="https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/hello/HelloUpdate.java">sample</a>, uses the Temporal Update primitive to send greetings. If the Update passes validation, the size of the message and queue are returned to the caller, otherwise an error is returned.</p>

<h3 id="workflow-4">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@WorkflowInterface</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">GreetingWorkflow</span> <span class="o">{</span>
    <span class="nd">@WorkflowMethod</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">getGreetings</span><span class="o">();</span>

    <span class="nd">@UpdateMethod</span>
    <span class="kt">int</span> <span class="nf">addGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>

    <span class="nd">@UpdateValidatorMethod</span><span class="o">(</span><span class="n">updateName</span> <span class="o">=</span> <span class="s">"addGreeting"</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">addGreetingValidator</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>

    <span class="nd">@SignalMethod</span>
    <span class="kt">void</span> <span class="nf">exit</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">GreetingWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">GreetingWorkflow</span> <span class="o">{</span>

    <span class="c1">// messageQueue holds up to 10 messages (received from updates)</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">messageQueue</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="mi">10</span><span class="o">);</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">receivedMessages</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="mi">10</span><span class="o">);</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">exit</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HelloActivity</span><span class="o">.</span><span class="na">GreetingActivities</span> <span class="n">activities</span> <span class="o">=</span>
        <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span>
            <span class="nc">HelloActivity</span><span class="o">.</span><span class="na">GreetingActivities</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
            <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">().</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">)).</span><span class="na">build</span><span class="o">());</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">getGreetings</span><span class="o">()</span> <span class="o">{</span>

      <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Block current thread until the unblocking condition is evaluated to true</span>
        <span class="nc">Workflow</span><span class="o">.</span><span class="na">await</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">!</span><span class="n">messageQueue</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">||</span> <span class="n">exit</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">messageQueue</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">exit</span><span class="o">)</span> <span class="o">{</span>
          <span class="k">return</span> <span class="n">receivedMessages</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">String</span> <span class="n">message</span> <span class="o">=</span> <span class="n">messageQueue</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
        <span class="n">receivedMessages</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">message</span><span class="o">);</span>
      <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">addGreeting</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">if</span> <span class="o">(</span><span class="n">name</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="nc">ApplicationFailure</span><span class="o">.</span><span class="na">newFailure</span><span class="o">(</span><span class="s">"Cannot greet someone with an empty name"</span><span class="o">,</span> <span class="s">"Failure"</span><span class="o">);</span>
      <span class="o">}</span>
      <span class="c1">// Updates can mutate workflow state like variables or call activities</span>
      <span class="n">messageQueue</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">activities</span><span class="o">.</span><span class="na">composeGreeting</span><span class="o">(</span><span class="s">"Hello"</span><span class="o">,</span> <span class="n">name</span><span class="o">));</span>
      <span class="c1">// Updates can return data back to the client</span>
      <span class="k">return</span> <span class="n">receivedMessages</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">+</span> <span class="n">messageQueue</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">addGreetingValidator</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">if</span> <span class="o">(</span><span class="n">receivedMessages</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&gt;=</span> <span class="mi">10</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Only 10 greetings may be added"</span><span class="o">);</span>
      <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">exit</span><span class="o">()</span> <span class="o">{</span>
      <span class="n">exit</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div></div>

<h2 id="retry-and-error-handling">Retry and Error Handling</h2>
<p>In Temporal, the default behavior is to progress a Workflow to completion. However, for certain errors, we may want to either change the behavior or even fail the Workflow. A retry policy is applied to Activities and by default it is unlimited. If we limit retries, and those retries are exhausted, the Temporal Workflow will fail. Similarly, depending on error condition, we may want to fail a Workflow explicitly. We can catch and re-throw errors, or even specify certain errors a Non-Retryable in our retry policy.</p>

<p>The below code, shows how to set Non-Retryable error and configure Activity retry options.</p>

<p>Custom Exception</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomNonRetryableException</span> <span class="kd">extends</span> <span class="nc">RuntimeException</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="nf">CustomNonRetryableException</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">message</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="activity-3">Activity</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">SampleActivity</span> <span class="o">{</span>
    <span class="nd">@ActivityMethod</span>
    <span class="nc">String</span> <span class="nf">performTask</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">CustomNonRetryableException</span><span class="o">,</span> <span class="nc">Exception</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SampleActivityImpl</span> <span class="kd">implements</span> <span class="nc">SampleActivity</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">performTask</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">CustomNonRetryableException</span> <span class="o">{</span>
        <span class="c1">// Example logic that may fail</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">someCondition</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">CustomNonRetryableException</span><span class="o">(</span><span class="s">"This error is non-retryable"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="s">"Task completed"</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">someCondition</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// Implement your failure condition logic</span>
        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="workflow-5">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@WorkflowInterface</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">SampleWorkflow</span> <span class="o">{</span>
    <span class="nd">@WorkflowMethod</span>
    <span class="nc">String</span> <span class="nf">runSample</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">SampleWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">SampleWorkflow</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SampleActivities</span> <span class="n">activities</span> <span class="o">=</span>
        <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span>
            <span class="nc">SampleActivities</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
            <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
                <span class="o">.</span><span class="na">setRetryOptions</span><span class="o">(</span>
                    <span class="nc">RetryOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">setInitialInterval</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
                        <span class="o">.</span><span class="na">setDoNotRetry</span><span class="o">(</span><span class="nc">CustomNonRetryableException</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">());</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">runSample</span><span class="o">()</span> <span class="o">{</span>
      <span class="n">activities</span><span class="o">.</span><span class="na">performTask</span><span class="o">();</span>
    <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div></div>

<h2 id="activity-timeouts-and-heartbeats">Activity Timeouts and Heartbeats</h2>
<p>We have already discussed the various timeouts in depth, however equally important, especially for longer running Activities are Heartbeats. Using a Heartbeat allows for Activity to let Temporal service know it is ok, and when it isn’t, we can re-schedule Activities, without waiting the entire ScheduleToClose or StartToClose timeout. In addition, you can also return information in the Heartbeat details, which can be very helpful for ensuring an Activity continue where it left off, regardless of failures.</p>

<p>The below code, shows how we can process a file, periodically Heartbeating and returning the line we are on, so things can be continued. In addition, both the StartToClose and heartbeat timeout are set in our Activity options.</p>

<h3 id="activity-4">Activity</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">FileProcessingActivity</span> <span class="o">{</span>
    <span class="nd">@ActivityMethod</span>
    <span class="kt">void</span> <span class="nf">processLargeFile</span><span class="o">(</span><span class="nc">String</span> <span class="n">fileName</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FileProcessingActivityImpl</span> <span class="kd">implements</span> <span class="nc">FileProcessingActivity</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processLargeFile</span><span class="o">(</span><span class="nc">String</span> <span class="n">fileName</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ActivityExecutionContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">Activity</span><span class="o">.</span><span class="na">getExecutionContext</span><span class="o">();</span>
        <span class="kt">int</span> <span class="n">totalLines</span> <span class="o">=</span> <span class="mi">100</span><span class="o">;</span>  <span class="c1">// Assume reading 100 lines for demonstration</span>

        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">line</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">line</span> <span class="o">&lt;</span> <span class="n">totalLines</span><span class="o">;</span> <span class="n">line</span><span class="o">++)</span> <span class="o">{</span>
            <span class="c1">// Do some processing here...</span>

            <span class="c1">// Heartbeat every 10 lines</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">line</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">context</span><span class="o">.</span><span class="na">heartbeat</span><span class="o">(</span><span class="n">line</span><span class="o">);</span>
            <span class="o">}</span>

            <span class="k">try</span> <span class="o">{</span>
                <span class="c1">// Simulate some processing time</span>
                <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">100</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Activity interrupted"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="workflow-6">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">FileProcessingWorkflow</span> <span class="o">{</span>
    <span class="nd">@WorkflowMethod</span>
    <span class="kt">void</span> <span class="nf">processFile</span><span class="o">(</span><span class="nc">String</span> <span class="n">fileName</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FileProcessingWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">FileProcessingWorkflow</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">FileProcessingActivity</span> <span class="n">activities</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">FileProcessingWorkflowImpl</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">activities</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span><span class="nc">FileProcessingActivity</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">setTaskQueue</span><span class="o">(</span><span class="s">"file-processing-task-queue"</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
                        <span class="o">.</span><span class="na">setHeartbeatTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">30</span><span class="o">))</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processFile</span><span class="o">(</span><span class="nc">String</span> <span class="n">fileName</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">activities</span><span class="o">.</span><span class="na">processLargeFile</span><span class="o">(</span><span class="n">fileName</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="versioning">Versioning</h2>
<p>Due to the requirement, that Workflows be deterministic, Workflow versioning, and choosing the right versioning strategy, are critical. Temporal offers several versioning strategies:</p>
<ul>
  <li>Workflow Definition</li>
  <li>TaskQueue</li>
  <li>Patch</li>
</ul>

<h3 id="workflow-definition">Workflow Definition</h3>
<p>Versioning strategy that involves versioning the Workflow definition file (MyWorkflowV1, MyWorkflowV2, etc). It involves creating new set of Workers for every version and updating both the Workflow starter as well as Worker. The V2 Workers handle V2 Workflows and once all V1 Workflows are complete they can be shutdown. This versioning strategy works best for short-lived workflows that don’t have a lot of changes or versions.</p>

<h3 id="taskqueue">TaskQueue</h3>
<p>Versioning strategy that involves versioning Workflow TaskQueue (MyTaskQueueV1, MyTaskQueueV2, etc). It involves creating new set of Workers for every version and updating both the Workflow starter as well as Worker. The V2 Workers handle V2 Workflows and once all V1 Workflows are complete they can be shutdown. This versioning strategy works best for short-lived Workflows that have a lot of changes or versions.</p>

<h3 id="patch">Patch</h3>
<p>Versioning strategy that involves versioning Workflow code and branching within Workflow code. Temporal SDK provides the <code class="language-plaintext highlighter-rouge">Workflow.getVersion</code> API which is used to set version and branch code. For example, below the <code class="language-plaintext highlighter-rouge">Workflow.getVersionin</code> API
sets the version to 1 in Workflow event history, and then is used to check if Workflow is the default version (our un-versioned or original Workflow code) and if it isn’t then it will execute our new, updated Activity.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">version</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">getVersion</span><span class="o">(</span><span class="s">"checksumAdded"</span><span class="o">,</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">DEFAULT_VERSION</span><span class="o">,</span> <span class="mi">1</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">version</span> <span class="o">==</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">DEFAULT_VERSION</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">activities</span><span class="o">.</span><span class="na">upload</span><span class="o">(</span><span class="n">args</span><span class="o">.</span><span class="na">getTargetBucketName</span><span class="o">(),</span> <span class="n">args</span><span class="o">.</span><span class="na">getTargetFilename</span><span class="o">(),</span> <span class="n">processedName</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
    <span class="kt">long</span> <span class="n">checksum</span> <span class="o">=</span> <span class="n">activities</span><span class="o">.</span><span class="na">calculateChecksum</span><span class="o">(</span><span class="n">processedName</span><span class="o">);</span>
    <span class="n">activities</span><span class="o">.</span><span class="na">uploadWithChecksum</span><span class="o">(</span>
        <span class="n">args</span><span class="o">.</span><span class="na">getTargetBucketName</span><span class="o">(),</span> <span class="n">args</span><span class="o">.</span><span class="na">getTargetFilename</span><span class="o">(),</span> <span class="n">processedName</span><span class="o">,</span> <span class="n">checksum</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This versioning strategy works best for long-running Workflows, where the code branches can be maintained and removed as to not end up with an unmanageable amount of code branching.</p>

<h2 id="cancellation-and-compensation">Cancellation and Compensation</h2>
<p>Cancellation and Compensation are very important patterns in Temporal. They ensure failures are handled gracefully, while maintaining a consistent state. Cancellation allows for halting Workflow execution, while Compensation executes a predefined list of actions, in order to undo any changes that may have ocurred, during Workflow execution.</p>

<p>In Temporal, Compensation is just a simple try/catch, where upon error, any changes will be undone.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">{</span>
    <span class="n">order</span> <span class="o">=</span> <span class="n">activities</span><span class="o">.</span><span class="na">processOrder</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ActivityFailure</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// Compensate Order (rollback)</span>
    <span class="n">order</span> <span class="o">=</span> <span class="n">activities</span><span class="o">.</span><span class="na">compensateOrder</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
    <span class="k">throw</span> <span class="nc">ApplicationFailure</span><span class="o">.</span><span class="na">newNonRetryableFailure</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span> <span class="s">"OrderFailed"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Cancellation can be implemented using a CancellationScope. The scope can be attached or even detached from Workflow. When user (through API, CLI, UI) cancels a Workflow, that cancellation is passed into Workflow, and then acted upon (if a scope is implemented). This gives us a chance to perform any compensation and also important cleanup steps, before terminating a Workflow.</p>

<h3 id="activity-5">Activity</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ActivityInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">OrderActivities</span> <span class="o">{</span>
    <span class="nd">@ActivityMethod</span>
    <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">);</span>

    <span class="nd">@ActivityMethod</span>
    <span class="kt">void</span> <span class="nf">compensateOrder</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">OrderActivitiesImpl</span> <span class="kd">implements</span> <span class="nc">OrderActivities</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processOrder</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Simulate a long operation that can be canceled</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Processing order: "</span> <span class="o">+</span> <span class="n">orderId</span><span class="o">);</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">10000</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Order processing was interrupted."</span><span class="o">);</span>
            <span class="k">throw</span> <span class="n">e</span><span class="o">;</span> <span class="c1">// Rethrow to allow cancellation</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">compensateOrder</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Compensating order: "</span> <span class="o">+</span> <span class="n">orderId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="workflow-7">Workflow</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WorkflowInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">CancellationWorkflow</span> <span class="o">{</span>
    <span class="nd">@WorkflowMethod</span>
    <span class="nc">String</span> <span class="nf">run</span><span class="o">();</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CancellationWorkflowImpl</span> <span class="kd">implements</span> <span class="nc">CancellationWorkflow</span> <span class="o">{</span>
    <span class="c1">// If cancellation received we should not wait for activity but try to cancel it</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderActivities</span> <span class="n">activities</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">newActivityStub</span><span class="o">(</span>
            <span class="nc">OrderActivities</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
            <span class="nc">ActivityOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setStartToCloseTimeout</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">5</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">setCancellationType</span><span class="o">(</span><span class="nc">ActivityCancellationType</span><span class="o">.</span><span class="na">TRY_CANCEL</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">()</span>
    <span class="o">);</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">CancellationScope</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">Workflow</span><span class="o">.</span><span class="na">newCancellationScope</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">activities</span><span class="o">.</span><span class="na">processOrder</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ActivityFailure</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// Check if the failure is due to cancellation</span>
                <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Activity was canceled: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
                <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">});</span>

        <span class="n">scope</span><span class="o">.</span><span class="na">run</span><span class="o">();</span>

        <span class="c1">// If the scope is canceled, run the compensation activity</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">scope</span><span class="o">.</span><span class="na">isCancelled</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">activities</span><span class="o">.</span><span class="na">compensateOrder</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="summary">Summary</h2>
<p>In this article we discussed some of the common Temporal Workflow patterns. These patterns provide reusable solutions to common problems, encountered when building Temporal Workflows. Finally, we provided code samples, illustrating the various Workflow pattern implementations in detail.</p>

<p>(c) 2024 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="Temporal" /><category term="Workflow Patterns" /><category term="Design Patterns" /><category term="Temporal" /><category term="Workflow" /><category term="Patterns" /><summary type="html"><![CDATA[Overview This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.]]></summary></entry><entry><title type="html">Temporal Fundamentals Part IV: Workflows</title><link href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows/" rel="alternate" type="text/html" title="Temporal Fundamentals Part IV: Workflows" /><published>2024-10-31T00:00:00+00:00</published><updated>2024-10-31T00:00:00+00:00</updated><id>https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows</id><content type="html" xml:base="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.</p>

<ul>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Basics">Temporal Fundamentals Part I: Basics</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Concepts/">Temporal Fundamentals Part II: Concepts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Timeouts/">Temporal Fundamentals Part III: Timeouts</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflows/">Temporal Fundamentals Part IV: Workflows</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workflow_Patterns/">Temporal Fundamentals Part V: Workflow Patterns</a></li>
  <li><a href="https://keithtenzer.com/temporal/Temporal_Fundamentals_Workers/">Temporal Fundamentals Part VI: Workers</a></li>
</ul>

<p>This article will focus on understanding Temporal Workflow design recommendations. It will explore general design practice and also get into specific recommendations for Temporal primitives.</p>

<p><img src="/assets/2024-10-31/workflow.png" alt="Temporal Workflow" /></p>

<h2 id="limits">Limits</h2>
<p>Temporal achieves its breathtaking scale, by its ability to execute billions upon billions of Workflows. However, each Workflow has its <a href="https://docs.temporal.io/cloud/limits#programming-model-level">limits</a> and it is important to understand them, when doing Workflow design.</p>

<h2 id="determinism">Determinism</h2>
<p>Workflows must be deterministic, Workflows can and will be replayed. Every Workflow replay must follow the same code path, for events that already ocurred in its execution history.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Use static code analyzers if available for SDK, which check for common sources of Non-Determinism.</li>
  <li>Workflows should be replayed using the SDK replayer, with previous version event history, to ensure new code doesn’t break determinism.</li>
  <li>Versioning Workflows should be done using appropriate versioning strategy.</li>
</ul>

<h2 id="workflowids">WorkflowIds</h2>
<p>WorkflowIds are unique and often they represent a name or identifier, that is important to the use case. Workflows have both, a WorkflowId, and a RunId. While the WorkflowId only exists once, each run including retry or continue-as-new of workflow, creates a new RunId.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Use WorkflowIds as idempotency keys when Workflow may be started more than once.</li>
  <li>Use appropriate WorkflowId <a href="https://docs.temporal.io/workflows#workflow-id-reuse-policy">reuse</a> policy</li>
  <li>Do not rely on Workflow RunId for business logic choices as it can change during Workflow execution, due to retry or continue-as-new.</li>
</ul>

<h2 id="workflow-event-history">Workflow Event History</h2>
<p>A Workflow is comprised of a series of tasks. Workflow tasks execute Workflow code and Activity tasks execute Activity code. All of these tasks and their states (Scheduled, Started, Completed) are stored in the Workflow event history. A single Workflow event history has limits on number of events (50,000) and size (50 MB).</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Ensure Workflow limits are not reached, otherwise Workflow will be terminated.</li>
  <li>Partition work across Workflows as opposed to having a single monolithic Workflow. In Temporal, you can have billions of running workflows, use them.</li>
  <li>Long running Workflows should use Continue-As-New within Workflow to continue with fresh event history or Child Workflows, to partition event history across many workflows.</li>
</ul>

<h2 id="workflow-timeouts">Workflow Timeouts</h2>
<p>Workflow tasks have a timeout of 10 seconds by default. In addition, the SDK has a deadlock detector which is 1 second. Workflow execution and run timeouts default to infinite.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Do not change defaults unless you have a very specific reason.</li>
  <li>It is very important to not block Workflow code due to these important timeouts, as that will block and potentially cause stuck Workflows.</li>
</ul>

<h2 id="workflow-failure">Workflow Failure</h2>
<p>Workflow code that throws a non-Temporal failure will cause Workflow task failure. By default Workflow task will be retried every 10 seconds, infinity until it succeeds.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Properly catch and throw exceptions. Can throw Temporal Application error if it is decided to fail Workflow.</li>
  <li>Don’t use Workflow retry policy, a Workflow should not fail due to intermittent issues.</li>
</ul>

<h2 id="workflow-primitives">Workflow Primitives</h2>
<h3 id="activities">Activities</h3>
<p>A Temporal Activity is used to call external services or APIs. Anything that can fail must be an Activity. Anything that could be non-Deterministic must also be done in an Activity.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Activities should be idempotent. In Temporal you can have at most once Activity execution (0 or 1) or at least once Activity execution (1 or more).</li>
  <li>Long running activities (more than few minutes) should always Heartbeat.</li>
  <li>Ensure proper Activity timeout for use case (StartToClose or ScheduleToClose).</li>
  <li>Ensure proper failure handling, retry policy, non-retryable errors and compensation is appropriate for use case.</li>
  <li>Activity payloads have limit of 2 MB. if payloads, inputs/outputs of Activity could be more, consider passing by reference. Another option could be compression using Data Converter.</li>
  <li>For polling, if frequent perform polling within activity with iterator. If infrequent perform polling using Activity retry. You can also consider Async completion approach, which allows Activity function to return without completing it.</li>
  <li>Use Local Activity for very short-lived Activities, where latency is important. For example, database write. Local Activities run under a Workflow task and cannot Heartbeat.</li>
</ul>

<h3 id="child-workflows">Child Workflows</h3>
<p>Temporal Workflows can partition themselves and create a Child Workflow. This creates a relationship between the parent and the child. Child Workflows can also create other Child Workflows for which they become a parent. A parent and child Workflow have relationship, and the Parent-Close-Policy determines what happens to a Child Workflow if its parent completes, fails or is timed out.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Use Child Workflows to partition into smaller chunks in order to stay within Workflow event history limits.</li>
  <li>Use Child Workflows when lifecycle varies between Workflows (order vs shipment), to breakup and keep Workflow manageable, potentially when multiple teams are involved in use case.</li>
  <li>Do not use Child Workflows to organize code, use programming language for that. Child Workflows will result in more events and actions so they are more expensive than just using activities.</li>
</ul>

<h3 id="signal">Signal</h3>
<p>A Signal is used to update or mutate a Workflow. This is one of the ways Workflows can be interacted with externally, either from another Workflow or the Temporal client. Signals have important behavior characteristics, they are fire and forget, but do have an order guarantee.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Don’t send many Signals per second for extended periods of time. As Signals are buffered, they must be processed prior to a Continue-As-New, which can result in that not happening, and as a result the event history reaching it’s limit.</li>
  <li>A Workflow is limited to 10,000 Signals received.</li>
  <li>Not recommended to have Signal call Activity, should limit scope of Signal handler to updating Workflow state and let Workflow code react to state changes.</li>
</ul>

<h3 id="update">Update</h3>
<p>Similar to Signals, Update is used to mutate a Workflow and allows Workflows to be interacted with externally. Where it differs from Signal is Update can return a response to the client/caller and do validation. If validation for example is rejected, the update would fail and the is error returned to the client/caller. With Signals, since they are fire and forget, you can easily flood or overwhelm a Workflow if you aren’t careful.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Updates that are rejected are not recorded in the event history.</li>
</ul>

<h3 id="query">Query</h3>
<p>In Temporal, any data structure maintained in the Workflow, can be exposed externally, using a Query. A Query is an asynchronous operation used to get the state of a Workflow execution. Queries work on both running and completed Workflows. A worker is required to respond to Query. If no workers are running, the Query call will fail.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Queries should never mutate state of Workflow, and should be read-only.</li>
  <li>It is not recommended to continuously poll using a Query and instead use more efficient patterns.</li>
  <li>Queries are not recorded in the event history.</li>
</ul>

<h3 id="timer">Timer</h3>
<p>A Timer in Temporal is a durable sleep, maintained by the Temporal service. Timers are used to delay execution within a Workflow, or make business logic decisions based on time. For example, failing a Workflow that doesn’t complete in X time, or cancelling an Activity that doesn’t complete in Y time.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Never use the programming language sleep in Workflow, instead use Workflow.timer to sleep Workflow.</li>
  <li>When using timers for business logic decisions, if timer doesn’t fire it should be properly cancelled so it is reflected in event history.</li>
  <li>Use timers to cancel and fail Workflows that run too long, instead of Workflow timeout.</li>
</ul>

<h3 id="continue-as-new">Continue-As-New</h3>
<p>The Continue-As-New primitive in Temporal allows for continuing Workflow, with a fresh or new event history. This is useful for long-running Workflows to prevent reaching event history limits. Continue-As-New allows for passing Workflow state from current runId/execution to a new one. As such, it can also be used for Workflow migration, as well as, other such use cases where passing Workflow state, and continuing in a new Workflow is advantageous.</p>

<p><strong>Recommendations</strong></p>
<ul>
  <li>Ensure the required state is being passed in to Continue-As-New and nothing is missing.</li>
  <li>Allow Temporal SDK to automatically Continue-As-New, when it thinks it should, for avoiding event history limits.</li>
</ul>

<h2 id="summary">Summary</h2>
<p>In this article we discussed Temporal Workflow design principles. We explained and also provided recommendations for Temporal Workflow primitives used to build Temporal Workflows.</p>

<p>(c) 2024 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="Temporal" /><category term="Temporal" /><category term="Durable Execution" /><category term="Replay" /><category term="Determinism" /><category term="Workflow Orchestration" /><category term="Workflow as Code" /><category term="Workflow" /><summary type="html"><![CDATA[Overview This is a six part series focused on Temporal fundamentals. It represents, in my words, what I have learned along the way and what I would’ve like to know on day one.]]></summary></entry><entry><title type="html">Hello Nexus: More than a Abstraction</title><link href="https://keithtenzer.com/temporal/Hello_Nexus_More_than_a_Abstraction/" rel="alternate" type="text/html" title="Hello Nexus: More than a Abstraction" /><published>2024-10-18T00:00:00+00:00</published><updated>2024-10-18T00:00:00+00:00</updated><id>https://keithtenzer.com/temporal/Hello_Nexus_More_than_a_Abstraction</id><content type="html" xml:base="https://keithtenzer.com/temporal/Hello_Nexus_More_than_a_Abstraction/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>Temporal announced Nexus during <a href="https://temporal.io/blog/unlock-new-possibilities-with-product-updates-from-replay-2024">Replay 2024</a>. Nexus has been a feature and capability in the works for several years. Without Nexus, Temporal developers didn’t have a lot of options for executing workflows or workflow primitives across namespace boundaries. There was no easy way to provide an API contract, and as such Workflows became a somewhat, leaky abstraction. Futhermore, beyond activities which were sometime overkill, developers did not have a way to make simple service calls, durably.</p>

<h2 id="nexus-feature">Nexus Feature</h2>
<p>Nexus is a groundbreaking feature and will take Temporal to the next level. It will fundamentally change, and open up new ways to build Temporal-driven applications. Nexus effectively removes the last few limitations that existed in Temporal, while also added a new way to call simple services durably.</p>

<p>Nexus enables:</p>
<ul>
  <li>Cross Namespace Communications</li>
  <li>API Service Contract for Workflows and Workflow primitives</li>
  <li>Ability to call simple services durably, where Workflow and Activity may have been overkill</li>
  <li>Better team or domain boundaries</li>
</ul>

<p>Multiple teams can now build domain services, backed by Temporal Workflows, which can be consumed or interacted with, in a variety of ways, using Nexus endpoints.</p>

<h2 id="how-nexus-works">How Nexus Works</h2>
<p>Nexus performs low-level RPC operations and provides an integrated SDK experience, enabling developers to create a handler and then invoke that handler from within a Workflow.</p>

<p>Once a Nexus endpoint receives a operation request, it will match it to a worker that implements the handler within the Namespace.</p>

<p>When creating a Nexus endpoint, provide a service name, Namespace and allowed Namespaces, which can call the endpoint. Optionally,  a description can be provided as markdown.</p>

<p><img src="/assets/2024-10-21/nexus_create.png" alt="Nexus Endpoint" /></p>

<h2 id="nexus-examples">Nexus Examples</h2>
<p>Nexus is used to call a Workflow, Workflow primitive (Signal/Update/Query) or even another simple service directly. This example shows how to create a Nexus endpoint for a Workflow, and execute that Workflow using the Nexus endpoint in the Go SDK.</p>

<h3 id="creating-nexus-service">Creating Nexus Service</h3>
<p>First, create a service for the Nexus endpoint. Nexus allows developers to define API contracts. The below snippet, shows how to create Nexus service and define service inputs or outputs.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">ServiceName</span> <span class="o">=</span> <span class="s">"myNexusEndpoint"</span>
<span class="k">const</span> <span class="n">OperationName</span> <span class="o">=</span> <span class="s">"my-op"</span>

<span class="k">type</span> <span class="n">Input</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Item</span>  <span class="n">Item</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">Output</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">Message</span> <span class="kt">string</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="creating-nexus-handler">Creating Nexus Handler</h3>
<p>Next, create a Nexus handler. There are two methods to use for creating a handler. For calling a Workflow use <code class="language-plaintext highlighter-rouge">NewWorkflowRunOperation</code> otherwise use <code class="language-plaintext highlighter-rouge">NewSyncOperation</code>.</p>

<p>As of today, Nexus operations must return something and do not support nil as a return. The below snippet, shows how to create a handler to run a Workflow.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="n">NexusWorkflowOperation</span> <span class="o">=</span> <span class="n">temporalnexus</span><span class="o">.</span><span class="n">NewWorkflowRunOperation</span><span class="p">(</span>
	<span class="n">app</span><span class="o">.</span><span class="n">OperationName</span><span class="p">,</span>
	<span class="n">workflows</span><span class="o">.</span><span class="n">MyWorkflow</span><span class="p">,</span>
	<span class="k">func</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">input</span> <span class="n">app</span><span class="o">.</span><span class="n">Input</span><span class="p">,</span> <span class="n">soo</span> <span class="n">nexus</span><span class="o">.</span><span class="n">StartOperationOptions</span><span class="p">)</span> <span class="p">(</span><span class="n">client</span><span class="o">.</span><span class="n">StartWorkflowOptions</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">client</span><span class="o">.</span><span class="n">StartWorkflowOptions</span><span class="p">{</span><span class="n">ID</span><span class="o">:</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"workflow-%v"</span><span class="p">,</span> <span class="n">input</span><span class="o">.</span><span class="n">Item</span><span class="o">.</span><span class="n">Id</span><span class="p">)},</span> <span class="no">nil</span>
	<span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="registering-nexus-with-worker">Registering Nexus with Worker</h3>
<p>As with Temporal Workflows and activities, Nexus services must be registered with a worker. The below snippet, shows how to register a Nexus service with a worker.</p>

<p>Note: you will also need to register any Workflows or Activities that the Nexus operation should trigger.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">service</span> <span class="o">:=</span> <span class="n">nexus</span><span class="o">.</span><span class="n">NewService</span><span class="p">(</span><span class="n">app</span><span class="o">.</span><span class="n">ServiceName</span><span class="p">)</span>
<span class="n">err</span> <span class="o">=</span> <span class="n">service</span><span class="o">.</span><span class="n">Register</span><span class="p">(</span><span class="n">handler</span><span class="o">.</span><span class="n">NexusWorkflowOperation</span><span class="p">)</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
	<span class="n">log</span><span class="o">.</span><span class="n">Fatalln</span><span class="p">(</span><span class="s">"Unable to register operations"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">w</span><span class="o">.</span><span class="n">RegisterNexusService</span><span class="p">(</span><span class="n">service</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="calling-a-nexus-from-workflow">Calling a Nexus from Workflow</h3>
<p>Now that we have defined a Nexus service, handler and registered it with a worker, we can call it from a Workflow. The below snippet, shows how to execute a Nexus Operation and provide back an Operation Id.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="n">op</span> <span class="n">workflow</span><span class="o">.</span><span class="n">NexusOperationExecution</span>
<span class="n">service</span> <span class="o">:=</span> <span class="n">workflow</span><span class="o">.</span><span class="n">NewNexusClient</span><span class="p">(</span><span class="s">"myNexusEndpoint"</span><span class="p">,</span> <span class="n">app</span><span class="o">.</span><span class="n">ServiceName</span><span class="p">)</span>

<span class="n">nf</span> <span class="o">:=</span> <span class="n">service</span><span class="o">.</span><span class="n">ExecuteOperation</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">app</span><span class="o">.</span><span class="n">OperationName</span><span class="p">,</span> <span class="n">Input</span><span class="p">,</span> <span class="n">workflow</span><span class="o">.</span><span class="n">NexusOperationOptions</span><span class="p">{})</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">nf</span>

<span class="n">nf</span><span class="o">.</span><span class="n">GetNexusOperationExecution</span><span class="p">()</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">op</span><span class="p">)</span>
<span class="n">logger</span><span class="o">.</span><span class="n">Info</span><span class="p">(</span><span class="s">" Started Nexus Operation: "</span> <span class="o">+</span> <span class="n">op</span><span class="o">.</span><span class="n">OperationID</span><span class="p">)</span>
</code></pre></div></div>

<p>A more detailed example can be seen in <a href="https://github.com/temporal-sa/temporal-order-management-demo/tree/main/go/nexus">Temporal Order Management Demo</a>.</p>

<h2 id="summary">Summary</h2>
<p>In this article we were introduced to the new groundbreaking Temporal feature: Nexus. Nexus provides an API contract and cross-namespace support for Temporal Workflows, as well as Temporal primitives such as Signal, Update or even Query. We discussed some of the use cases behind Nexus and saw first hand, with examples, how to easily invoke not only Workflows, Workflow primitives but any service that requires durability, using Nexus.</p>

<p>(c) 2024 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="Temporal" /><category term="Temporal" /><category term="Durable Execution" /><category term="Replay" /><category term="Determinism" /><category term="Workflow Orchestration" /><category term="Nexus" /><summary type="html"><![CDATA[Overview Temporal announced Nexus during Replay 2024. Nexus has been a feature and capability in the works for several years. Without Nexus, Temporal developers didn’t have a lot of options for executing workflows or workflow primitives across namespace boundaries. There was no easy way to provide an API contract, and as such Workflows became a somewhat, leaky abstraction. Futhermore, beyond activities which were sometime overkill, developers did not have a way to make simple service calls, durably.]]></summary></entry><entry><title type="html">Altering Space-Time Continuum: Testing for Determinism in Temporal Workflows</title><link href="https://keithtenzer.com/temporal/Altering_Space-Time_Continuum_Testing_for_Determinism/" rel="alternate" type="text/html" title="Altering Space-Time Continuum: Testing for Determinism in Temporal Workflows" /><published>2024-10-07T00:00:00+00:00</published><updated>2024-10-07T00:00:00+00:00</updated><id>https://keithtenzer.com/temporal/Altering_Space-Time_Continuum_Testing_for_Determinism</id><content type="html" xml:base="https://keithtenzer.com/temporal/Altering_Space-Time_Continuum_Testing_for_Determinism/"><![CDATA[<p><img src="/assets/2022-08-15/logo-temporal-with-copy.svg" alt="Temporal" /></p>
<h2 id="overview">Overview</h2>
<p>Even time traveling has its rules. To prevent one from creating an alternative future reality, Temporal requires Workflow determinism. In this article we will look at how to test Temporal workflows and ensure they are deterministic as part of CI/CD.</p>

<h3 id="what-is-workflow-determinism">What is Workflow Determinism?</h3>
<p>A Temporal Workflow is Deterministic, if it does the same thing, producing the same outputs, given the same inputs.</p>

<p>Workflow Determinism is only checked when a Workflow is replayed (when we go back in time and reconstruct state). In Temporal, workflows are replayed when Workflow state must be reconstructed. Workflow state must be reconstructed when a Workflow is in the middle of execution and its worker no longer has that Workflow cached. A Workflow that is in the middle of execution, can be evicted from the Worker cache (due to memory), or can be re-scheduled to a new worker because for example, its original worker died, or is having issues.</p>

<h3 id="what-causes-non-determinism">What Causes Non-Determinism?</h3>
<p>The causes of Non-Deterministic errors are from doing something non-deterministic, such as UUID generation in Workflow code, or by not properly versioning Workflow changes. For example, changing order of Workflow Activity execution in Workflow from (Activity1, Activity2, Activity3) to (Activity3, Activity2, Activity1) and then restarting workers, while Workflow execution is still occurring. Restarting workers, forces currently executing workflows to replay and if Activity order (for activities that already occurred) changed, a Non-Deterministic error is thrown.</p>

<h3 id="testing-for-non-determinism">Testing for Non-Determinism</h3>
<p>Thankfully, the Temporal SDK provides mechanisms for performing Workflow replay. Replaying a Workflow takes a pre-recorded Temporal Workflow event history and replays it, against a given Workflow code version. Before rolling out new Workflow code changes, it is strongly recommended to perform a replay test.</p>

<p>There are two ways you can replay a Workflow:</p>
<ul>
  <li>Using the SDK WorkflowReplayer from the testing facility</li>
  <li>Directly inside the Worker itself</li>
</ul>

<p>Using the WorkflowReplayer, allows for specifying the Workflow classes that should be replayed vs using Replay from within the Worker, which will test only the classes that are registered, to that specific Worker.</p>

<p>The below sample in Java, shows how to download event history and perform a replay using WorkflowReplayer.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">WorkflowReplayer</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Logger</span> <span class="n">log</span> <span class="o">=</span> <span class="nc">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="nc">WorkflowReplayer</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">replayWorkflowHistory</span><span class="o">(</span><span class="nc">String</span> <span class="n">workflowId</span><span class="o">,</span> <span class="nc">WorkflowClient</span> <span class="n">workflowClient</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="nc">WorkflowExecutionHistory</span> <span class="n">history</span> <span class="o">=</span> <span class="n">getWorkflowHistory</span><span class="o">(</span><span class="n">workflowId</span><span class="o">,</span> <span class="n">workflowClient</span><span class="o">);</span>
    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Replaying workflow id "</span> <span class="o">+</span> <span class="n">workflowId</span><span class="o">);</span>
    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="nc">Environment</span><span class="o">.</span><span class="na">getServerInfo</span><span class="o">().</span><span class="na">toString</span><span class="o">());</span>

    <span class="k">try</span> <span class="o">{</span>
      <span class="n">io</span><span class="o">.</span><span class="na">temporal</span><span class="o">.</span><span class="na">testing</span><span class="o">.</span><span class="na">WorkflowReplayer</span><span class="o">.</span><span class="na">replayWorkflowExecution</span><span class="o">(</span><span class="n">history</span><span class="o">,</span> <span class="nc">Hello</span><span class="o">.</span><span class="na">HelloWorkflowImpl</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Failed to replay workflow "</span> <span class="o">+</span> <span class="n">workflowId</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="kd">private</span> <span class="nc">WorkflowExecutionHistory</span> <span class="nf">getWorkflowHistory</span><span class="o">(</span><span class="nc">String</span> <span class="n">workflowId</span><span class="o">,</span> <span class="nc">WorkflowClient</span> <span class="n">workflowClient</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">workflowClient</span><span class="o">.</span><span class="na">fetchHistory</span><span class="o">(</span><span class="n">workflowId</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span><span class="o">{</span>
    <span class="nc">WorkflowServiceStubs</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">WorkflowServiceStubs</span><span class="o">.</span><span class="na">newServiceStubs</span><span class="o">(</span><span class="nc">TemporalClient</span><span class="o">.</span><span class="na">getWorkflowServiceStubs</span><span class="o">().</span><span class="na">getOptions</span><span class="o">());</span>

    <span class="nc">WorkflowClientOptions</span><span class="o">.</span><span class="na">Builder</span> <span class="n">builder</span> <span class="o">=</span> <span class="nc">WorkflowClientOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">();</span>
    <span class="nc">WorkflowClientOptions</span> <span class="n">clientOptions</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">setNamespace</span><span class="o">(</span><span class="nc">Environment</span><span class="o">.</span><span class="na">getNamespace</span><span class="o">()).</span><span class="na">build</span><span class="o">();</span>
    <span class="nc">WorkflowClient</span> <span class="n">client</span> <span class="o">=</span> <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">service</span><span class="o">,</span> <span class="n">clientOptions</span><span class="o">);</span>


    <span class="nc">WorkflowReplayer</span> <span class="n">workflowReplayer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WorkflowReplayer</span><span class="o">();</span>
    <span class="nc">String</span> <span class="n">workflowId</span> <span class="o">=</span> <span class="nc">Environment</span><span class="o">.</span><span class="na">getWorkflowId</span><span class="o">();</span>

    <span class="k">try</span> <span class="o">{</span>
      <span class="n">workflowReplayer</span><span class="o">.</span><span class="na">replayWorkflowHistory</span><span class="o">(</span><span class="n">workflowId</span><span class="o">,</span> <span class="n">client</span><span class="o">);</span>
      <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Replay test successful"</span><span class="o">);</span>
      <span class="nc">System</span><span class="o">.</span><span class="na">exit</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Failed to replay workflowId "</span> <span class="o">+</span> <span class="n">workflowId</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">e</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
      <span class="nc">System</span><span class="o">.</span><span class="na">exit</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The below sample in Java, shows how to download event history and perform a replay from within Worker.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">TemporalWorkerSelfReplay</span> <span class="o">{</span>

    <span class="nd">@SuppressWarnings</span><span class="o">(</span><span class="s">"CatchAndPrintStackTrace"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="kd">final</span> <span class="nc">String</span> <span class="no">TASK_QUEUE</span> <span class="o">=</span> <span class="nc">Environment</span><span class="o">.</span><span class="na">getTaskqueue</span><span class="o">();</span>

        <span class="nc">String</span> <span class="n">workflowId</span> <span class="o">=</span> <span class="nc">Environment</span><span class="o">.</span><span class="na">getWorkflowId</span><span class="o">();</span>
        <span class="nc">WorkflowServiceStubs</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">WorkflowServiceStubs</span><span class="o">.</span><span class="na">newServiceStubs</span><span class="o">(</span><span class="nc">TemporalClient</span><span class="o">.</span><span class="na">getWorkflowServiceStubs</span><span class="o">().</span><span class="na">getOptions</span><span class="o">());</span>
        <span class="nc">WorkflowClientOptions</span><span class="o">.</span><span class="na">Builder</span> <span class="n">builder</span> <span class="o">=</span> <span class="nc">WorkflowClientOptions</span><span class="o">.</span><span class="na">newBuilder</span><span class="o">();</span>
        <span class="nc">WorkflowClientOptions</span> <span class="n">clientOptions</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">setNamespace</span><span class="o">(</span><span class="nc">Environment</span><span class="o">.</span><span class="na">getNamespace</span><span class="o">()).</span><span class="na">build</span><span class="o">();</span>
        <span class="nc">WorkflowClient</span> <span class="n">client</span> <span class="o">=</span> <span class="nc">WorkflowClient</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">service</span><span class="o">,</span> <span class="n">clientOptions</span><span class="o">);</span>

        <span class="nc">WorkflowReplayer</span> <span class="n">workflowReplayer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WorkflowReplayer</span><span class="o">();</span>

        <span class="nc">WorkflowExecutionHistory</span> <span class="n">history</span> <span class="o">=</span> <span class="n">workflowReplayer</span><span class="o">.</span><span class="na">getWorkflowHistory</span><span class="o">(</span><span class="n">workflowId</span><span class="o">,</span> <span class="n">client</span><span class="o">);</span>

        <span class="nc">WorkerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">WorkerFactory</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="nc">TemporalClient</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>

        <span class="nc">Worker</span> <span class="n">worker</span> <span class="o">=</span> <span class="n">factory</span><span class="o">.</span><span class="na">newWorker</span><span class="o">(</span><span class="no">TASK_QUEUE</span><span class="o">);</span>
        <span class="n">worker</span><span class="o">.</span><span class="na">registerWorkflowImplementationTypes</span><span class="o">(</span><span class="nc">Hello</span><span class="o">.</span><span class="na">HelloWorkflowImpl</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">worker</span><span class="o">.</span><span class="na">registerActivitiesImplementations</span><span class="o">(</span><span class="k">new</span> <span class="nc">Hello</span><span class="o">.</span><span class="na">HelloActivitiesImpl</span><span class="o">());</span>

        <span class="n">worker</span><span class="o">.</span><span class="na">replayWorkflowExecution</span><span class="o">(</span><span class="n">history</span><span class="o">);</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Workflow replay for workflowId: "</span> <span class="o">+</span> <span class="n">workflowId</span> <span class="o">+</span> <span class="s">" completed successfully"</span><span class="o">);</span>

        <span class="n">factory</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Worker started for task queue: "</span> <span class="o">+</span> <span class="no">TASK_QUEUE</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="temporal-workflow-replay-in-cicd">Temporal Workflow Replay in CI/CD</h3>
<p>Taking things a step further, instead of relying on every developer to test their Workflow code for Non-Determinism, wouldn’t it be better to ensure this critical test happens as part of CI/CD?</p>

<h4 id="step-1-run-a-workflow">Step 1: Run a Workflow</h4>
<p>First, let’s run a one-time Temporal Workflow using a k8s job, as we need an event history to perform a replay test. The below sample, will launch a Worker and start a Temporal HelloWorkflow Workflow using <a href="https://github.com/temporal-sa/temporal-replayer/blob/v1/src/main/java/io/temporal/samples/replay/Hello.java">v1</a> code.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nv">$ </span>kubectl create <span class="nt">-f</span> yaml/job.yaml <span class="nt">-n</span> namespace
</code></pre></div></div>

<p>The below sample, shows how to use k8s job to execute a Workflow.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">batch/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Job</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/build</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
    <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-starter</span>
    <span class="na">app.kubernetes.io/version</span><span class="pi">:</span> <span class="s">v1.0</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-starter</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_WORKFLOW_ID</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">HelloWorkflow</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw.tmprl.cloud:7233</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_TASK_QUEUE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">hello</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-starter</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-hello-starter:v1.0</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
        <span class="na">secret</span><span class="pi">:</span>
          <span class="na">defaultMode</span><span class="pi">:</span> <span class="m">420</span>
          <span class="na">secretName</span><span class="pi">:</span> <span class="s">temporal-tls</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Never</span>
  <span class="na">backoffLimit</span><span class="pi">:</span> <span class="m">4</span>
</code></pre></div></div>

<h4 id="step-2-run-worker-deployment-with-replay-test-v1">Step 2: Run Worker Deployment with Replay Test (v1)</h4>
<p>Using the WorkflowReplayer sample, a docker image is launched and within it a replay test is performed, prior to actual Worker deployment rollout. This is done using an init container. The replay test <strong>succeeds</strong> as the <a href="https://github.com/temporal-sa/temporal-replayer/blob/v1/src/main/java/io/temporal/samples/replay/Hello.java#L113">v1 code path</a> is the same as the pre-recorded event history from step 1.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl <span class="nt">-f</span> yaml/deployment-v1.yaml <span class="nt">-n</span> namespace
</code></pre></div></div>

<p>Below is the k8s deployment config for executing replay test against v1 code.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/build</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
    <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
    <span class="na">app.kubernetes.io/version</span><span class="pi">:</span> <span class="s">v1.0</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">progressDeadlineSeconds</span><span class="pi">:</span> <span class="m">600</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">revisionHistoryLimit</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
      <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">rollingUpdate</span><span class="pi">:</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="s">25%</span>
      <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="s">25%</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">creationTimestamp</span><span class="pi">:</span> <span class="no">null</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app.kubernetes.io/build</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
        <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
        <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
        <span class="na">app.kubernetes.io/version</span><span class="pi">:</span> <span class="s">v1.0</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">initContainers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_WORKFLOW_ID</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">HelloWorkflow</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw.tmprl.cloud:7233</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_TASK_QUEUE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">hello</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-replayer</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-replayer:v1.0</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">securityContext</span><span class="pi">:</span>
          <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">terminationMessagePath</span><span class="pi">:</span> <span class="s">/dev/termination-log</span>
        <span class="na">terminationMessagePolicy</span><span class="pi">:</span> <span class="s">File</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw.tmprl.cloud:7233</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_TASK_QUEUE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">hello</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-hello-worker:v1.0</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">securityContext</span><span class="pi">:</span>
          <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">terminationMessagePath</span><span class="pi">:</span> <span class="s">/dev/termination-log</span>
        <span class="na">terminationMessagePolicy</span><span class="pi">:</span> <span class="s">File</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">dnsPolicy</span><span class="pi">:</span> <span class="s">ClusterFirst</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
      <span class="na">schedulerName</span><span class="pi">:</span> <span class="s">default-scheduler</span>
      <span class="na">securityContext</span><span class="pi">:</span> <span class="pi">{}</span>
      <span class="na">terminationGracePeriodSeconds</span><span class="pi">:</span> <span class="m">30</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
        <span class="na">secret</span><span class="pi">:</span>
          <span class="na">defaultMode</span><span class="pi">:</span> <span class="m">420</span>
          <span class="na">secretName</span><span class="pi">:</span> <span class="s">temporal-tls</span>
</code></pre></div></div>

<h4 id="step-3-run-worker-deployment-with-replay-test-v2">Step 3: Run Worker Deployment with Replay Test (v2)</h4>
<p>Using the WorkflowReplayer sample, a docker image is launched and within it a replay test is performed, prior to actual Worker deployment rollout. This is done using an init container. The replay test <strong>fails</strong> as the <a href="https://github.com/temporal-sa/temporal-replayer/blob/v2/src/main/java/io/temporal/samples/replay/Hello.java#L113">v2 code path</a> is not the same as the pre-recorded event history from step 1.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> yaml/deployment-v2.yaml <span class="nt">-n</span> namespace
</code></pre></div></div>

<p>Below is the k8s deployment config for executing replay test against v2 code.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/build</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
    <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
    <span class="na">app.kubernetes.io/version</span><span class="pi">:</span> <span class="s">v2.0</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">progressDeadlineSeconds</span><span class="pi">:</span> <span class="m">600</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">revisionHistoryLimit</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
      <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
  <span class="na">strategy</span><span class="pi">:</span>
    <span class="na">rollingUpdate</span><span class="pi">:</span>
      <span class="na">maxSurge</span><span class="pi">:</span> <span class="s">25%</span>
      <span class="na">maxUnavailable</span><span class="pi">:</span> <span class="s">25%</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">RollingUpdate</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">creationTimestamp</span><span class="pi">:</span> <span class="no">null</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app.kubernetes.io/build</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
        <span class="na">app.kubernetes.io/component</span><span class="pi">:</span> <span class="s">worker</span>
        <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
        <span class="na">app.kubernetes.io/version</span><span class="pi">:</span> <span class="s">v2.0</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">initContainers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_WORKFLOW_ID</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">HelloWorkflow</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw.tmprl.cloud:7233</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_TASK_QUEUE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">hello</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-replayer</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-replayer:v2.0</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">securityContext</span><span class="pi">:</span>
          <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">terminationMessagePath</span><span class="pi">:</span> <span class="s">/dev/termination-log</span>
        <span class="na">terminationMessagePolicy</span><span class="pi">:</span> <span class="s">File</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_ADDRESS</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw.tmprl.cloud:7233</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_TASK_QUEUE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">hello</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_NAMESPACE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">helloworld.sdvdw</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_CERT_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.crt</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TEMPORAL_KEY_PATH</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s">/etc/certs/tls.key</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ktenzer/temporal-hello-worker:v2.0</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">temporal-hello-worker</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Always</span>
        <span class="na">securityContext</span><span class="pi">:</span>
          <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">terminationMessagePath</span><span class="pi">:</span> <span class="s">/dev/termination-log</span>
        <span class="na">terminationMessagePolicy</span><span class="pi">:</span> <span class="s">File</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/certs</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
      <span class="na">dnsPolicy</span><span class="pi">:</span> <span class="s">ClusterFirst</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
      <span class="na">schedulerName</span><span class="pi">:</span> <span class="s">default-scheduler</span>
      <span class="na">securityContext</span><span class="pi">:</span> <span class="pi">{}</span>
      <span class="na">terminationGracePeriodSeconds</span><span class="pi">:</span> <span class="m">30</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">certs</span>
        <span class="na">secret</span><span class="pi">:</span>
          <span class="na">defaultMode</span><span class="pi">:</span> <span class="m">420</span>
          <span class="na">secretName</span><span class="pi">:</span> <span class="s">temporal-tls</span>
</code></pre></div></div>

<p>For more details, refer to <a href="https://github.com/temporal-sa/temporal-replayer">Temporal Replayer</a>.</p>

<h2 id="summary">Summary</h2>
<p>In this article we discussed the importance of Temporal Workflow Determinism. We looked at the causes for Non-determinism in Temporal workflows. Finally, leveraging the Java SDK, we showed how to write a replay test and even integrate it into CI/CD using a k8s deployment.</p>

<p>(c) 2024 Keith Tenzer</p>]]></content><author><name>Keith Tenzer</name></author><category term="Temporal" /><category term="Temporal" /><category term="Durable Execution" /><category term="Replay" /><category term="Determinism" /><category term="Workflow Orchestration" /><summary type="html"><![CDATA[Overview Even time traveling has its rules. To prevent one from creating an alternative future reality, Temporal requires Workflow determinism. In this article we will look at how to test Temporal workflows and ensure they are deterministic as part of CI/CD.]]></summary></entry></feed>