Skip to content
iQBus HomeDocumentationBlogContact
Updated September 7, 2026
Tutorials/Getting started/Shipment status push

Shipment status push

A warehouse packs and ships customer orders. As each one moves along, the warehouse system marks it picked, then packed, then shipped, but it only writes those updates into its own database. It never reaches out to tell anyone. Meanwhile the online store wants to show customers where their order is. So the store needs each of those status changes pushed to it as it happens. Since the warehouse cannot send anything itself, something has to check it regularly, notice what is new, and pass each update to the store. The end result is that every status change shows up in the store, once, a few minutes after it happens.

So the job is a steady loop. Every few minutes, look in the warehouse for status changes you have not sent yet, and push each new one to the store.

Every new status reaches the store within minutes Clock every few minutes Check for new status changes Warehouse picked, packed, shipped new ones Online store shows the customer where the order is

The warehouse system is the WMS, the store wants the updates on its REST API, and iQBus bridges the two with a polling consumer: a clock, a query, and a call. Here is how it builds it.

Everything in iQBus rides a shared message backbone called the bus: parts of the flow publish messages onto it, and other parts subscribe to the ones they care about, so the pieces never call each other directly. A receive port (an inbound endpoint that puts messages onto the bus) on the Scheduler (an adapter, a connector to one kind of outside system, here a clock) fires every five minutes and publishes a tick, typed ShipmentPollTick. A send port (an outbound endpoint that picks messages up and acts on them) bound to the SQL Caller in two-way mode subscribes to that tick (a subscription is the filter a port uses to pick up only the messages it wants) and runs the pick-up procedure:

EXPRESSION
{{Message.MessageType}} == "ShipmentPollTick"

That procedure ends in FOR XML, so its result comes back as the XML SqlCallerResult envelope; a splitter (a pipeline component, a small piece of custom code that reshapes a message in transit) turns it into one message per status row, and a map (a named rule that turns one message shape into another) shapes each row into a canonical ShipmentStatus message on the bus. You write neither the splitter nor the map by hand: the AI Accelerator generates both from a plain-language description, and the working code is in this page as a fallback.

The query reads everything past a high-water mark, a LastExportedId the procedure advances in the same transaction that returns the rows. The interval decides how often you look; the mark decides what you pick up, so a missed or doubled poll neither drops nor repeats work.

A second send port, bound to the API Caller, subscribes to the result and POSTs each status to the storefront, addressing the order by the order number the map promotes:

EXPRESSION
{{Message.MessageType}} == "ShipmentStatus"
Scheduler every 5 min BUS tick SQL Caller (two-way) split + map the rows WMS DB ShipmentStatus ×N API Caller StatusToStorefront Storefront API failed ShipmentSyncFailed routed to a failures table Activity Notification fallback, emails operations

When it fails. Two ports can fail, the pick-up and the push, and both carry the same Exception Message Type, ShipmentSyncFailed. On an error the in-flight message is republished under that type with its payload untouched, so the payload tells you which leg broke: an empty body is a tick whose pick-up failed, a JSON status is a push that failed. A third send port subscribes to it and writes each failure into a dead-letter table operations works from, so the failure is handled, and the run does not suspend:

EXPRESSION
{{Message.MessageType}} == "ShipmentSyncFailed"

Underneath that sits an Activity Notification, a per-Application email subscription that fires on any failed run. It is the safety net for what the exception route cannot foresee, including the route itself failing: it only tells a person, and the message it reports on stays suspended in tracking for replay. Step 7 builds both and explains why you want both.

Advance the mark with the read. If the high-water mark moves in a separate step, a crash between the two either re-sends or skips a window of statuses. One procedure, one transaction: return the rows and advance LastExportedId together.

Build it, step by step. The steps follow the integration itself, from the clock to the storefront, not the order that is quickest to click through. Every Authentication, message type, pipeline and map is created from the dialog that first asks for it, so you never have to leave the port you are configuring to go and make something it needs. The warehouse database objects get a step of their own just before the port that calls them.

Before you start. Everything in this walkthrough lives inside one Application. Under Applications, create it and select it, so every port, map, pipeline and message type below is created in its scope. The Application is also where tracking is set, and one choice covers every port in it: while you are building, use Enabled + Body, which records each run step by step with the message body, so you can open any tick in tracking and see exactly what was picked up, split, mapped and pushed. Once the flow is proven and moves to production, drop it to Only on Error if you wish.
Application
SettingValue
NameAcmeShipments
NamespaceAcmeShipments
TrackingEnabled + Body
DescriptionWarehouse shipment statuses pushed to the storefront
1
Step One
Create the clock

