Skip to content
Art2link ESB v2.02 LTS HomeDocumentationBlogContact
Tutorials/Healthcare & EDI/EDI 850 purchase orders

EDI 850 purchase orders

Your biggest retail customer places its orders the old-fashioned way. Instead of calling a web service, it writes purchase orders in a long-standing business format called EDI and drops them, several at a time, into a shared mailbox on a secure file server. The job is simple to state. Every fifteen minutes, collect whatever new orders have arrived, read them, and book the good ones into your order system the same hour. Now and then a customer will send an order that is garbled or incomplete. That one must be turned away politely with a standard “we could not accept this” reply, and one bad order must never hold up all the others in the same batch.

So three parties are involved: the retail customer who writes the orders, the shared mailbox where they land, and your own order system that needs to book them. Every fifteen minutes you sweep the mailbox, open each order, book the ones that look right, and send the customer a short rejection notice for any that do not. The picture below is the whole story in plain terms.

Collect the orders, book the good ones, turn away the bad Retail customer writes orders Shared mailbox orders wait here Collect and read each order Order system good order booked Garbled order polite rejection to customer

Here is how Art2link ESB builds that. Everything inside the product moves as small messages across a shared message backbone called the bus. A flow is wired from a few simple parts: a receive port brings data in from the outside world, a send port hands data out to a destination, and an adapter is the connector a port uses to talk to a specific kind of system (a file server, a database, a mailbox). The trigger here is the scheduled-download shape: a Scheduler (a clock that wakes the flow on a timer) ticks every fifteen minutes, and a two-way SFTP Caller port reaches into the mailbox and fetches whatever is new.

The interesting work happens on the way back in. EDI is written as lines of cryptic codes that nothing else on the bus understands, so it has to be translated the moment it arrives. That translation is done by a pipeline component, a small piece of custom code that runs on a port to reshape a message as it passes through. The component here is a disassembler: it reads the incoming EDI bundle (which can hold several orders), splits it into one order at a time, and rewrites each one as a clean, ordinary order record the rest of the flow can work with. As it does so it stamps each one with a message type of Edi850Order, a label that says “this is a purchase order,” so later parts of the flow can pick it out. This is the splitter pattern with a translation step folded in, because only this component knows where one order ends and the next begins. The same idea reused as a plain receive port is the pipeline component at its most basic.

Each order, now a clean Edi850Order on the bus, is checked against a schema (a rulebook for what a valid order must contain) before anything is booked. The orders that pass are ordinary order-intake material. The port that books them watches the bus and picks up only messages carrying that label, a standing instruction called a subscription:

EXPRESSIONsubscription filter
{{Message.MessageType}} == "Edi850Order"
interchangeX12 850 disassembler→ Edi850Order BUS validate(schema) valid schema fail Book, SQL Callervalid orders DB 997 mapnegative ACK partner

When it fails. EDI fails in layers, and each layer has its place. A file that is not an interchange at all, truncated, wrong delimiters, is rejected by the disassembler as a poison message and the run suspends at the edge with the original bytes intact in tracking; Activity Notifications (On Error Only) mails the team. A single bad 850 inside a good interchange fails alone after the split, exactly like a bad record in the settlement debatch. A PO that parses cleanly but violates the schema is marked errored at validation; the fetch port’s exception handling re-publishes it as Edi850Rejected, and a map, its control number drawn by a custom function, turns that into a negative 997 back to the partner (Step 5), a deterministic, terminal rejection, not a retry. And a booking failure downstream re-publishes under the shared exception type with the usual O365 Mail alert:

EXPRESSIONfailure subscription
{{Message.MessageType}} == "EdiOrderFailed"
Keep the raw interchange. EDI disputes are settled in segments, not JSON. Subscribe an archive send port to the fetched file before disassembly (the relay’s flight-recorder move) so “what did the customer actually send on the 4th?” is a lookup, and the negative 997 you return on rejection (Step 5) has the control numbers it must echo.
Why the interchange goes through a component, not a map. A map’s source format must be XML or JSON; flat text like X12 can never be a map source. So the raw interchange is disassembled to Edi850Order JSON by a pipeline component first, and the component classifies each output as Edi850Order so the bus carries JSON. The one map in this flow (Step 5) is allowed precisely because its source is JSON: it reads the Edi850Rejected JSON and writes the 997 as text on the output side, which is fine; only the source side is constrained.

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

