Skip to content
Art2link ESB HomeDocumentationBlogContact
Updated May 5, 2026
Tutorials/Getting started/Inventory export to 3PL

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.

A nightly stock file for the warehouse Every night at the cut-off Count the stock how many on hand Shape the file warehouse format Warehouse server dated stock file lands Warehouse picks it up plans the morning’s picking

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.

EXPRESSION
{{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:

EXPRESSION
{{Message.MessageType}} == "StockSnapshot"
TEMPLATE
/inbound/stock/stock_{{Promoted.StockSnapshot.SnapshotDate}}.csv
Scheduler nightly 23:00 BUS SQL Caller (two-way) stock extract ERP DB StockSnapshot map → 3PL layout SFTP Caller atomic write 3PLSFTP

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.

EXPRESSION
{{Message.MessageType}} == "StockExportFailed"
Dated filenames double as an audit trail. stock_2026-06-04.csv means a dispute about what was sent Tuesday is settled by looking at Tuesday's file, and a replayed run overwrites its own date, never a different night's.

Build it, step by step. The steps run in dependency order: every object is created before the object that selects it.

Before you start. Every artifact below, ports, message types, maps, lives inside one Application. Under Applications, create it first, Name and Namespace AcmeOrders, both code-safe, no dots or special characters, and select it so the steps build into it.
About SqlCallerResult. The SQL Caller hands the inbound map a result under the standard type name SqlCallerResult. Its format is whatever the procedure’s final SELECT emits, not a fixed envelope: a plain SELECT or FOR JSON returns JSON, FOR XML returns XML. The extract procedure (Step 3) ends in FOR JSON, so the result is a JSON array and the map’s source is JSON (Step 4). You do not create this type by hand; the SQL Caller provides it. If you prefer it listed, add a message type SqlCallerResult with format JSON.
1
Step One
Create the message types
The two types

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:

NameFormatPurpose
StockSnapshotXMLthe canonical stock document the extract port publishes (Step 7)
StockExportFailedXMLthe 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 on StockSnapshot
PromotionPath
SnapshotDate/Stock/SnapshotDate

2
Step Two
Create the Authentications

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.

ERP database, for the SQL Caller
SettingValue
NameErpDb
AdapterSQL Caller
DefinitionSQL Server Connection
Connection String (Database Config)the ERP database’s connection string, credentials included
3PL server, for the SFTP Caller
SettingValue
NameWarehouse3plSftp
AdapterSFTP Caller
DefinitionSFTP Password Authentication
Username / Passwordthe 3PL’s credentials
Host Key Fingerprintthe 3PL server’s host-key fingerprint

3
Step Three
Create the database objects

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.

The table and the extract procedure
SQL
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;

4
Step Four
Build the inbound map
Create the map

Under the application’s Maps, create the map the extract port’s inbound flow (Step 7) will select:

Map settingValue
NameResultToStockSnapshot
Source message typeSqlCallerResult (JSON), the SQL Caller’s result; this query ends in FOR JSON (Step 3), so the result is a JSON array
Target message typeStockSnapshot, from Step 1
The result

Because the procedure’s SELECT ends in FOR JSON PATH, the rows come back as a JSON array, one object per SKU:

JSON
[
  { "SnapshotDate": "2026-06-04", "Sku": "TP-4501", "OnHand": 118, "Reserved": 12 },
  { "SnapshotDate": "2026-06-04", "Sku": "HX-0090", "OnHand": 64,  "Reserved": 0 }
]
The canonical StockSnapshot

The map collapses that array into one canonical document, lifting the snapshot date off the first element:

XML
<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>
The SQL Caller returns what the SELECT asks for. This query ends in FOR JSON, so the result is JSON, and the map’s source is JSON to match. A plain SELECT would also return JSON; FOR XML would return XML. If the map’s source format disagrees with what the SELECT emits, the map matches nothing and the message republishes empty.
Author the XSLT

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:

XSLT 3.0
<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>

5
Step Five
Build the outbound map
Create the map

The second map runs on the upload port’s outbound side (Step 8) and produces the file the 3PL ingests:

Map settingValue
NameStockSnapshotTo3plCsv
Source message typeStockSnapshot, from Step 1
Targetthe 3PL’s flat file, written to the wire as-is
The 3PL file

Semicolon-delimited, date compacted to yyyymmdd, quantity available computed per line:

CSV
SKU;QTY_AVAILABLE;SNAPSHOT_DATE
TP-4501;106;20260604
HX-0090;64;20260604
Author the XSLT

One template over the canonical document:

XSLT 3.0
<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&#10;</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>&#10;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

6
Step Six
Create the clock

Create a receive port NightlyStockClock on the Scheduler: Repeat Every 1 / Repeat Unit Days, Daily Window opening 11:00:00 PM.


7
Step Seven
Create the extract port
General

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.)

SettingValue
AdapterSQL Caller
WayTwo
AuthenticationErpDb, from Step 2
Command TypeStoredProcedure
Command Textdbo.GetStockSnapshot, from Step 3
Select the map

In the port’s inbound flow, select ResultToStockSnapshot, from Step 4, so the extract is published as StockSnapshot.


8
Step Eight
Create the upload port
General

Create a send port StockToWarehouse on the SFTP Caller, subscribing on {{Message.MessageType}} == "StockSnapshot". The Filename binds the SnapshotDate promotion from Step 1:

SettingValue
AdapterSFTP Caller
WayOne
AuthenticationWarehouse3plSftp, from Step 2
Hostsftp.3pl.example
Remote Directory/inbound/stock
Filenamestock_{{Promoted.StockSnapshot.SnapshotDate}}.csv
Overwrite PolicyOverwrite
Select the map

On the outbound side, select the StockSnapshotTo3plCsv map from Step 5 (Typed, keyed on StockSnapshot).


9
Step Nine
Wire the failure path
Set the Exception Message Type

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 the OpsAlert send port

Create an O365 Mail port OpsAlert subscribing on {{Message.MessageType}} == "StockExportFailed".


10
Step Ten
Start the ports and test
Start in dependency order

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.

Turn on tracking

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.

Trigger the tick

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.

Break it on purpose

Block the upload and confirm the alert plus replay.

Turn bodies off in production. Enabled + Body is the most expensive tracking level, extra processing and database space, and it records every payload including successful runs. Once the flow is proven, set the ports to Only on Error instead: failures still capture the full body for diagnosis, while healthy runs are not recorded.