Inventory export to 3PL
Acme uses an outside warehouse to pack and ship its orders. That warehouse plans tomorrow’s work from a stock file Acme sends over tonight, so it has to know exactly how many of each product is on hand. Every night Acme reads its own stock figures, writes them into a file shaped the way the warehouse wants, and drops that file onto the warehouse’s server before its cut-off. The warehouse picks up the file in the morning and plans the day. If the file is late or missing, the warehouse has nothing to plan from, so a run that does not finish has to raise an alarm rather than fail quietly.
So the goal is plain. Once a night, take a fresh count of what is in stock, turn it into the file the warehouse expects, and put that file on the warehouse’s server before the deadline. The same dated file every night, never half-written, never missing without someone being told.
Here is how Art2link ESB builds that. The trigger is a clock. A Scheduler receive port (a receive port is the entry point that brings work onto the bus, the shared message backbone every flow publishes to and reads from) named NightlyStockClock fires once at 23:00. A two-way SQL Caller send port (a send port is the exit point that hands a message to an outside system, and SQL Caller is the adapter, the connector to a specific kind of system, that talks to a database) subscribes to that tick and calls dbo.GetStockSnapshot, which returns one row per SKU. A subscription is just the rule that decides which messages a port picks up. The SQL Caller returns those rows to the port as its standard SqlCallerResult result; an inbound map (a map reshapes a message from one layout into another) collapses them into the canonical StockSnapshot document and publishes it to the bus. An SFTP Caller send port subscribes to that snapshot, and an outbound map on it shapes the document into the warehouse’s fixed flat layout, their column order, their date format, because the partner’s format is an edge concern, not something the bus should carry: map at the edges. This is the mirror image of the price list import.
{{Config.PortName}} == "NightlyStockClock"
The SFTP send port subscribes to the snapshot and writes the file. Directory and filename are templated, a dated name keeps every night's file distinct, and the adapter's atomic write (upload to a temp name, rename into place) guarantees the 3PL's picker never reads a half-written file:
{{Message.MessageType}} == "StockSnapshot"
/inbound/stock/stock_{{Promoted.StockSnapshot.SnapshotDate}}.csv
When it fails. Set one shared Exception Message Type on both send ports, StockExportFailed, and an O365 Mail port subscribed to it for operations. The upload side deserves generous retries first: partner SFTP servers have maintenance windows, and a file that lands at 23:40 instead of 23:05 is a non-event, while a file that silently never lands is tomorrow's empty picking schedule. Whatever exhausts its retries waits in tracking for replay.
{{Message.MessageType}} == "StockExportFailed"
Build it, step by step. The steps run in dependency order: every object is created before the object that selects it.
Under the application’s Message types, create the two types this flow routes on. A message type is a name plus a format, no schema required:
| Name | Format | Purpose |
|---|---|---|
| StockSnapshot | XML | the canonical stock document the extract port publishes (Step 7) |
| StockExportFailed | XML | the shared exception type both send ports publish failures under (Step 9) |
Promotions live on the message type, so add the one this flow binds while you are here. The SFTP filename (Step 8) binds it, and adapter parameters are plain strings: they bind {{…}} tokens but never evaluate a body path.
| Promotion | Path |
|---|---|
| SnapshotDate | /Stock/SnapshotDate |
Two external parties, two credentials. Under the application’s Authentications, the dialog asks for a Name, the Adapter it pairs with, and a Definition, the credential shape that adapter offers, with the Application preset; the Definition decides the config section that follows.
| Setting | Value |
|---|---|
| Name | ErpDb |
| Adapter | SQL Caller |
| Definition | SQL Server Connection |
| Connection String (Database Config) | the ERP database’s connection string, credentials included |
| Setting | Value |
|---|---|
| Name | Warehouse3plSftp |
| Adapter | SFTP Caller |
| Definition | SFTP Password Authentication |
| Username / Password | the 3PL’s credentials |
| Host Key Fingerprint | the 3PL server’s host-key fingerprint |
Create the stock table and the extract procedure in the ERP database; the extract port (Step 7) calls the procedure. The snapshot date rides on every row so the filename template and the 3PL file agree on it. The SELECT ends in FOR JSON PATH, so the multiple rows come back as a JSON array, the shape the inbound map (Step 4) iterates.
CREATE TABLE dbo.StockOnHand ( Sku VARCHAR(40) NOT NULL PRIMARY KEY, OnHand INT NOT NULL, Reserved INT NOT NULL CONSTRAINT DF_Stock_Res DEFAULT 0 ); CREATE OR ALTER PROCEDURE dbo.GetStockSnapshot AS BEGIN SET NOCOUNT ON; SELECT CONVERT(CHAR(10), SYSUTCDATETIME(), 23) AS SnapshotDate, Sku, OnHand, Reserved FROM dbo.StockOnHand ORDER BY Sku FOR JSON PATH; END;
Under the application’s Maps, create the map the extract port’s inbound flow (Step 7) will select:
| Map setting | Value |
|---|---|
| Name | ResultToStockSnapshot |
| Source message type | SqlCallerResult (JSON), the SQL Caller’s result; this query ends in FOR JSON (Step 3), so the result is a JSON array |
| Target message type | StockSnapshot, from Step 1 |
Because the procedure’s SELECT ends in FOR JSON PATH, the rows come back as a JSON array, one object per SKU:
[
{ "SnapshotDate": "2026-06-04", "Sku": "TP-4501", "OnHand": 118, "Reserved": 12 },
{ "SnapshotDate": "2026-06-04", "Sku": "HX-0090", "OnHand": 64, "Reserved": 0 }
]The map collapses that array into one canonical document, lifting the snapshot date off the first element:
<Stock> <SnapshotDate>2026-06-04</SnapshotDate> <Item><Sku>TP-4501</Sku><OnHand>118</OnHand><Reserved>12</Reserved></Item> <Item><Sku>HX-0090</Sku><OnHand>64</OnHand><Reserved>0</Reserved></Item> </Stock>
Maps are authored in XSLT 3.0 and run on Saxon HE 12.9. JSON in, XML out: parse-json(.) turns the result into an array, ?1?SnapshotDate lifts the date off the first element, and ?* iterates the array:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/"> <xsl:variable name="rows" select="parse-json(.)"/> <Stock> <SnapshotDate><xsl:value-of select="$rows?1?SnapshotDate"/></SnapshotDate> <xsl:for-each select="$rows?*"> <Item> <Sku><xsl:value-of select="?Sku"/></Sku> <OnHand><xsl:value-of select="?OnHand"/></OnHand> <Reserved><xsl:value-of select="?Reserved"/></Reserved> </Item> </xsl:for-each> </Stock> </xsl:template> </xsl:stylesheet>
The second map runs on the upload port’s outbound side (Step 8) and produces the file the 3PL ingests:
| Map setting | Value |
|---|---|
| Name | StockSnapshotTo3plCsv |
| Source message type | StockSnapshot, from Step 1 |
| Target | the 3PL’s flat file, written to the wire as-is |
Semicolon-delimited, date compacted to yyyymmdd, quantity available computed per line:
SKU;QTY_AVAILABLE;SNAPSHOT_DATE TP-4501;106;20260604 HX-0090;64;20260604
One template over the canonical document:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:template match="/Stock"> <xsl:variable name="d" select="translate(SnapshotDate, '-', '')"/> <xsl:text>SKU;QTY_AVAILABLE;SNAPSHOT_DATE </xsl:text> <xsl:for-each select="Item"> <xsl:value-of select="Sku"/> <xsl:text>;</xsl:text> <xsl:value-of select="number(OnHand) - number(Reserved)"/> <xsl:text>;</xsl:text> <xsl:value-of select="$d"/> <xsl:text> </xsl:text> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Create a receive port NightlyStockClock on the Scheduler: Repeat Every 1 / Repeat Unit Days, Daily Window opening 11:00:00 PM.
Create a send port StockExtract on the SQL Caller, subscribing on {{Config.PortName}} == "NightlyStockClock", the clock from Step 6. Everything it references already exists, so each field is a pure selection. (The dropdowns can also create types and maps inline; building the dependencies first keeps each dialog a selection.)
| Setting | Value |
|---|---|
| Adapter | SQL Caller |
| Way | Two |
| Authentication | ErpDb, from Step 2 |
| Command Type | StoredProcedure |
| Command Text | dbo.GetStockSnapshot, from Step 3 |
In the port’s inbound flow, select ResultToStockSnapshot, from Step 4, so the extract is published as StockSnapshot.
Create a send port StockToWarehouse on the SFTP Caller, subscribing on {{Message.MessageType}} == "StockSnapshot". The Filename binds the SnapshotDate promotion from Step 1:
| Setting | Value |
|---|---|
| Adapter | SFTP Caller |
| Way | One |
| Authentication | Warehouse3plSftp, from Step 2 |
| Host | sftp.3pl.example |
| Remote Directory | /inbound/stock |
| Filename | stock_{{Promoted.StockSnapshot.SnapshotDate}}.csv |
| Overwrite Policy | Overwrite |
On the outbound side, select the StockSnapshotTo3plCsv map from Step 5 (Typed, keyed on StockSnapshot).
On both send ports, StockExtract and StockToWarehouse, set Exception Message Type to StockExportFailed, from Step 1, and give the SFTP port generous retries first.
Create an O365 Mail port OpsAlert subscribing on {{Message.MessageType}} == "StockExportFailed".
Everything you configure is live the moment you save it, there is nothing to deploy. Start the send ports first, StockExtract, StockToWarehouse, and OpsAlert, then the receive port NightlyStockClock.
Set Tracking to Enabled + Body on every port the flow touches, so you can walk each run step by step, with its message body, in tracking.
To get a tick without waiting for 23:00, temporarily set NightlyStockClock to Repeat Every 1 / Repeat Unit Minutes with no Daily Window, let it fire once, then restore the nightly schedule. Confirm the dated file lands complete on the 3PL server (the atomic write means no partial file is ever visible), and the run reads cleanly in tracking. Re-run the same night and confirm the file overwrites its own date.
Block the upload and confirm the alert plus replay.