Before you start. Create the Application AcmeEdi with its namespace under Applications and select it, so every artifact below lands in it.
1
Step One
Create the message types and the schema
The five types

Under the application’s Message types, create the five types this flow routes on. A message type is a name plus a format:

NameFormatPurpose
EdiInterchangeTXTthe raw interchange as fetched, the fallback type the disassembler component reads and the archive port (Step 9) subscribes to; never a map source
Edi850OrderJSONone parsed PO the component emits and classifies, the type the schema below validates
Edi850RejectedJSONa PO that failed validation, on its way to a 997; the JSON source of the Step 5 map
Edi997AckTXTthe negative acknowledgment the map renders as text on its output side (Step 5)
EdiOrderFailedJSONthe shared exception type the operational alerts subscribe to (Step 11)
Promotions on Edi850Order

Promotions live on the message type, so add them while you are here. The booking port’s parameters (Step 8) and the 997 filename (Step 10) bind these values, and adapter parameters are plain strings: they bind {{…}} tokens but never evaluate a body path.

PromotionPath
PoNumber$.orderNumber
CustomerId$.customer.id
OrderTotal$.total

PoNumber surfaces every parsed PO’s number as {{Promoted.Edi850Order.PoNumber}} for routing and tracing; the other two feed the booking procedure.

Promotion on Edi850Rejected

A rejected PO re-publishes payload intact, but promotions are type-qualified, so the rejection type needs its own; the rejected PO’s own interchange number keeps each acknowledgment distinct (Step 10). EdiOrderFailed needs none, its alert subject binds {{Message.MessageType}}:

PromotionPath
InterchangeControl$.interchange.control
Attach the schema to Edi850Order

Associate a schema with the Edi850Order message type. Because the disassembler (Step 4) emits JSON, this is a JSON Schema; for an XML payload it would be XSD. Validation fires automatically the moment a message is classified as Edi850Order, a fast structural rejection before the order is booked or routed. A payload that fails is marked errored: the outcome is deterministic and terminal, so there is no retry that would rescue it, the right response is to tell the partner, which is what the 997 does.

JSONEdi850Order schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Edi850Order",
  "type": "object",
  "required": ["orderNumber", "customer", "lines"],
  "properties": {
    "orderNumber": { "type": "string", "pattern": "^PO-" },
    "customer": {
      "type": "object",
      "required": ["id"],
      "properties": {
        "id":   { "type": "string" },
        "name": { "type": "string" }
      }
    },
    "lines": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "qty"],
        "properties": {
          "sku":       { "type": "string" },
          "qty":       { "type": "integer", "minimum": 1 },
          "unitPrice": { "type": "number" }
        }
      }
    }
  }
}

2
Step Two
Create the Authentications

Three external parties, three 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.

Customer mailbox, for the SFTP Caller
SettingValue
NameRetailcoSftp
AdapterSFTP Caller
DefinitionSFTP Password Authentication
Username / Passwordthe credentials for the customer’s SFTP server
Host Key Fingerprint (optional)the customer’s host-key fingerprint
Order database, for the SQL Caller
SettingValue
NameOrdersDbSql
AdapterSQL Caller
DefinitionSQL Server Connection
Connection String (Database Config)the order database’s connection string, credentials included
Alert mailbox, for O365 Mail
SettingValue
NameO365Ops
AdapterO365 Mail Sender
DefinitionMicrosoft Graph
Tenant Id / Client Id / Client Secret (Graph AuthConfig)an app registration with Mail.Send granted

3
Step Three
Create the database objects
The orders table and upsert procedure

Create the table and procedure the booking port (Step 8) calls, repeated here so this tutorial is self-contained; the MERGE is idempotent on the PO number:

SQLorders table + upsert
CREATE TABLE dbo.Orders (
    OrderNumber  VARCHAR(40)   NOT NULL PRIMARY KEY,
    CustomerId   VARCHAR(40)   NOT NULL,
    CustomerName NVARCHAR(200) NULL,
    Region       VARCHAR(20)   NULL,
    OrderTotal   DECIMAL(18,2) NOT NULL,
    Currency     CHAR(3)       NOT NULL CONSTRAINT DF_Orders_Cur DEFAULT 'EUR',
    ReceivedAt   DATETIME2     NOT NULL CONSTRAINT DF_Orders_Rcv DEFAULT SYSUTCDATETIME()
);

CREATE OR ALTER PROCEDURE dbo.UpsertOrder
    @OrderNumber VARCHAR(40),
    @CustomerId  VARCHAR(40),
    @OrderTotal  DECIMAL(18,2)
AS
BEGIN
    SET NOCOUNT ON;
    MERGE dbo.Orders AS tgt
    USING (SELECT @OrderNumber AS OrderNumber) AS src
        ON tgt.OrderNumber = src.OrderNumber
    WHEN MATCHED THEN
        UPDATE SET CustomerId = @CustomerId,
                   OrderTotal = @OrderTotal
    WHEN NOT MATCHED THEN
        INSERT (OrderNumber, CustomerId, OrderTotal)
        VALUES (@OrderNumber, @CustomerId, @OrderTotal);
END;
The control-number counter

Create the counter table and the transactional draw, the same discipline the 810 tutorial applies to its interchange numbers. There the number reaches the assembler component through a SQL Caller and a Variable; here a custom function (Step 5) draws it directly, two routes to the same atomic increment:

SQLcontrol-number counter
CREATE TABLE dbo.EdiControlNumber (
    Partner       VARCHAR(30) NOT NULL PRIMARY KEY,
    LastControlNo BIGINT      NOT NULL CONSTRAINT DF_Ctl DEFAULT 0
);
INSERT INTO dbo.EdiControlNumber (Partner, LastControlNo) VALUES ('RETAILCO', 0);

CREATE OR ALTER PROCEDURE dbo.NextControlNumber
    @Partner VARCHAR(30)
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @next BIGINT;
    UPDATE dbo.EdiControlNumber WITH (UPDLOCK, HOLDLOCK)
    SET @next = LastControlNo = LastControlNo + 1
    WHERE Partner = @Partner;
    SELECT @next AS ControlNumber;
END;

4
Step Four
Build the disassembler component
Create the component

In Pipeline components, create Edi850Disassembler, the AI assistant’s guided mode fits. It splits the interchange on segment terminators, walks each ST…SE set, and emits one Edi850Order per PO. Critically, each message carries the interchange, group and transaction control numbers, the 997 must echo them if this PO is later rejected.

The interchange it parses
EDIX12 850 interchange
ISA*00*          *00*          *ZZ*RETAILCO       *ZZ*YOURCO         *260605*0710*U*00401*000004821*0*P*>~
GS*PO*RETAILCO*YOURCO*20260605*0710*4821*X*004010~
ST*850*0001~
BEG*00*SA*PO-7714**20260605~
N1*ST*Brams Outdoor BV*92*C-2201~
PO1*1*40*EA*61.20**VN*TP-4501~
CTT*1~
SE*7*0001~
GE*1*4821~
IEA*1*000004821~
The component code
C#Edi850Disassembler.cs
using System.Text.Json;
using CC.Art2link.Pipelines.Domain.Models.PipelineComponents;

public sealed class EdiConfig
{
    public string SegmentTerminator { get; set; } = "~";
    public string ElementSeparator  { get; set; } = "*";
}

public sealed class Edi850Disassembler : PipelineComponentBase<EdiConfig>
{
    public override string Name => "Edi850Disassembler";

