<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Marc Gille-Sepehri</title>
    <description>The latest articles on Forem by Marc Gille-Sepehri (@marcgillesepehri).</description>
    <link>https://forem.com/marcgillesepehri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873378%2F6d642848-2404-400f-894f-61c3a7f393e4.png</url>
      <title>Forem: Marc Gille-Sepehri</title>
      <link>https://forem.com/marcgillesepehri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/marcgillesepehri"/>
    <language>en</language>
    <item>
      <title>Self-Starting Processes: Timer Events, Mailbox Polling, and Why Your Agentic Workflows Should Launch Themselves</title>
      <dc:creator>Marc Gille-Sepehri</dc:creator>
      <pubDate>Thu, 16 Apr 2026 21:55:48 +0000</pubDate>
      <link>https://forem.com/marcgillesepehri/self-starting-processes-timer-events-mailbox-polling-and-why-your-agentic-workflows-should-387a</link>
      <guid>https://forem.com/marcgillesepehri/self-starting-processes-timer-events-mailbox-polling-and-why-your-agentic-workflows-should-387a</guid>
      <description>&lt;p&gt;&lt;em&gt;Follow-up to &lt;a href="https://dev.to/marcgillesepehri/why-we-keep-process-data-outside-the-engine-and-why-it-changes-everything-for-agentic-bpm-27p4"&gt;Why We Keep Process Data Outside the Engine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the first article, we made the case for keeping process data outside the engine. The &lt;code&gt;instanceId&lt;/code&gt; is the only binding key. Your code handles the intelligence. The engine handles the orchestration.&lt;/p&gt;

&lt;p&gt;But there was a gap in that story. Someone still had to start the process.&lt;/p&gt;

&lt;p&gt;A REST call. A button click. A cron job in a separate service calling &lt;code&gt;startInstance()&lt;/code&gt;. The engine could orchestrate anything — once a human or an external trigger told it to begin. The process itself had no agency over its own lifecycle.&lt;/p&gt;

&lt;p&gt;That changes now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processes That Start Themselves
&lt;/h2&gt;

&lt;p&gt;in-concert now supports &lt;strong&gt;timer start events&lt;/strong&gt; and &lt;strong&gt;message start events&lt;/strong&gt; — standard BPMN elements that let a process definition declare when and why it should launch, without any external trigger.&lt;/p&gt;

&lt;p&gt;A timer start event says: &lt;em&gt;run this process every hour&lt;/em&gt;. Or every weekday at 8:30. Or on the last Friday of every month. Or three times at 10-minute intervals, then stop.&lt;/p&gt;

&lt;p&gt;A message start event says: &lt;em&gt;run this process when an email arrives in this mailbox&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Deploy the BPMN. The engine takes it from there. No Lambda functions. No external scheduler. No webhook plumbing. The process definition is self-sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timer Start Events — Every Flavour of "When"
&lt;/h2&gt;