The integration begins with the thing that sets it in motion, the clock. Under the application’s Ports, create a receive port bound to the Scheduler. The Scheduler talks to nothing outside the bus, so it is the one adapter with no Authentication to create; its whole configuration is the cadence. If you have to step away mid-setup, Save Draft keeps the port under just its name; a draft does not exist to the runtime until the required fields are filled in and the port is saved properly.

Receive port
SettingValue
NameShipmentStatusClock
ApplicationAcmeShipments (preset)
Port TypeReceive
AdapterScheduler
Adapter Message TypeShipmentPollTick, created below, at this field
Exception Message Typeempty; a Scheduler tick has nothing to fail on the way in, the failures this flow handles are on the send ports
WayOne (the Scheduler offers nothing else; there is no caller waiting for a reply)
Configure the adapter

Open the adapter configuration (the button beside Adapter). Five minutes is a sensible cadence for a status feed: quick enough that the storefront feels live, slow enough that an empty poll costs nothing worth counting. Leave the Payload empty, the tick is only a wake-up call; the message still carries the standard envelope, and that is all the pick-up port needs.

Adapter configuration
SettingValue
Repeat Every5
Repeat UnitMinutes
Time Zoneyours; it only matters to the optional Date and Daily Windows, the cadence itself is interval-based
Date Window, Daily Windowoff; the warehouse ships around the clock
Payloadempty
Type the tick at the adapter

Back on the port’s General tab, one row of the table above is still open, the required Adapter Message Type. The runtime applies it the instant the Scheduler releases the tick, so even an empty message is typed before anything else runs, and that type is what the pick-up port subscribes to in Step 3. Nothing exists yet, so create the message type here at the field. A message type is a name plus a Format, no schema required; the body is empty, so plain text describes it best and there is nothing to promote.

Message type, the tick
SettingValue
NameShipmentPollTick
FormatTXT
Sample dataempty, the tick carries no body
Promotionsnone, the subscription matches on the type alone

Saving the type returns you to the port with Adapter Message Type now reading ShipmentPollTick, which completes the General table above. Save the port. ShipmentStatusClock is complete, stopped, and waiting; it is the last port you start, in Step 8.


2
Step Two
Create the warehouse database objects

The warehouse database is the system you are integrating with, not part of iQBus, so it is set up by hand rather than asked of the AI Accelerator. In it, create a procedure that returns all status rows with an id above LastExportedId and advances the mark in the same transaction; the pick-up port in Step 3 calls it. This is the high-water mark that makes the poll safe to miss or repeat: a missed or repeated tick neither drops nor double-sends. The result SELECT ends in FOR XML PATH('Row'), ROOT('Result'), so the SQL Caller’s response is the XML envelope the splitter in Step 4 parses.

The source and watermark tables

The warehouse writes ShipmentStatus; ExportWatermark remembers how far this feed has read.

SQL
CREATE TABLE dbo.ShipmentStatus (
    ShipmentId  BIGINT      NOT NULL PRIMARY KEY,
    OrderNumber VARCHAR(40) NOT NULL,
    Status      VARCHAR(20) NOT NULL,
    StatusAt    DATETIME2   NOT NULL
);

CREATE TABLE dbo.ExportWatermark (
    FeedName       VARCHAR(60) NOT NULL PRIMARY KEY,
    LastExportedId BIGINT      NOT NULL CONSTRAINT DF_Wm DEFAULT 0
);
INSERT INTO dbo.ExportWatermark (FeedName, LastExportedId)
VALUES ('ShipmentStatus', 0);
The pick-up procedure
SQL
CREATE OR ALTER PROCEDURE dbo.GetNewShipmentStatuses
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @from BIGINT, @to BIGINT;

    BEGIN TRANSACTION;
        SELECT @from = LastExportedId
        FROM dbo.ExportWatermark WITH (UPDLOCK, HOLDLOCK)
        WHERE FeedName = 'ShipmentStatus';

        SELECT @to = MAX(ShipmentId) FROM dbo.ShipmentStatus;

        SELECT ShipmentId, OrderNumber, Status, StatusAt
        FROM dbo.ShipmentStatus
        WHERE ShipmentId > @from
        ORDER BY ShipmentId
        FOR XML PATH('Row'), ROOT('Result');

        UPDATE dbo.ExportWatermark
        SET LastExportedId = ISNULL(@to, @from)
        WHERE FeedName = 'ShipmentStatus';
    COMMIT;