    protected override Task<PipelineComponentOutput> ExecuteAsync(
        PipelineComponentInput input,
        EdiConfig config,
        CancellationToken cancellationToken)
    {
        try
        {
            var segments = input.Body
                .Split(config.SegmentTerminator, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.Trim().Split(config.ElementSeparator))
                .ToList();

            var isa = segments.FirstOrDefault(s => s[0] == "ISA");
            var gs  = segments.FirstOrDefault(s => s[0] == "GS");
            var sender       = isa is not null ? isa[6].Trim()  : "";
            var receiver     = isa is not null ? isa[8].Trim()  : "";
            var control      = isa is not null ? isa[13].Trim() : "";
            var groupControl = gs  is not null ? gs[6].Trim()   : "";
            var functionalId = gs  is not null ? gs[1].Trim()   : "";   // GS01, e.g. "PO"

            var messages = new List<PipelineMessage>();
            List<string[]>? st = null;
            foreach (var seg in segments)
            {
                if (seg[0] == "ST") st = new List<string[]>();
                st?.Add(seg);
                if (seg[0] == "SE" && st is not null)
                {
                    messages.Add(BuildOrder(st, sender, receiver, control, groupControl, functionalId));
                    st = null;
                }
            }

            return Task.FromResult(new PipelineComponentOutput
            {
                Success = true, Messages = messages
            });
        }
        catch (Exception ex)
        {
            return Task.FromResult(new PipelineComponentOutput
            {
                Success      = false,
                ErrorMessage = $"Edi850Disassembler: {ex.Message}",
                Exception    = ex
            });
        }
    }

    private static PipelineMessage BuildOrder(
        List<string[]> st, string sender, string receiver,
        string control, string groupControl, string functionalId)
    {
        var stSeg = st.First(s => s[0] == "ST");
        var beg   = st.First(s => s[0] == "BEG");
        var n1    = st.FirstOrDefault(s => s[0] == "N1");
        var lines = st.Where(s => s[0] == "PO1")
            .Select(p => new { sku = p[7], qty = int.Parse(p[2]), unitPrice = decimal.Parse(p[4]) })
            .ToArray();
        var total = lines.Sum(l => l.qty * l.unitPrice);

        var order = new
        {
            orderNumber = beg[3],
            customer    = new { id = n1?[4], name = n1?[2] },
            total,
            lines,
            // everything a 997 must echo about this message, lifted from the envelope
            interchange = new
            {
                control,
                groupControl,
                stControl        = stSeg[2],   // ST02
                functionalId,                  // GS01, e.g. "PO"
                transactionSetId = stSeg[1],   // ST01, e.g. "850"
                sender,
                receiver
            }
        };

        return new PipelineMessage
        {
            Body        = JsonSerializer.Serialize(order),
            MessageType = "Edi850Order"
        };
    }
}
The canonical output

Each PO comes out as the canonical order shape, classified as Edi850Order:

JSONcanonical order
{
  "orderNumber": "PO-7714",
  "customer": { "id": "C-2201", "name": "Brams Outdoor BV" },
  "total": 2448.00,
  "lines": [
    { "sku": "TP-4501", "qty": 40, "unitPrice": 61.20 }
  ],
  "interchange": {
    "control": "000004821",
    "groupControl": "4821",
    "stControl": "0001",
    "functionalId": "PO",
    "transactionSetId": "850",
    "sender": "RETAILCO",
    "receiver": "YOURCO"
  }
}

5
Step Five
Build the 997 map

No pipeline component this time. The rejected PO already carries everything a 997 must echo, the group code (AK1), the acknowledged set (AK2), the sender and receiver (swapped, since the 997 replies to whoever sent the 850), and the acknowledgment itself is flat text, which is exactly what a map emits with method="text". The one piece of live data is the fresh interchange control number for the outgoing 997, and that is a job for a custom function: one deliberate, transactional draw from the Step 3 counter, the single-database-call shape that keep custom functions lean sanctions.

Create the Constant

The function needs to reach the database, and a connection string is deployment configuration, not code, define it as a Constant on the application:

NameDescriptionValue
EdiDbConnControl-number database connectionServer=prod-sql-01;Database=Orders;Integrated Security=true
Create the custom function

In Custom functions, create fnNextControlNumber. Functions are system-scoped, so from now on any map in any application can draw an EDI control number. It does exactly one thing, one stored-procedure call, one number back:

C#fnNextControlNumber
using CC.Art2link.Attributes;
using Microsoft.Data.SqlClient;

namespace Custom.Functions;

public static class EdiFunctions
{
    // One job: draw the next interchange control number for a partner.
    // Every call consumes a number, callers capture the result once.
    [CustomFunction("fnNextControlNumber")]
    public static string NextControlNumber(string partner, string connectionString)
    {
        using var conn = new SqlConnection(connectionString);
        conn.Open();

        using var cmd = new SqlCommand("dbo.NextControlNumber", conn);
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@Partner", partner);

        return Convert.ToInt64(cmd.ExecuteScalar()).ToString();
    }
}
Create the map