&lt;p&gt;Put a timer on a start event and the engine creates a persistent schedule. A background worker fires it, starts an instance, advances the schedule, and goes back to sleep. If the server restarts, the schedule is in MongoDB — nothing is lost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;bpmn:startEvent&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"TimerStart"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Daily compliance check"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;bpmn:timerEventDefinition&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;bpmn:timeCycle&amp;gt;&lt;/span&gt;R/P1D&lt;span class="nt"&gt;&amp;lt;/bpmn:timeCycle&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/bpmn:timerEventDefinition&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/bpmn:startEvent&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a process that runs once a day, forever, until you pause it. The engine supports five expression formats:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ISO 8601 repeating interval&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;R/PT1H&lt;/code&gt;, &lt;code&gt;R3/PT10M&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every hour (unbounded), or 3 times at 10-min intervals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISO 8601 duration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PT30M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once, 30 minutes after deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISO 8601 date-time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-12-25T00:00:00Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once, at that exact moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron (5-field)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30 8 * * 1-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Weekdays at 8:30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RRULE (RFC 5545)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FREQ=MONTHLY;BYDAY=FR;BYSETPOS=-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Last Friday of every month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RRULE is the format behind Outlook and Google Calendar recurrence. Any pattern you can set in a calendar invitation, you can use to schedule a process. &lt;code&gt;FREQ&lt;/code&gt;, &lt;code&gt;INTERVAL&lt;/code&gt;, &lt;code&gt;BYDAY&lt;/code&gt;, &lt;code&gt;BYMONTHDAY&lt;/code&gt;, &lt;code&gt;BYMONTH&lt;/code&gt;, &lt;code&gt;BYSETPOS&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;UNTIL&lt;/code&gt; — all supported. Zero external dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;bpmn:startEvent&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"TimerStart"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Last Friday of every month"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;bpmn:timerEventDefinition&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;bpmn:timeCycle&amp;gt;&lt;/span&gt;DTSTART:20260130T090000Z
RRULE:FREQ=MONTHLY;BYDAY=FR;BYSETPOS=-1&lt;span class="nt"&gt;&amp;lt;/bpmn:timeCycle&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/bpmn:timerEventDefinition&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/bpmn:startEvent&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pause and resume any schedule at runtime via the SDK or REST API. The schedule is a first-class object — queryable, manageable, observable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schedules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listTimerSchedules&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;definitionId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pauseTimerSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... later&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resumeTimerSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why This Matters for Agentic Workflows
&lt;/h3&gt;

&lt;p&gt;Agentic systems are not request-response. They are continuous. A compliance monitoring agent should check every morning whether anything changed overnight. A portfolio rebalancing agent should evaluate positions on a schedule. A reporting agent should assemble and distribute summaries at the end of every week.&lt;/p&gt;

&lt;p&gt;These are not one-off tasks triggered by a user. They are standing processes with their own heartbeat. Timer start events give them that heartbeat — expressed in standard BPMN, persisted in the engine, surviving restarts and deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Start Events — Email as a Process Trigger
&lt;/h2&gt;

&lt;p&gt;Timer events handle "when." Message events handle "what happened."&lt;/p&gt;

&lt;p&gt;A message start event with the &lt;code&gt;graph-mailbox&lt;/code&gt; connector tells the engine: poll this Microsoft 365 mailbox, and when an unread email arrives, start a process instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;bpmn:message&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"Msg_Inbox"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"inbox-poll"&lt;/span&gt;
  &lt;span class="na"&gt;tri:connectorType=&lt;/span&gt;&lt;span class="s"&gt;"graph-mailbox"&lt;/span&gt;
  &lt;span class="na"&gt;tri:mailbox=&lt;/span&gt;&lt;span class="s"&gt;"support@your-company.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;bpmn:startEvent&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"Start"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Email received"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;bpmn:messageEventDefinition&lt;/span&gt; &lt;span class="na"&gt;messageRef=&lt;/span&gt;&lt;span class="s"&gt;"Msg_Inbox"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/bpmn:startEvent&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;tri:&lt;/code&gt; extension attributes on the &lt;code&gt;&amp;lt;bpmn:message&amp;gt;&lt;/code&gt; element identify the connector type and the mailbox. The Graph API credentials are configured once as engine settings — environment variables or SDK &lt;code&gt;init()&lt;/code&gt; — and never appear in the BPMN.&lt;/p&gt;

&lt;p&gt;Deploy the process. The engine polls. An email arrives. A process instance is created.&lt;/p&gt;

&lt;h3&gt;
  
  
  The onMailReceived Callback
&lt;/h3&gt;

&lt;p&gt;Here is where the "data outside the engine" principle from the first article meets the real world.&lt;/p&gt;