END;
The SQL Caller returns what the SELECT asks for. This query ends in FOR XML, so the result is XML, and the splitter you build in Step 4 consumes XML to match. A plain SELECT would have returned JSON, which the splitter would not recognise. Decide the format in the SELECT, then give everything downstream a source that agrees with it.

3
Step Three
Create the SQL pick-up port

Under the application’s Ports, create a send port bound to the SQL Caller in two-way mode. Rather than being told what to deliver, a send port states which messages it wants; this one wants every tick the clock publishes. Two-way means the procedure’s result comes back into the port and is published to the bus again, which is where Steps 4 and 5 do their work.

Send port
SettingValue
NameShipmentPickup
ApplicationAcmeShipments (preset)
Port TypeSend
AdapterSQL Caller
Adapter Message TypeSqlCallerResult, the type of the result coming back; a send port carries one only in two-way mode, and Step 4 sets it
Exception Message TypeShipmentSyncFailed, created next, at this field
WayTwo
Subscription{{Message.MessageType}} == "ShipmentPollTick"
Name the failure while you are here

Every port has an Exception Message Type. When a run fails, the in-flight message has its type swapped to this one and is republished to the bus, payload untouched, so that another port can pick the failure up and do something with it; without it, the failed run simply suspends. The type does not exist yet, so create it at the field. Its Format describes what will most often be inside it: for the push port in Step 6 that is a JSON status, for this port it is the empty tick, and JSON reads both. Step 7 gives the type its route.

Message type, the failure
SettingValue
NameShipmentSyncFailed
FormatJSON
Sample datanone; the body is whatever was in flight when the run failed
Promotionsnone
Configure the adapter

Open the adapter configuration. At the Auth Config field, create the warehouse database credential in place; the dialog opens on top of the port and returns you to it when you save:

Authentication, warehouse database
SettingValue
NameWarehouseDbSql
AdapterSQL Caller (preset)
DefinitionSQL Server Connection
Connection String (Database Config)one of the two below
Which connection string

The SQL Caller connects from where iQBus runs, so two things decide the string: how iQBus reaches the server and how the server identifies iQBus. Reachability first: an Azure SQL database on a public endpoint needs iQBus’s outbound address allowed through the server firewall; one behind a private endpoint has to sit in, or be peered with, the iQBus network; an on-premises SQL Server, which is where a WMS database usually lives, is reached over a VPN or ExpressRoute link, and its hostname has to resolve from the iQBus side. Then the identity, which is what the two strings differ in. iQBus supports SQL authentication and managed identity; it does not support Entra ID interactive sign-in, and putting an Entra account’s username and password into a connection string is strongly discouraged, so neither appears here. Replace the placeholders in angle brackets with your own values:

MethodConnection StringUse it when
SQL authenticationServer=tcp:<server>.database.windows.net,1433;Database=<database>;User ID=<login>;Password=<password>;Encrypt=True;TrustServerCertificate=False;the simplest to start with, and the only option for a SQL Server that is not Entra-integrated; the password lives in this Authentication, encrypted at rest
Managed identityServer=tcp:<server>.database.windows.net,1433;Database=<database>;Authentication=Active Directory Managed Identity;Encrypt=True;no secret at all: the iQBus deployment’s own Azure identity is granted access on the database (CREATE USER [<identity>] FROM EXTERNAL PROVIDER, then the roles it needs); the strongest choice for production

For an on-premises server, swap the Server= value for its hostname or IP and port and use SQL authentication. For this tutorial, SQL authentication against a database whose firewall lets iQBus in is enough. Whichever you pick, the login needs EXECUTE on dbo.GetNewShipmentStatuses, and in Step 7 on dbo.RecordShipmentSyncFailure as well.

Back in the adapter configuration, set the command. The procedure takes no parameters, so there are no Input Parameters rows:

Adapter configuration
SettingValue
Auth ConfigWarehouseDbSql, just created
Command TypeStored Procedure
Command Textdbo.GetNewShipmentStatuses, from Step 2
Input Parametersnone

Leave the port open; the next two steps continue on its Response side.


4
Step Four
Split the result into one message per row

Still inside ShipmentPickup, move to the Response side, the send port’s inbound leg, where the procedure’s result re-enters the bus. Because a message is entering here, the response side carries a required Adapter Message Type, applied the instant the SQL Caller releases the result to the next step inside the port. The SQL Caller hands its two-way result over under the standard name SqlCallerResult; select it here. If the picker does not offer it yet, create it at the field with the values below, its Format is XML because the procedure ends in FOR XML.

