<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:discourse="http://discourse.org/rss/modules/discourse/">
<channel>
<title>Integration Engine on Roque</title>
<link>https://j-roque.com/tags/integration-engine/</link>
<description>Recent content in Integration Engine on Roque</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<managingEditor>joao.roque.94@gmail.com (João Roque)</managingEditor>
<webMaster>joao.roque.94@gmail.com (João Roque)</webMaster>
<copyright>© 2026 João Roque</copyright>
<lastBuildDate>Wed, 29 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://j-roque.com/tags/integration-engine/index.xml" rel="self" type="application/rss+xml"/>
<item>
<title>The Integration Engine, Three Layers Deep</title>
<link>https://j-roque.com/posts/20260729-integration-engine/</link>
<pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
<author>joao.roque.94@gmail.com (João Roque)</author>
<guid>https://j-roque.com/posts/20260729-integration-engine/</guid>
<description>A practitioner&amp;rsquo;s tour of the Integration Entry, Package, and Engine model behind CM MES integrations — plus the batch/parent gotchas and the atomicity tax nobody mentions until it bites.</description>
<content:encoded>&lt;p&gt;&lt;img src="https://j-roque.com/posts/20260729-integration-engine/featured.png" alt="The Integration Engine, Three Layers Deep" /&gt;&lt;/p&gt;&lt;p&gt;If your integration layer can&amp;rsquo;t survive the ERP going down for maintenance, you don&amp;rsquo;t have an integration layer you have a dependency.&lt;/p&gt;
&lt;p&gt;A recap of blog &lt;a
href="https://devblog.criticalmanufacturing.com/blog/20250429_integration_entries/"
target="_blank"
&gt;A Closer Look at Integration Entries&lt;/a&gt; and &lt;a
href="https://devblog.criticalmanufacturing.com/blog/20250619_low_code_integration/"
target="_blank"
&gt;Integration Entries, Part II: Low Code&lt;/a&gt;.&lt;/p&gt;
&lt;h2 class="relative group"&gt;The synchronous trap
&lt;div id="the-synchronous-trap" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-synchronous-trap" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Most integration problems start the same way: someone wires a &lt;strong&gt;synchronous&lt;/strong&gt; call between two systems, it works fine in the demo, and six months later a single SAP outage takes down production reporting. The request-response model is simple to reason about, which is exactly why people reach for it first. It&amp;rsquo;s also the &lt;strong&gt;wrong default&lt;/strong&gt; for anything that isn&amp;rsquo;t trivially fast and trivially reliable.&lt;/p&gt;
&lt;p&gt;The alternative is &lt;strong&gt;asynchronous&lt;/strong&gt; processing: the caller writes a message somewhere, gets an immediate acknowledgment, and a background worker does the actual work on its own schedule. This buys you three things that matter in a shop-floor context — &lt;strong&gt;long-running tasks don&amp;rsquo;t block the caller&lt;/strong&gt;, the &lt;strong&gt;receiving side scales independently&lt;/strong&gt;, and the &lt;strong&gt;two systems are no longer tightly coupled&lt;/strong&gt; to each other&amp;rsquo;s uptime.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Sync Async"
src="https://image.j-roque.com/posts/20260729-integration-engine/sync-async.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;That last one is the one people underrate. If MES can hand a message to a queue and walk away, an ERP outage stops being a P1.&lt;/p&gt;
&lt;p&gt;CM MES bakes this pattern in as the &lt;strong&gt;Integration Engine&lt;/strong&gt;, and it&amp;rsquo;s the mechanism behind nearly every ERP, PLM, or asset-management integration you&amp;rsquo;ll build on the platform.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Overview Integration Engine"
src="https://image.j-roque.com/posts/20260729-integration-engine/overview-integrationengine.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 class="relative group"&gt;Three components, one job
&lt;div id="three-components-one-job" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#three-components-one-job" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;The framework splits cleanly into three concepts, and once they click, the rest of the system reads like plumbing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integration Entry&lt;/strong&gt; — the message itself. Conceptually the same thing as a message sitting in a RabbitMQ queue: metadata plus a payload.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration Package&lt;/strong&gt; — the component that knows what to &lt;em&gt;do&lt;/em&gt; with a message once it&amp;rsquo;s picked up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration Engine&lt;/strong&gt; — the orchestrator that pulls entries, figures out which package should handle each one, and dispatches them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inbound entries (ERP → MES) get created through the standard &lt;code&gt;CreateObject&lt;/code&gt; API or a custom API that does validation up front. Outbound entries (MES → ERP) are typically created by DEE Actions reacting to something that just happened on the shop floor. Either direction, the entry lands in the same store and gets picked up the same way — which is the whole point of decoupling.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Integration Framework"
src="https://image.j-roque.com/posts/20260729-integration-engine/integration-framework.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;div class="not-prose"&gt;
&lt;div style="position:relative; width:100%; height:500px; border-radius:0.5rem; overflow:hidden;"&gt;
&lt;iframe
src="https://help.criticalmanufacturing.com/userguide/administration/system-integrations/"
title="Integration Engine"
style="border:none; display:block; width:100%; height:100%; zoom:0.5;"
loading="lazy"
&gt;&lt;/iframe&gt;
&lt;button
onclick="document.getElementById('iframe-modal-0').classList.remove('invisible')"
title="Open fullscreen"
style="position:absolute; bottom:0.5rem; right:0.5rem; display:flex; align-items:center; gap:0.3rem; padding:0.3rem 0.6rem; border-radius:0.375rem; border:none; cursor:pointer; font-size:0.75rem; font-weight:600; background:rgba(0,0,0,0.55); color:#fff; backdrop-filter:blur(4px);"
&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width:1em;height:1em;"&gt;&lt;path fill-rule="evenodd" d="M15 3a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V5.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L18.586 4H16a1 1 0 0 1-1-1ZM3 9a1 1 0 0 1 1 1v2.586l4.293-4.293a1 1 0 0 1 1.414 1.414L5.414 14H8a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1Z" clip-rule="evenodd"/&gt;&lt;path fill-rule="evenodd" d="M3 15a1 1 0 0 1 1 1v2.586l4.293-4.293a1 1 0 1 1 1.414 1.414L5.414 18H8a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1ZM21 3a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0V5.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L18.586 4H16a1 1 0 0 1-1-1Z" clip-rule="evenodd"/&gt;&lt;/svg&gt;
Fullscreen
&lt;/button&gt;
&lt;/div&gt;
&lt;div
id="iframe-modal-0"
class="invisible"
onclick="if(event.target===this)this.classList.add('invisible')"
style="position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; background:rgba(64,64,64,0.6); backdrop-filter:blur(4px); padding:1.5rem;"
&gt;
&lt;div style="position:relative; width:100%; height:100%; max-width:1400px; border-radius:0.5rem; overflow:hidden; box-shadow:0 25px 50px rgba(0,0,0,0.4);"&gt;
&lt;button
onclick="document.getElementById('iframe-modal-0').classList.add('invisible')"
title="Close"
style="position:absolute; top:0.5rem; right:0.5rem; z-index:10; display:flex; align-items:center; justify-content:center; width:2rem; height:2rem; border-radius:9999px; border:none; cursor:pointer; background:rgba(0,0,0,0.55); color:#fff; backdrop-filter:blur(4px);"
&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width:1em;height:1em;"&gt;&lt;path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/&gt;&lt;/svg&gt;
&lt;/button&gt;
&lt;iframe
src="https://help.criticalmanufacturing.com/userguide/administration/system-integrations/"
title="Integration Engine"
style="border:none; display:block; width:100%; height:100%;"
allowfullscreen
&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function() {
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var m = document.getElementById('iframe-modal-0');
if (m) m.classList.add('invisible');
}
});
})();
&lt;/script&gt;
&lt;hr&gt;
&lt;p&gt;The &lt;strong&gt;Integration Entry&lt;/strong&gt; represents a message to be processed and is the core entity type of the integration entity framework. It carries a small, deliberate set of properties:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Unique identifier — GUID or a naming convention, your call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MessageType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;What kind of message this is; drives package resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SourceSystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lookup (IntegrationSystem)&lt;/td&gt;
&lt;td&gt;Where the message came from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TargetSystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lookup (IntegrationSystem)&lt;/td&gt;
&lt;td&gt;Where it&amp;rsquo;s going&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SystemState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enum&lt;/td&gt;
&lt;td&gt;Current processing status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IsRetriable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;Whether a failed entry is eligible for retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The payload itself lives in a separate but tightly-coupled entity, &lt;code&gt;IntegrationMessage&lt;/code&gt;. It&amp;rsquo;s rarely handled on its own but it matters in one very specific operational way, which we&amp;rsquo;ll get to.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An Integration Entry is just a message on a queue that happens to be an entity type. Don&amp;rsquo;t over-model it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 class="relative group"&gt;The state machine you&amp;rsquo;re actually debugging
&lt;div id="the-state-machine-youre-actually-debugging" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-state-machine-youre-actually-debugging" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Every entry moves through a state machine: &lt;code&gt;Received → Processing → Processed&lt;/code&gt;, with &lt;code&gt;Failed&lt;/code&gt; and &lt;code&gt;Rejected&lt;/code&gt; as the off-ramps.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Received&lt;/code&gt; is the signal that tells the orchestrator &amp;ldquo;this one&amp;rsquo;s up for grabs.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Processing&lt;/code&gt; exists mostly for Automation Job scenarios (more on that below) — for a standard DEE-Action-backed integration, you&amp;rsquo;ll rarely see it linger there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Failed entries with &lt;code&gt;IsRetriable&lt;/code&gt; set get swept up by a timer, &lt;code&gt;RetryIntegrationEntry&lt;/code&gt;, which runs every five minutes by default and flips them back to &lt;code&gt;Received&lt;/code&gt;. This is the detail that trips people up in support tickets: setting &lt;code&gt;IsRetriable&lt;/code&gt; to true does &lt;strong&gt;not&lt;/strong&gt; immediately requeue the entry. You&amp;rsquo;re waiting on the next timer tick, not triggering an event.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="State Model"
src="https://image.j-roque.com/posts/20260729-integration-engine/statemodel-integrationentry.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 class="relative group"&gt;Packages: the part that does the work
&lt;div id="packages-the-part-that-does-the-work" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#packages-the-part-that-does-the-work" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Out of the box you get two packages.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GenericIntegrationHandler&lt;/code&gt; bridges the framework to MES orchestration — you point it at a DEE Action per message type, and that action does the actual work. &lt;code&gt;SapIntegrationHandler&lt;/code&gt; connects directly to an SAP instance.&lt;/p&gt;
&lt;p&gt;In practice, the generic handler covers the overwhelming majority of real deployments, SAP included, because most SAP integrations go through middleware rather than a direct connection.&lt;/p&gt;
&lt;p&gt;Resolution — which package handles which entry — is configured in the &lt;code&gt;IntegrationHandlerResolution&lt;/code&gt; smart table, keyed by &lt;code&gt;SourceSystem&lt;/code&gt;, &lt;code&gt;TargetSystem&lt;/code&gt;, and &lt;code&gt;MessageType&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Integration Handler Resolution"
src="https://image.j-roque.com/posts/20260729-integration-engine/integration-handlerresolution.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;For the generic handler you supply a DEE Action name, and optionally an error-handling action that fires immediately on failure. That second field is more useful than it sounds: if you know a given error class is transient (ERP temporarily unreachable, say), the error action can flip the entry to retry automatically instead of waiting for someone to notice it sitting in &lt;code&gt;Failed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the generic handler genuinely doesn&amp;rsquo;t fit — you need a persistent connection to an external message broker, for instance — you can build a custom package as an assembly with five kinds of classes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Package&lt;/code&gt; (registration and active/inactive state)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Monitor&lt;/code&gt; (watches connectivity to the third-party system)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Handler&lt;/code&gt; (the actual business logic), and &lt;code&gt;Sender&lt;/code&gt;/&lt;code&gt;Receiver&lt;/code&gt; (workers that talk to the external environment). Worth knowing: in the generic package&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sender&lt;/code&gt; and &lt;code&gt;Receiver&lt;/code&gt; are essentially empty stubs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All the real work sits in the &lt;code&gt;Handler&lt;/code&gt;, which just looks up and invokes a DEE Action. That&amp;rsquo;s not a limitation — it&amp;rsquo;s the reason the generic handler is flexible enough to not need replacing.&lt;/p&gt;
&lt;h2 class="relative group"&gt;What the orchestrator actually does on every tick
&lt;div id="what-the-orchestrator-actually-does-on-every-tick" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-the-orchestrator-actually-does-on-every-tick" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Integration SEQ Diagram"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250429_integration_entries/integration_seq_diagram.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;On host startup, the Integration Scheduler launches, loads active packages into an in-memory registry (so resolution doesn&amp;rsquo;t hit the database on every message), and starts the orchestrator loop. Each cycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pull the next &lt;code&gt;Received&lt;/code&gt; entry (or batch) via &lt;code&gt;T_PullNextIntegrationEntry&lt;/code&gt; stored procedure.&lt;/li&gt;
&lt;li&gt;Calculate the route — which package handles this entry.&lt;/li&gt;
&lt;li&gt;Dispatch to that package&amp;rsquo;s &lt;code&gt;Handler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write the result and flip the state to &lt;code&gt;Processed&lt;/code&gt; or &lt;code&gt;Failed&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The pull step is the interesting part, because it has to work correctly across multiple hosts pulling from the same table at once:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;-- conceptual shape of what T_PullNextIntegrationEntry does
SELECT TOP (@BatchSize) *
FROM IntegrationEntry
WITH (READPAST, UPDLOCK)
WHERE SystemState = &amp;#39;Received&amp;#39;
ORDER BY CreationDate ASC&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;READPAST&lt;/code&gt; skips rows another transaction already has locked instead of blocking on them; &lt;code&gt;UPDLOCK&lt;/code&gt; claims the rows this transaction just read so a second host can&amp;rsquo;t grab the same entries a moment later. That&amp;rsquo;s the entirety of the cross-host coordination story — no distributed lock manager, just SQL doing what SQL is good at.&lt;/p&gt;
&lt;h2 class="relative group"&gt;BatchId and ParentIntegrationEntryId: read the fine print
&lt;div id="batchid-and-parentintegrationentryid-read-the-fine-print" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#batchid-and-parentintegrationentryid-read-the-fine-print" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Two properties change how entries get pulled, and both have a sharp edge that isn&amp;rsquo;t obvious from the property description alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BatchId&lt;/strong&gt; groups entries so they&amp;rsquo;re pulled together and processed sequentially by the same host — occupying a single processing slot, not one slot each. But the grouping guarantee only applies to entries that already exist when the pull happens. If Host A has already locked two entries with &lt;code&gt;BatchId = B&lt;/code&gt;, and a third entry with the same &lt;code&gt;BatchId&lt;/code&gt; gets created before Host A finishes, does Host B risk picking it up? The answer yes, that&amp;rsquo;s possible. The batching guarantee is about entries present at pull time, not a standing claim on the batch identifier.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Batch Processing"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250429_integration_entries/integration_batch_id.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ParentIntegrationEntryId&lt;/strong&gt; creates an ordering dependency — entry B won&amp;rsquo;t become eligible until entry A (its parent) is processed. What it does &lt;em&gt;not&lt;/em&gt; do is act as a circuit breaker. If the parent fails permanently after exhausting its retries, the framework does not automatically prevent the child from being picked up indefinitely — it just never becomes eligible while the parent is actively &lt;code&gt;Failed&lt;/code&gt;-and-retriable-or-in-flight. Getting this wrong means silently stuck message chains that look healthy in every dashboard until someone goes looking.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Parent Child Processing"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250429_integration_entries/integration_parent_id.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ParentIntegrationEntryId&lt;/code&gt; orders execution. It does not guarantee the chain completes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 class="relative group"&gt;Configuration knobs, and the one you shouldn&amp;rsquo;t touch first
&lt;div id="configuration-knobs-and-the-one-you-shouldnt-touch-first" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configuration-knobs-and-the-one-you-shouldnt-touch-first" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler enabled&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Cmf/System/Configuration/Integration/IntegrationSchedulerIsActive/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic package enabled&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Cmf/System/Configuration/Integration/GenericIntegration/IsActive/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling interval&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Cmf/System/Configuration/Integration/PollingInterval/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max retries&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Cmf/System/Configuration/Integration/MaxNumberOfRetries/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel requests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Cmf/System/Configuration/Integration/NumberOfParallelRequests/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 (per host)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;NumberOfParallelRequests&lt;/code&gt; is the one people reach for first when throughput feels slow, and it&amp;rsquo;s the one that deserves the most respect. It&amp;rsquo;s per host, not per system — two hosts at the default gives you ten concurrent processing slots cluster-wide. Batched entries still only consume one slot regardless of how many messages are in the batch. Bump this number without load-testing first and you&amp;rsquo;ll find your bottleneck somewhere less convenient, like the ERP-side API you&amp;rsquo;re now hammering five times harder.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Low-code: drag, drop, and a Kafka topic you didn&amp;rsquo;t ask for
&lt;div id="low-code-drag-drop-and-a-kafka-topic-you-didnt-ask-for" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#low-code-drag-drop-and-a-kafka-topic-you-didnt-ask-for" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;The newer addition to this story is building integrations without writing a DEE Action at all — using an &lt;code&gt;AutomationController&lt;/code&gt; workflow instead. The canonical example, and a good one: importing a production order from an ERP.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Lifecycle"
src="https://image.j-roque.com/posts/20260729-integration-engine/po-integration-lifecycle.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;#34;plantCode&amp;#34;: &amp;#34;PLT01&amp;#34;,
  &amp;#34;partNumber&amp;#34;: &amp;#34;PN-4471-B&amp;#34;,
  &amp;#34;dueDate&amp;#34;: &amp;#34;2026-08-15T00:00:00Z&amp;#34;,
  &amp;#34;quantity&amp;#34;: 500,
  &amp;#34;unitOfMeasure&amp;#34;: &amp;#34;EA&amp;#34;,
  &amp;#34;priority&amp;#34;: 2
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You register this shape as an &lt;code&gt;IoTEventDefinition&lt;/code&gt; (scoped to &lt;strong&gt;Enterprise Integration&lt;/strong&gt; — you can import the JSON directly and let the system infer property types instead of typing each one by hand), build an &lt;code&gt;AutomationController&lt;/code&gt; workflow that consumes it, and instead of pointing &lt;code&gt;IntegrationHandlerResolution&lt;/code&gt; at a DEE Action, you point it at the event definition. The workflow itself — load the facility, load the product, build the material object, call &lt;code&gt;CreateObject&lt;/code&gt; — reads almost exactly like the equivalent DEE Action would, just assembled visually.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="PO IoT Event"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250429_integration_entries/po-event.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="PO Handler Resolution"
src="https://image.j-roque.com/posts/20260729-integration-engine/po-handlerresolution.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Now we can build a workflow that is going to consume the message.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Low Code Workflow"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250619_low_code_integration/automation_controller_workflow.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Under the hood it&amp;rsquo;s genuinely more moving parts than the DEE Action path: the message goes to a dedicated Kafka topic per event definition, the Connect IoT manager consumes it and creates an Automation Job, a second message round-trips through RabbitMQ, and &lt;em&gt;then&lt;/em&gt; the workflow executes.&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Tech Diagram"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250619_low_code_integration/low_code_tech_diagram.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Job Running"
src="https://devblog.criticalmanufacturing.com/blogPosts/posts/20250619_low_code_integration/integration_entry_processed.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;All of that complexity buys you real things — faster prototyping, workflows non-developers can read and modify, components reused across multiple integrations. It is not, however, free.&lt;/p&gt;
&lt;h2 class="relative group"&gt;The atomicity tax nobody mentions in the demo
&lt;div id="the-atomicity-tax-nobody-mentions-in-the-demo" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-atomicity-tax-nobody-mentions-in-the-demo" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the part that belongs in bold above the fold, and doesn&amp;rsquo;t get enough airtime: &lt;strong&gt;an Automation Job workflow is not a database transaction.&lt;/strong&gt; Each task executes independently. If your workflow creates a material in one task and a production order in the next, and the second task fails, you now have a material with no production order and no automatic rollback of the first step.&lt;/p&gt;
&lt;p&gt;A DEE Action wrapped in a single MES transaction doesn&amp;rsquo;t have this problem, it either fully commits or fully rolls back. A multi-step low-code workflow can leave you in a state that&amp;rsquo;s inconsistent by construction, and the framework will not warn you about it.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a reason to avoid low-code integrations.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a reason to design them assuming partial failure is normal, not exceptional — idempotent steps, compensating actions, or simply keeping the workflow to a single state-changing call.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Low-code doesn&amp;rsquo;t remove the need for transactional thinking. It just moves where you have to do it yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the creation of Business Workflows you are now also able to create DEEs with Low Code. This maintains the transactionality and offers a low code solution.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Final thoughts
&lt;div id="final-thoughts" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#final-thoughts" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;The generic handler plus a DEE Action will get you through the overwhelming majority of integration requirements, and there&amp;rsquo;s no shame in that being the boring, correct answer. Low-code earns its place for prototypes, cross-functional handoffs, and genuinely simple single-step imports, but it is still not a wholesale replacement for the traditional path, and not in domains where a partially-completed workflow can be a compliance problem rather than an inconvenience.&lt;/p&gt;
&lt;p&gt;Read the two source posts if you haven&amp;rsquo;t — &lt;a
href="https://devblog.criticalmanufacturing.com/blog/20250429_integration_entries/"
target="_blank"
&gt;Integration Entries&lt;/a&gt; and &lt;a
href="https://devblog.criticalmanufacturing.com/blog/20250619_low_code_integration/"
target="_blank"
&gt;Low-Code Integrations&lt;/a&gt; go deeper on the wiring than I have room for here. But the batch-locking edge case and the atomicity gap are the two things worth carrying into your next design review, because they&amp;rsquo;re exactly the kind of detail that only shows up once you&amp;rsquo;re already in production.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This blog post was based on a talk by Ricardo Cunha in 2025-10-16 @CM-Portugal&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded>
<enclosure url="https://j-roque.com/posts/20260729-integration-engine/featured.png" type="image/png" length="0"/>
<media:content url="https://j-roque.com/posts/20260729-integration-engine/featured.png" type="image/png" medium="image"/>
</item>
</channel>
</rss>