&lt;p&gt;The engine creates the process instance — so you have an &lt;code&gt;instanceId&lt;/code&gt; — but does not advance a single token until your callback returns. Your code receives the full email: subject, sender, body, and attachment metadata. You store it in your domain. You decide whether to proceed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;graph-mailbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GRAPH_TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GRAPH_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GRAPH_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;onMailReceived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getAttachmentContent&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Store the email in your domain, bound to the process instance&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedDateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Download attachments on demand — metadata is already there, content is lazy&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;att&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAttachmentContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Return { skip: true } to terminate the instance without running&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSpam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;onServiceCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Your agentic logic — LLM calls, tool invocations, etc.&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attachments are not pre-loaded into memory. The callback receives metadata — name, content type, size — and a &lt;code&gt;getAttachmentContent()&lt;/code&gt; function that downloads a single attachment on demand. A 40 MB zip does not sit in your Node process unless you explicitly ask for it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;{ skip: true }&lt;/code&gt; return value terminates the instance. Spam filter, sender allowlist, duplicate detection — your code, your rules. The engine created the instance so you have an &lt;code&gt;instanceId&lt;/code&gt; to correlate against. If you skip, it is cleanly terminated. If you proceed, the process runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters for Agentic Workflows
&lt;/h3&gt;

&lt;p&gt;Email is the entry point for most business processes in the real world. Customer requests, supplier invoices, regulatory notifications, internal approvals — they arrive as emails with attachments, and someone has to triage them, extract data, route them, and act.&lt;/p&gt;

&lt;p&gt;This is exactly what agentic workflows do. The BPMN process models the routing. The LLM handles the triage and extraction. The human task is the escalation point when the AI is uncertain. And now the trigger — the email itself — is part of the process definition.&lt;/p&gt;

&lt;p&gt;No middleware. No separate polling service. No Azure Function glue. The BPMN file declares the mailbox. The engine polls it. Your &lt;code&gt;onMailReceived&lt;/code&gt; callback stores the data. The process runs.&lt;/p&gt;

&lt;p&gt;A support email arrives → the agent extracts the intent → checks the knowledge base → drafts a response → routes to a human reviewer if confidence is low → sends the reply. All modelled in BPMN. All starting from an email. All running without anyone clicking "start."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture — Same Pattern, Different Triggers
&lt;/h2&gt;

&lt;p&gt;Both timer and message start events follow the same internal pattern we use for the continuation worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; creates a persistent schedule document in MongoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker loop&lt;/strong&gt; polls for due schedules, claims with an optimistic lease&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire&lt;/strong&gt; calls &lt;code&gt;startInstance()&lt;/code&gt; — same as if you called it yourself via the API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advance&lt;/strong&gt; updates the schedule (next fire time, or mark as exhausted)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Multi-instance safe. Survives restarts. No in-memory state. The schedule is a MongoDB document with an index — the same infrastructure the engine already uses for continuations and outbox delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come Build With Us
&lt;/h2&gt;

&lt;p&gt;Timer and message start events are available now in &lt;code&gt;@the-real-insight/in-concert&lt;/code&gt; on npm. The full documentation — RRULE expressions, cron, Graph mailbox setup, &lt;code&gt;onMailReceived&lt;/code&gt; callback reference — is in the &lt;a href="https://github.com/The-Real-Insight/in-concert/blob/main/docs/sdk/usage.md" rel="noopener noreferrer"&gt;SDK usage guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are building agentic workflows and want your processes to have their own lifecycle — starting on a schedule, reacting to emails, running continuously without external triggers — this is the engine layer for that.&lt;/p&gt;

&lt;p&gt;Star the repo. Try it on a real process. Open an issue if something does not work the way you expect. The BPMN subset is growing, and the patterns we are building — timer-driven agents, email-triggered workflows, LLM-routed decisions — are where #agenticbpm gets practical.&lt;/p&gt;