The envelope the SQL Caller returns

One <Row> per status row, each column an element. Two rows are shown; a tick may return none, one, or hundreds:

XML
<Result>
  <Row><ShipmentId>88412</ShipmentId><OrderNumber>SO-10472</OrderNumber><Status>Shipped</Status><StatusAt>2026-06-04T16:12:00</StatusAt></Row>
  <Row><ShipmentId>88413</ShipmentId><OrderNumber>SO-10488</OrderNumber><Status>Packed</Status><StatusAt>2026-06-04T16:14:30</StatusAt></Row>
</Result>
Message type, the result envelope
SettingValue
NameSqlCallerResult
FormatXML
Sample datathe Result XML above
Promotionsnone, the envelope is split before anything routes on it
Adapter Message Type, the open row of the Step 3 table
SettingValue
Adapter Message TypeSqlCallerResult
The pipeline, and the splitter inside it

Next comes the response Pipeline. One envelope holding many rows is no use to the storefront, which wants one call per status, so a splitter has to turn the envelope into one message per <Row>. A port never selects a component directly; it selects a pipeline, a named, ordered list of pipeline components that also holds the default value for each component’s configuration property. Create the pipeline at the Pipeline field, and the component from the pipeline’s Components list; the component is where the code lives, and it is the first of the two iQBus artifacts in this tutorial you do not have to write yourself.

Build it with the AI Accelerator

You do not have to write this component by hand. Open the AI Accelerator, describe the splitter in plain language, and let it generate the C# for you to review and save. It knows how iQBus pipeline components work, the base class, how a component returns many messages and types each one, but it sees nothing of your Application: not the ports, not the message types you have created, nothing but the text you give it. So the prompt carries everything the component needs: the envelope it reads, what each emitted message must contain, the property that names the type, and what to do with an empty result. A prompt like this is enough:

AI ACCELERATOR PROMPT
Build a pipeline component named SqlCallerResultSplitter that splits an XML result envelope into one message per row.

The inbound body is an XML document whose root is Result and whose children are Row elements, each Row holding one column per child element. Emit one message per Row, with the Row element itself, children included, as the outgoing body. Stamp every emitted message with the message type given by a required component property named RowMessageType, a string with no default, so the pipeline decides the type. An envelope with no Row elements is normal, not an error: return no messages so nothing is published for that run. If the body is not well-formed XML, fail the component with a clear error message.

The envelope it reads looks like this (yours will carry different values and different column names, but the same Result and Row structure):

<Result>
  <Row><ShipmentId>88412</ShipmentId><OrderNumber>SO-10472</OrderNumber><Status>Shipped</Status><StatusAt>2026-06-04T16:12:00</StatusAt></Row>
  <Row><ShipmentId>88413</ShipmentId><OrderNumber>SO-10488</OrderNumber><Status>Packed</Status><StatusAt>2026-06-04T16:14:30</StatusAt></Row>
</Result>

Each message it emits must be exactly one Row, like this:

<Row><ShipmentId>88412</ShipmentId><OrderNumber>SO-10472</OrderNumber><Status>Shipped</Status><StatusAt>2026-06-04T16:12:00</StatusAt></Row>

The AI Accelerator writes a first draft, not a finished component: read what it produced, check that the property is named RowMessageType exactly, since the pipeline table below sets it by that name, and confirm in Step 8 that each split message in tracking is one <Row>. If you would rather start from working code, expand it here to read or copy.