Under the application’s Maps, create the map; both message types come from Step 1:

Map settingValue
NameEdi850RejectedToEdi997Ack
Source message typeEdi850Rejected, from Step 1
Target message typeEdi997Ack, from Step 1
Author the XSLT

The rejected order’s JSON body arrives as a string the map parses with parse-json(); it draws the control number once into a variable via art:invoke and renders the whole 997 as text. The {{Constant.EdiDbConn}} argument is resolved by the binding preprocessor before the XSLT runs. The AK9/SE counts are for the single set being acknowledged, a 997 that acknowledges several sets at once would aggregate them across the functional group:

XSLT 3.0Edi850RejectedToEdi997Ack.xsl
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:art="urn:art2link:functions"
    exclude-result-prefixes="art">

  <xsl:output method="text"/>

  <!-- JSON arrives as a string; parse-json() turns it into an XPath 3.1 map -->
  <xsl:template match="/">
    <xsl:variable name="in" select="parse-json(.)"/>
    <!-- The 997 answers whoever sent the 850, so the IDs swap. -->
    <xsl:variable name="sender"   select="string($in?interchange?receiver)"/>
    <xsl:variable name="receiver" select="string($in?interchange?sender)"/>

    <!-- One deliberate call. Every invocation draws a number from the
         counter, so capture it once and reuse the variable. -->
    <xsl:variable name="ctl"
        select="art:invoke('fnNextControlNumber', $receiver, '{{Constant.EdiDbConn}}')"/>
    <xsl:variable name="ctl9" select="format-number(number($ctl), '000000000')"/>

    <xsl:variable name="now" select="current-dateTime()"/>
    <xsl:variable name="d6"  select="format-dateTime($now, '[Y,2-2][M01][D01]')"/>
    <xsl:variable name="d8"  select="format-dateTime($now, '[Y0001][M01][D01]')"/>
    <xsl:variable name="t4"  select="format-dateTime($now, '[H01][m01]')"/>

    <xsl:variable name="segments" select="(
      'ISA*00*          *00*          *ZZ*' ||
          substring($sender   || '               ', 1, 15) || '*ZZ*' ||
          substring($receiver || '               ', 1, 15) ||
          '*' || $d6 || '*' || $t4 || '*U*00401*' || $ctl9 || '*0*P*>',
      'GS*FA*' || $sender || '*' || $receiver || '*' || $d8 || '*' || $t4 ||
          '*' || $ctl || '*X*004010',
      'ST*997*0001',
      'AK1*' || $in?interchange?functionalId || '*' || $in?interchange?groupControl,
      'AK2*' || $in?interchange?transactionSetId || '*' || $in?interchange?stControl,
      'AK5*R',
      'AK9*R*1*1*0',
      'SE*6*0001',
      'GE*1*' || $ctl,
      'IEA*1*' || $ctl9
    )"/>
    <xsl:value-of select="string-join($segments, '~&#10;')"/>
    <xsl:text>~&#10;</xsl:text>
  </xsl:template>

</xsl:stylesheet>
Every call draws a number. fnNextControlNumber consumes a control number on each invocation. Gaps are normal in EDI, a replayed rejection simply draws a fresh number, but accidental extra calls are not: capture the result in one xsl:variable and reference the variable, exactly the discipline keep custom functions lean prescribes.
The acknowledgment it renders

The negative acknowledgment the partner receives:

EDI997 negative ACK
ISA*00*          *00*          *ZZ*YOURCO         *ZZ*RETAILCO       *260605*0712*U*00401*000000031*0*P*>~
GS*FA*YOURCO*RETAILCO*20260605*0712*31*X*004010~
ST*997*0001~
AK1*PO*4821~
AK2*850*0001~
AK5*R~
AK9*R*1*1*0~
SE*6*0001~
GE*1*31~
IEA*1*000000031~

6
Step Six
Create the Scheduler clock
General

Create the Scheduler receive port EdiMailboxClock: Repeat Every 15, Repeat Unit Minutes. Everything the ports from here on reference 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.)