&lt;p&gt;We are The Real Insight GmbH, and we believe BPMN is the orchestration backbone for the agentic era. Processes should not wait to be told when to start. They should know.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/The-Real-Insight/in-concert" rel="noopener noreferrer"&gt;github.com/The-Real-Insight/in-concert&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://www.npmjs.com/package/@the-real-insight/in-concert" rel="noopener noreferrer"&gt;npmjs.com/package/@the-real-insight/in-concert&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://the-real-insight.com" rel="noopener noreferrer"&gt;the-real-insight.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Powered by The Real Insight GmbH BPMN Engine — &lt;a href="https://the-real-insight.com" rel="noopener noreferrer"&gt;the-real-insight.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agenticbpm</category>
      <category>bpmn</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why We Keep Process Data Outside the Engine — and Why It Changes Everything for Agentic BPM</title>
      <dc:creator>Marc Gille-Sepehri</dc:creator>
      <pubDate>Sat, 11 Apr 2026 11:19:37 +0000</pubDate>
      <link>https://forem.com/marcgillesepehri/why-we-keep-process-data-outside-the-engine-and-why-it-changes-everything-for-agentic-bpm-27p4</link>
      <guid>https://forem.com/marcgillesepehri/why-we-keep-process-data-outside-the-engine-and-why-it-changes-everything-for-agentic-bpm-27p4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8bkkfmys0gtxlpdxoy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8bkkfmys0gtxlpdxoy6.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a design decision at the heart of &lt;a href="https://github.com/The-Real-Insight/in-concert" rel="noopener noreferrer"&gt;in-concert&lt;/a&gt; that surprises people when they first encounter it: &lt;strong&gt;the engine knows nothing about your data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No domain objects stored inside the engine. No variable interpolation in BPMN expressions. No built-in scripting that reaches into your database. When a process instance is running, the engine holds exactly one thing that belongs to you: an &lt;code&gt;instanceId&lt;/code&gt;. Everything else — documents, application state, business context, AI responses — lives in your systems, bound to that id.&lt;/p&gt;

&lt;p&gt;This is not an oversight. It is the central architectural choice, and it shapes everything else about how in-concert works. And this is the  beginning of #agenticbpm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Data-Coupled Engines
&lt;/h2&gt;

&lt;p&gt;Traditional BPM engines — Camunda, Flowable, Activiti — manage process variables alongside process state. You deploy a BPMN, you pass in variables, and the engine stores them, interpolates them into conditions, and threads them through the execution. It is convenient at first. Then reality arrives.&lt;/p&gt;

&lt;p&gt;Your process needs to evaluate a condition against data that lives in your ERP. Or the "variable" is actually a 40-page document. Or the service task needs to call an LLM with context assembled from five different sources. Or your security model requires that PII never leaves your own database.&lt;/p&gt;

&lt;p&gt;Suddenly the engine is not a neutral orchestrator. It has become a data store you did not ask for, a security boundary you have to manage, and an integration point that does not understand the shape of your actual domain.&lt;/p&gt;

&lt;p&gt;We built in-concert after running into exactly these problems. The solution was radical simplicity: &lt;strong&gt;the engine does not store your data, period&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Touch Points — and Why They Are All Fuzzy
&lt;/h2&gt;

&lt;p&gt;In any BPMN process, there are three places where execution intersects with the outside world. In a traditional engine, these are handled by scripting, expression languages, and built-in connectors. In in-concert, they are handled by your code — deliberately, explicitly, and with full access to everything you know.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Service Tasks
&lt;/h3&gt;

&lt;p&gt;A service task means "call something external and continue." In a classic engine, you write a connector or a script that runs inside the engine's JVM or Node.js process. The engine manages the call, handles the result, and stores output variables.&lt;/p&gt;

&lt;p&gt;In in-concert, the engine calls your handler and waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onServiceCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Full control. Assemble context from anywhere.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myDataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContextFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;toolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completeExternalTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workItemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler receives the &lt;code&gt;instanceId&lt;/code&gt; and whatever metadata you put on the BPMN node (&lt;code&gt;tri:toolId&lt;/code&gt;, &lt;code&gt;tri:toolType&lt;/code&gt;, custom extensions). It completes when it is done — whether that is 50ms or 50 minutes later, whether via a direct response, a message queue reply, or a webhook. The engine waits. It does not care how long.&lt;/p&gt;