Show the full component code (C#), expand to read or copy

The component parses the body with XDocument, takes every Row under the root and returns one PipelineMessage per row, each typed from the configuration. An empty list means nothing is published for that tick. The configuration class has one property, marked [Required] with no default, which is why the pipeline has to supply it. Because it knows nothing about shipments, the same component serves any FOR XML PATH('Row'), ROOT('Result') query, and the FHIR lab results tutorial reuses it in its own Application.

C#
using System.Xml.Linq;
using System.ComponentModel.DataAnnotations;
using CC.iQBus.Pipelines.Domain.Models.PipelineComponents;

public sealed class SqlSplitterConfig
{
    // Message Type assigned to each row message before the map runs.
    [Required] public string RowMessageType { get; set; } = string.Empty;
}

public sealed class SqlCallerResultSplitter
    : PipelineComponentBase<SqlSplitterConfig>
{
    public override string Name => "SqlCallerResultSplitter";

    protected override Task<PipelineComponentOutput> ExecuteAsync(
        PipelineComponentInput input,
        SqlSplitterConfig config,
        CancellationToken cancellationToken)
    {
        try
        {
            var doc  = XDocument.Parse(input.Body);
            var rows = doc.Root?.Elements("Row") ?? Enumerable.Empty<XElement>();

            var messages = rows
                .Select(r => new PipelineMessage
                {
                    Body        = r.ToString(),
                    MessageType = config.RowMessageType
                })
                .ToList();

            return Task.FromResult(new PipelineComponentOutput
            {
                Success  = true,
                Messages = messages   // empty list => nothing published
            });
        }
        catch (Exception ex)
        {
            return Task.FromResult(new PipelineComponentOutput
            {
                Success      = false,
                ErrorMessage = $"SqlCallerResultSplitter: {ex.Message}",
                Exception    = ex
            });
        }
    }
}
Generated output varies, so treat the samples on this page as examples. The Accelerator does not return the exact same code twice, and neither will your draft match the collapsed version line for line. What has to hold regardless of the draft is the contract the rest of the flow depends on: the component property is named RowMessageType, each emitted body is one <Row> element, and the map in Step 5 produces the paths its promotion reads. With tracking on Enabled + Body, open any run in Step 8 to see the real bodies.
Pipeline component
SettingValue
NameSqlCallerResultSplitter
Codethe C# the AI Accelerator produced, or the collapsed code above

Save the component and you are back in the pipeline with it listed. Its one property has no default in code, so the pipeline is where you say what each split row is classified as. The value is plain text, the name a message type will carry; the type itself is created in the next step, from the map’s Source field, under exactly this name:

Pipeline
SettingValue
NameShipmentRowSplitPipeline
ComponentsSqlCallerResultSplitter, just created
RowMessageType (default for the component)ShipmentRow
Response pipeline
SettingValue
PipelineShipmentRowSplitPipeline, just created

Leave the port open; the next step continues on the Response side’s Map step.


5
Step Five
Build the row map, and the ShipmentStatus it produces

Still on the Response side of ShipmentPickup, go to the Map step and set Map Assignment to Typed. Add one row and, at its Map field, create the map. A map is not a file, it is a named resource defined by its name and the two message types it sits between, and neither type exists yet, so both are created from the map dialog’s own fields:

Map
SettingValue
NameShipmentRowToStatus
Source message typeShipmentRow, created next, at this field
Target message typeShipmentStatus, created after it, at the Target field
The split row

The source is what the splitter emits, one <Row>, and its name must be exactly the RowMessageType you typed into the pipeline in Step 4, or the split messages will carry a type that no map matches. Paste one row into Sample data: it has no runtime effect, but it documents the shape and gives the map tester something to run.

XML
<Row>
  <ShipmentId>88412</ShipmentId>
  <OrderNumber>SO-10472</OrderNumber>
  <Status>Shipped</Status>
  <StatusAt>2026-06-04T16:12:00</StatusAt>
</Row>
Message type, the split row
SettingValue
NameShipmentRow
FormatXML
Sample datathe Row XML above
Promotionsnone, nothing routes on the raw row
The canonical ShipmentStatus

Now the target, from the Target field. This is the shape the storefront consumes, and the one value the API send port needs outside the body, the order number that goes into the URL in Step 6, is lifted out of it by a promotion. A promotion is a named path into the payload (JPath for a JSON type), so before naming it, look at the payload the map will produce:

JSON
{
  "orderNumber": "SO-10472",
  "status": "Shipped",
  "statusAt": "2026-06-04T16:12:00",
  "source": "wms"
}

Paste that JSON into the type’s Sample data first. Then add the promotion row, open its ellipsis menu and choose Generate Path: it shows the sample, you click the node you want, and the JPath is written for you. Click orderNumber and you get $.orderNumber. You can also type the path by hand, but the tool removes the guesswork on deeper shapes.

Message type, the canonical status
SettingValue
NameShipmentStatus
FormatJSON
Sample datathe ShipmentStatus JSON above
Promotions on ShipmentStatus
PromotionPathRead by
OrderNumber$.orderNumberthe URL Template of the API send port, Step 6

Adapter fields are plain strings: they bind {{…}} tokens such as {{Promoted.ShipmentStatus.OrderNumber}} but never evaluate a body path themselves, which is why the promotion has to exist on the type. Save the type; you are back in the map with both types selected.

Build it with the AI Accelerator

Now the map body, the second artifact you do not write by hand. Open the AI Accelerator, describe the translation in plain language, and let it generate the XSLT for you to review and paste into the map. It knows how iQBus maps read XML and emit JSON, but it sees nothing of your Application: not the two types you just created, not the promotion, nothing but the text you give it. So the prompt carries everything the map needs: the Row sample, the ShipmentStatus sample, and the path the promotion reads. A prompt like this is enough:

AI ACCELERATOR PROMPT
Build a map from an XML message type named ShipmentRow to a JSON message type named ShipmentStatus.

Match the row's columns to the status fields by meaning: the order number, the status word, and the timestamp of the status change, all as strings. Leave the shipment id out, the storefront has no use for it. Add one constant field, source set to wms. One path must be exactly $.orderNumber, because a promotion reads it. Output indented JSON.

The ShipmentRow source looks like this (yours will carry different values but the same shape):

<Row>
  <ShipmentId>88412</ShipmentId>
  <OrderNumber>SO-10472</OrderNumber>
  <Status>Shipped</Status>
  <StatusAt>2026-06-04T16:12:00</StatusAt>
</Row>

The ShipmentStatus target must look like this:

{
  "orderNumber": "SO-10472",
  "status": "Shipped",
  "statusAt": "2026-06-04T16:12:00",
  "source": "wms"
}

The AI Accelerator writes a first draft, not a finished map, so test it before you rely on it. The map editor has a tester built in: paste the sample Row as the input, run the map, and compare the result with the ShipmentStatus sample above. What has to match exactly is the promoted path, $.orderNumber; field order and formatting may differ, because the Accelerator does not return the exact same result twice. If you would rather start from working code, expand it here to read or copy.

Show the full map code (XSLT 3.0), expand to read or copy

Maps are authored in XSLT 3.0 and run on Saxon HE 12.9. The source is XML, so the template matches the Row element directly and reads each column as a child element; the result is built as a map{} and serialized to JSON with method="text" output.

XSLT 3.0
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="/Row">
    <xsl:value-of select="serialize(
      map {
        'orderNumber': string(OrderNumber),
        'status': string(Status),
        'statusAt': string(StatusAt),
        'source': 'wms'
      },
      map { 'method': 'json', 'indent': true() })"/>
  </xsl:template>