7
Step Seven
Create the fetch port
General

Create a two-way SFTP Caller send port EdiMailboxFetch subscribing on {{Config.PortName}} == "EdiMailboxClock":

SettingValue
AdapterSFTP Caller
WayTwo
Hostsftp.retailco.example
AuthenticationRetailcoSftp, from Step 2
CommandDownload
Remote path/outbound/po, the mailbox directory the customer drops interchanges in; the download picks up whatever is new since the last tick
Classify and disassemble

In the port’s inbound flow, classification comes first: classify the fetched file as EdiInterchange at the start of the inbound flow, before the disassembler runs, so the raw interchange publishes under the type the archive port (Step 9) subscribes to. Then reference Edi850Disassembler, from Step 4, in the inbound pipeline.

Set the Exception Message Type

Set this port’s Exception Message Type to Edi850Rejected, from Step 1, when schema validation errors a message in the inbound flow, the port re-publishes it under that type, payload and control numbers intact, for the 997 path to pick up.


8
Step Eight
Create the booking port
General

Create a send port OrderBooking on the SQL Caller, subscribing on {{Message.MessageType}} == "Edi850Order". Only validated orders ever carry that type, so nothing un-checked reaches the database. The Step 1 PoNumber Promotion earns its keep here too: a narrower subscription such as {{Promoted.Edi850Order.PoNumber}} == "PO-7714" pulls a single PO out of the stream when one needs tracing or quarantining.

SettingValue
AdapterSQL Caller
WayOne
Auth ConfigOrdersDbSql, from Step 2
Command TypeStoredProcedure
Command Textdbo.UpsertOrder
Input Parameters

Adapter parameters bind promotions, not body paths; the three Promotions defined on Edi850Order in Step 1 supply the values to the procedure from Step 3, one key/value row each:

ParameterValue
OrderNumber{{Promoted.Edi850Order.PoNumber}}
CustomerId{{Promoted.Edi850Order.CustomerId}}
OrderTotal{{Promoted.Edi850Order.OrderTotal}}

9
Step Nine
Create the archive port
General

Add an archive SFTP send port EdiArchive on {{Message.MessageType}} == "EdiInterchange", the type the fetch port classifies in Step 7, writing to Remote Directory /archive/edi-in on your archive server to keep the raw file.


10
Step Ten
Create the 997 delivery port
General

Create an SFTP Caller send port Ack997Out subscribing on {{Message.MessageType}} == "Edi850Rejected", delivering to the partner’s acknowledgment directory:

SettingValue
AdapterSFTP Caller
Hostsftp.retailco.example
AuthenticationRetailcoSftp, from Step 2
Remote Directory/inbound/ack
Filename997_{{Promoted.Edi850Rejected.InterchangeControl}}.edi, binding the Promotion from Step 1
Select the map

In the port’s outbound flow, select the map from Step 5 (Typed, keyed on Edi850Rejected).


11
Step Eleven
Wire the failure path
Set the Exception Message Type

On the booking, archive and Ack997Out ports, set Exception Message Type to EdiOrderFailed, from Step 1.

Create the OpsAlert send port

Create an O365 Mail send port OpsAlert subscribing on {{Message.MessageType}} == "EdiOrderFailed":

SettingValue
AuthenticationO365Ops, from Step 2
Fromnoreply@acme.example
Toops@acme.example
SubjectOperations alert, {{Message.MessageType}}

Cover files the disassembler cannot parse at all (not even an interchange) with Activity Notifications (On Error Only), those suspend as poison messages before any PO exists to reject.


12
Step Twelve
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, then the receive port.

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 a run

To test without waiting for the next quarter-hour tick, temporarily set EdiMailboxClock to repeat every 1 Minutes, then restore the schedule.

Drop a mixed interchange

Drop an interchange holding two POs, one valid, one missing a required field (say a PO with no PO1 line, which the schema’s minItems rejects).

Follow both POs

In tracking the valid PO validates and books; the invalid one is errored at the schema, re-published as Edi850Rejected, and a negative 997 lands in the partner’s acknowledgment directory, while the raw interchange sits in the archive showing both as received.

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.