&lt;p&gt;But "call an LLM" understates what the handler can actually do. Consider what happens in a real agentic workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM as context mapper.&lt;/strong&gt; Your process node carries a &lt;code&gt;tri:toolId&lt;/code&gt; that identifies an MCP tool — say, &lt;code&gt;search-crm&lt;/code&gt; or &lt;code&gt;generate-proposal&lt;/code&gt;. The tool has a defined input schema. Your application data has its own shape. The LLM's job here is not to answer a question — it is to map your domain objects into the tool's input format, invoke the tool, and map the structured output back into whatever your process needs next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;onServiceCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;toolId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// e.g. "search-crm"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// get tool + schema&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myDataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContextFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// LLM maps context → tool input schema&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapToToolInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Invoke the MCP tool&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// LLM maps tool output → domain result&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapFromToolOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completeExternalTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workItemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — &lt;strong&gt;LLM as the mapping and reasoning layer around structured tool calls&lt;/strong&gt; — is where BPMN and agentic AI (i.e. #agenticbpm) meet most naturally. The process definition models the flow and the intent. The BPMN node identifies which tool to use. The LLM handles the fuzzy work of bridging between your data model and the tool's contract. And because all of this happens in your code, you can swap models, adjust prompts, and iterate on the mapping logic without touching the process definition at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The handler is also where long-running integration lives.&lt;/strong&gt; Publish a job to a queue, return immediately, and complete the task when the consumer acknowledges. Poll an external system until it is ready. Wait for a webhook. None of this requires anything special from the engine — it simply waits until your handler calls &lt;code&gt;completeExternalTask&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Transition Conditions — the XOR Gateway
&lt;/h3&gt;

&lt;p&gt;This is where the fuzziness gets interesting. In a BPMN XOR gateway, one of several outgoing flows is selected based on a condition. In a traditional engine, you write an expression: &lt;code&gt;${amount &amp;gt; 1000}&lt;/code&gt;, or a FEEL expression, or a Groovy script. The engine evaluates it against stored variables.&lt;/p&gt;

&lt;p&gt;But what if the condition is not a clean boolean expression? What if it is "does this application look fraudulent?" or "is this document complete enough to proceed?" or "based on the conversation so far, which department should handle this?"&lt;/p&gt;

&lt;p&gt;These are not expressions. They are judgements — and judgements require context, and often require an LLM.&lt;/p&gt;

&lt;p&gt;In in-concert, gateway decisions are routed to your handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// payload.transitions is the list of outgoing flows with names and conditions&lt;/span&gt;
    &lt;span class="c1"&gt;// You evaluate — using your data, your rules, your LLM&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myDataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContextFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;myRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decisionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;selectedFlowIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flowId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine gives you the transition options. You choose. The evaluation logic — however simple or sophisticated — belongs to you. You can use a simple &lt;code&gt;if/else&lt;/code&gt;. You can call an LLM with the full application context. You can run a rules engine. The engine does not prescribe how you decide; it only records that you did.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Human Tasks — the Worklist
&lt;/h3&gt;

&lt;p&gt;User interaction is the most obviously fuzzy of the three. A human task is not deterministic. The user brings judgement, context, domain knowledge, and occasionally the wrong answer. The task might be "review this contract," "approve this expense," or "assess whether this customer qualifies."&lt;/p&gt;

&lt;p&gt;In in-concert, human tasks are projected to a queryable worklist. Your UI queries it, filtered by role, by claimed status, by instance. The user sees the task, opens your application where the full document and context live, makes a decision, and your code calls &lt;code&gt;completeUserTask()&lt;/code&gt; with the result.&lt;/p&gt;