</xsl:stylesheet>

Save the map, and the row on the Response side’s Map step reads:

Map assignment
Source Message TypeMapTarget Message Type
ShipmentRowShipmentRowToStatusShipmentStatus

Save the port. ShipmentPickup is complete: every tick it picks up becomes zero or more ShipmentStatus messages on the bus, one per new row.


6
Step Six
Create the API push port

Under the application’s Ports, create a send port bound to the API Caller. It subscribes to every canonical status the pick-up port publishes and POSTs it to the storefront, one call per status. The Exception Message Type already exists from Step 3, so this time it is a selection:

Send port
SettingValue
NameStatusToStorefront
ApplicationAcmeShipments (preset)
Port TypeSend
AdapterAPI Caller
Adapter Message Typenone; a one-way send port publishes nothing back
Exception Message TypeShipmentSyncFailed, from Step 3
WayOne
Subscription{{Message.MessageType}} == "ShipmentStatus"

The message is already in the storefront’s shape, so the port’s Map and Pipeline steps stay empty; everything happens in the adapter.

Configure the adapter

Open the adapter configuration. At the Auth Config field, create the storefront credential in place. The storefront issued a username and password for its API, so the Definition is Basic Authentication; the adapter builds the Authorization header from it on every call, you never type that header yourself:

Authentication, storefront API
SettingValue
NameStorefrontApi
AdapterAPI Caller (preset)
DefinitionAPI Basic Authentication
Usernamewms-sync
PasswordSf!2026-status-push-9c4e7a

The values above are fine for the tutorial, and the Postman mock server in Step 8 accepts anything; for the real storefront use the credentials its team issued, and keep the password out of email. Back in the adapter configuration, fill in the call. The URL names the order the status belongs to, which is what the promotion from Step 5 is for; the body is the canonical status exactly as the map produced it:

Adapter configuration
SettingValue
Auth Config TypeStatic
Auth ConfigStorefrontApi, just created
HTTP MethodPOST
URL Templatehttps://shop.acme.example/api/orders/{{Promoted.ShipmentStatus.OrderNumber}}/shipment-status
Content-Typeapplication/json
Request Headersnone; the Authorization header comes from the Authentication
Request Body{{Message.Body}}

Save the port. In Step 8 you will come back and swap the storefront host for the Postman mock server’s address, so the test has something real to hit.


7
Step Seven
Handle failures, twice

Two things can go wrong in this flow, and you already named them: the pick-up fails, so the tick is republished as ShipmentSyncFailed with an empty body, or a push fails, so the status is republished as ShipmentSyncFailed with the JSON status as its body. Right now nothing subscribes to that type, so a failure would go nowhere. This step configures failure handling twice, because the two mechanisms do different jobs.

Two mechanisms, two jobs. The Exception Message Type and its route are part of the integration: they handle the failures you can foresee, by giving the failed message a real destination, here a dead-letter table operations works from. Because the failure is handled, the run does not suspend. The Activity Notification is monitoring, not integration: it reacts to any failed run in the Application, tells a person, and does nothing else; it routes nothing and handles nothing, so the message it reports on does suspend and waits in tracking for replay. It is the safety net for what you did not foresee, and the exception route itself failing is the clearest example, since a warehouse database that is down takes the dead-letter table down with it. Build the route for what you know can break; build the notification for everything else.
The dead-letter table and procedure

The route needs somewhere to put the failure. Operations already lives in the warehouse database, so the dead-letter table goes there for this tutorial; in production you might prefer a database of your own. Like Step 2, this is scaffolding for an external system, so it is supplied as SQL rather than asked of the AI Accelerator. The procedure classifies the failure from its payload: an empty body can only be a tick, so it is a pick-up failure; anything else is a push that did not land.

SQL
CREATE TABLE dbo.ShipmentSyncFailures (
    FailureId  BIGINT        IDENTITY(1,1) NOT NULL PRIMARY KEY,
    Leg        VARCHAR(10)   NOT NULL,          -- PickUp or Push
    Payload    NVARCHAR(MAX) NULL,              -- the status that was in flight, empty for a tick
    FailedAt   DATETIME2     NOT NULL CONSTRAINT DF_SyncFail_At DEFAULT SYSUTCDATETIME(),
    ResolvedAt DATETIME2     NULL               -- operations sets this once re-sent
);
SQL
CREATE OR ALTER PROCEDURE dbo.RecordShipmentSyncFailure
    @Payload NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    INSERT INTO dbo.ShipmentSyncFailures (Leg, Payload)
    VALUES (CASE WHEN NULLIF(LTRIM(RTRIM(@Payload)), '') IS NULL THEN 'PickUp' ELSE 'Push' END,
            @Payload);
END;
The exception route

Under the application’s Ports, create one more send port on the SQL Caller, this time one-way: it subscribes to the failure type and calls the procedure above. It reuses the WarehouseDbSql Authentication from Step 3, so at the Auth Config field this is a selection, not a creation. Leave its own Exception Message Type empty: a route that republishes its own failures as ShipmentSyncFailed would pick them up again, and this is exactly the case the Activity Notification below exists for.

Send port
SettingValue
NameFailedSyncToDeadLetter
ApplicationAcmeShipments (preset)
Port TypeSend
AdapterSQL Caller
Adapter Message Typenone; one-way
Exception Message Typeempty, on purpose
WayOne
Subscription{{Message.MessageType}} == "ShipmentSyncFailed"
Adapter configuration
SettingValue
Auth ConfigWarehouseDbSql, from Step 3
Command TypeStored Procedure
Command Textdbo.RecordShipmentSyncFailure
Input Parameters
ParameterValue
Payload{{Message.Body}}

Save the port. From now on a failed push lands as a Push row with the status JSON in Payload, ready for operations to re-send once the storefront is back, and the run itself completes as handled.

The safety net

Under the application’s Activity Notifications, create the notification. With Subscription on All and On Error Only on, it fires for any failed run in the Application, whichever port failed and whether or not you foresaw it:

Activity Notification
SettingValue
NameShipmentSyncErrors
ApplicationAcmeShipments (preset)
On Error OnlyOn
SubscriptionAll
ModeOn Event
Recipientsone row: Name Operations, Email ops@acme.example
Is EnabledEnabled
One-time prerequisite. Notifications go out through the platform email channel, configured once for the whole installation under Email notification configuration (an Entra app registration with Mail.Send). Until that is done, the notification can be created and enabled but sends nothing. There is no per-Application mail credential.
Do not let the notification stand in for the route. An email that says "a push failed" leaves the status stranded until someone reads the email, finds the run, and replays it. The dead-letter row is the status, already where operations can act on it, and the run is finished. Keep the notification for the cases the route cannot reach; when one arrives, the right response is usually to add a route for it.

8
Step Eight
Start the ports and test with Postman
Stand in for the storefront