&lt;p&gt;The engine never renders a form. It never stores the contract. It never knows what the user saw. It only knows that a human task at a given node in a given process instance was completed with a given result — and it advances accordingly.&lt;/p&gt;

&lt;p&gt;This lets you build any interaction model: cherry-picking worklists, supervisor assignment, AI-assisted pre-screening before human review. The engine is the backbone, not the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Unlocks for Agentic BPM
&lt;/h2&gt;

&lt;p&gt;Here is the part that we find genuinely exciting.&lt;/p&gt;

&lt;p&gt;AI agents need orchestration. A single LLM call is not a workflow — it is a function. Useful, but limited. Real agentic systems involve sequences of steps, parallel branches, human checkpoints, error handling, retries, long-running waits. They need state across time. They need the ability to hand off between AI and human. They need audit trails.&lt;/p&gt;

&lt;p&gt;BPMN is a remarkably good fit for this. It has been modelling complex, long-running processes for decades. It handles parallelism, subprocesses, boundary events, timers, and message correlation out of the box. And it is visual — a BPMN diagram is something a business analyst and a developer can read together.&lt;/p&gt;

&lt;p&gt;in-concert brings BPMN to agentic systems with a clean separation: &lt;strong&gt;the engine handles the orchestration; your code handles the intelligence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Service tasks become LLM invocations. Gateway decisions become LLM evaluations against your domain context. Human tasks become the checkpoints where a person reviews or overrides what the AI decided. And because all data and logic live outside the engine, you can iterate on your prompts, your models, and your routing logic without touching the process definition.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;instanceId&lt;/code&gt; is the thread that holds it together. Every LLM call, every database query, every human task can be correlated to a specific process instance. You know exactly where in the process you are, what decisions were made, and what the audit trail looks like — because in-concert records all of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;in-concert is open source, MIT-licensed (with attribution), and published on npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @the-real-insight/in-concert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK works in two modes. For microservice deployments, run the engine as a standalone service and connect via REST and WebSocket. For embedded or test use, run it directly in-process against MongoDB — same API, no server needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BpmnEngineClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@the-real-insight/in-concert/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// REST mode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BpmnEngineClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Local / embedded mode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BpmnEngineClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full quick start, API reference, and BPMN conformance matrix are in the &lt;a href="https://github.com/The-Real-Insight/in-concert" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. The README documents every SDK method with accurate type signatures pulled directly from the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come Build With Us
&lt;/h2&gt;

&lt;p&gt;in-concert is early. The BPMN subset is intentionally focused — we implement what production workflows actually need, and we fail loudly on anything we do not support yet. There is meaningful work to be done on the conformance surface, the developer experience, and the agentic integration patterns.&lt;/p&gt;

&lt;p&gt;If this resonates with you — if you have built on BPM engines and felt the friction of data-coupled orchestration, or if you are thinking about how to bring structure to agentic AI workflows — we would love to have you involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Star the repo.&lt;/strong&gt; Try it on a real process. Open an issue. Submit a PR. The contribution guide is in &lt;code&gt;docs/contributing.md&lt;/code&gt; and there are &lt;code&gt;good first issue&lt;/code&gt; labels for anyone who wants to start small.&lt;/p&gt;

&lt;p&gt;We are The Real Insight GmbH, and we are building the engine layer for #agenticbpm. This is just the beginning.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/The-Real-Insight/in-concert" rel="noopener noreferrer"&gt;github.com/The-Real-Insight/in-concert&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://www.npmjs.com/package/@the-real-insight/in-concert" rel="noopener noreferrer"&gt;npmjs.com/package/@the-real-insight/in-concert&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://the-real-insight.com" rel="noopener noreferrer"&gt;the-real-insight.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Powered by The Real Insight GmbH BPMN Engine — &lt;a href="https://the-real-insight.com" rel="noopener noreferrer"&gt;the-real-insight.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bpmn</category>
      <category>aiagents</category>
      <category>node</category>
      <category>agenticbpm</category>
    </item>
  </channel>
</rss>