The storefront in this tutorial does not exist, so give the API Caller something real to call. We use a Postman mock server; any request-capturing tool (Beeceptor, webhook.site, a tiny local API) works the same way. Install Postman, sign in (mock servers need an account), and create the mock: New → Mock Server, add one request row, name the mock and create it. Postman shows the mock’s address, something like https://a1b2c3d4-....mock.pstmn.io; copy it.

Mock server request
SettingValue
NameAcmeStorefrontStandIn
MethodPOST
Request URL/api/orders/:orderNumber/shipment-status (the colon makes the order number a path variable, so any order matches)
Response code200
Response body{"accepted": true}

Open StatusToStorefront and, in its adapter configuration, replace https://shop.acme.example in the URL Template with the mock’s address, keeping the path and the promotion token exactly as they were. The mock ignores the Basic Authentication header, which is fine here. Save the port.

Start in dependency order

There is nothing to deploy, everything you configured is already saved and live, because iQBus applies changes immediately. To bring the flow online you start the send ports first, then the receive port, and among the send ports, the ones furthest downstream first: start FailedSyncToDeadLetter, then StatusToStorefront, then ShipmentPickup, and last the clock, ShipmentStatusClock. Starting each subscriber before its producer means no tick fires before the pick-up is listening, and no status is published before the push is ready to take it. (To take the flow offline, stop the clock first, for the same reason.) Tracking is already Enabled + Body from the Application, so every run you make now is recorded step by step with its payload. If five minutes is too long to wait between attempts, set Repeat Every on the clock to 1 for the duration of the test and restore it afterwards; Restart the port to reset its timer.

Feed the warehouse

The trigger in this flow is not an HTTP call but a row appearing in the warehouse. Insert two statuses, then wait for the next tick:

SQL
INSERT INTO dbo.ShipmentStatus (ShipmentId, OrderNumber, Status, StatusAt) VALUES
    (88412, 'SO-10472', 'Shipped', '2026-06-04T16:12:00'),
    (88413, 'SO-10488', 'Packed',  '2026-06-04T16:14:30');

Then open Tracking, find the run the tick started, and step through it, opening each message body as you go:

Step in TrackingWhat to confirm
Scheduler firesan empty message typed ShipmentPollTick is published
ShipmentPickup picks it upthe subscription matched and dbo.GetNewShipmentStatuses was called
SQL Caller respondsthe body is the <Result> envelope with two <Row> elements, typed SqlCallerResult
Pipeline ShipmentRowSplitPipelinetwo messages come out, each one <Row>, typed ShipmentRow
Map ShipmentRowToStatuseach body is now the canonical JSON with orderNumber, status, statusAt, source
Publishtwo ShipmentStatus messages are on the bus and StatusToStorefront picked up both
API Callertwo POSTs, to /api/orders/SO-10472/shipment-status and /api/orders/SO-10488/shipment-status, each answered 200

Confirm the other end too: in Postman, open the mock server and its call log shows the two requests with the status JSON as their bodies. Then check the mark moved: SELECT LastExportedId FROM dbo.ExportWatermark now reads 88413, and the next tick, with nothing new, runs the procedure, returns an empty <Result/>, and the splitter publishes nothing, which tracking shows as a run that ends at the pipeline with no messages out. Insert a third row and confirm exactly one more push.

Break it on purpose, first the route

Make the push fail: in the mock, change the example’s response code to 500 and save, then insert a new status row and wait for the tick. The pick-up succeeds, the status is published, and StatusToStorefront fails after its retries. Watch what happens next in tracking: the message is republished as ShipmentSyncFailed, FailedSyncToDeadLetter picks it up and calls the procedure, and the run finishes as handled, not suspended. In the database, SELECT * FROM dbo.ShipmentSyncFailures shows one Push row with the status JSON as its Payload. No email is sent, because nothing was left unhandled. Set the mock’s response back to 200.

Then the safety net

Now take away something the route depends on: stop the warehouse database, or temporarily revoke EXECUTE on dbo.GetNewShipmentStatuses, and wait for the tick. The pick-up fails, the tick is republished as ShipmentSyncFailed, and the route tries to write the failure to the same database, which fails too. This is the case no route can handle, and it is where the notification earns its keep: the operations mailbox gets the ShipmentSyncErrors email from Step 7, and the failed run sits suspended in tracking with its payload intact, available for replay once the database is back. Restore the database or the permission, replay the suspended run, and confirm it completes.

Before production. Point StatusToStorefront back at the real storefront host, restore the clock to five minutes, and once the flow is proven, open the Application and drop tracking to Only on Error if body retention is a concern; failed runs keep their payload for replay either way.