Skip to main content

Outbound Periodic

A periodic outbound message type produces a recurring bulk export — a daily on-hand file, a customer list, an EDI invoice batch. The export class extends DEVIntegExportBulkBase (a RunBaseBatch subclass), so it runs as a normal D365FO batch job and delivers through a connector such as SFTP or Azure File Share.

How it works

  1. The export runs on demand or on a batch schedule. Each run first writes a record to the Export log with status In progress.
  2. Your exportData() method builds the payload — usually a CSV file, but it can also be a JSON message for a queue — using helper methods from the base class.
  3. The file is delivered to the target folder (or the message to the queue). A Test run sends the file to your browser instead, so you can check the output without touching the server.
  4. The log is updated with the line count, duration, and final statusDone, Warning, or Error — which you can alert on.

Unlike event-based exports, a periodic export is stateless by default: each run rebuilds the whole dataset from a query. To send only what changed, use an incremental export.

Setting up and running the export

Configure the message type

In Outbound message types, set the Source (connection type and target folder), the Processing class, and the File parameters — a file name with a %d date placeholder, its date format, and the CSV delimiter. The message type below is a no-code SQL export, so it also carries the SQL text:

Bulk outbound message type setup

Run it

Export runs the job. It can go to a batch schedule (recurrence) for daily or hourly runs, or run once interactively. Tick Test run to send the file to your browser instead of the server — handy for checking output, and it does not advance an incremental export's state:

Export run dialog with batch recurrence and Test run

Test connection on the form lists the target folder, so you can verify the channel before the first run.

Monitor

Every run is recorded in the Export log with its line count, duration, and status. Set a standard D365FO alert on Error or Warning rows:

Export log with line counts, duration, and status

Building the export

You can build a periodic export at three levels of effort. The first needs no code at all.

No code: a SQL statement or data entity

DEVIntegExportBulkSQL exports the result of a SQL statement set directly in the message type — no class to write:

SELECT ITEMID, sum(availPhysical) as Qty, InventLocationId FROM INVENTSUM
WHERE CLOSEDQTY = 0 AND DATAAREAID = 'USMF'
GROUP BY ITEMID, InventLocationId
HAVING sum(availPhysical) > 0

The same class works against a data entity view for entity-shaped exports without code — for example SELECT CUSTOMERACCOUNT, ORGANIZATIONNAME, PRIMARYCONTACTEMAIL FROM CUSTCUSTOMERV3ENTITY. The result lands in the target folder as a CSV:

No-code SQL export result on the SFTP server

This is great for prototyping; real exports usually need X++.

Simple X++: build the file in code

For custom logic, write a class that extends DEVIntegExportBulkBase and override exportData(). The base class provides the file helpers — initCSVStream, writeHeaderLine, writeDataLine, sendFileToStorage — so you write only the business logic. The public sample DEVIntegTutorialExportBulkInventOnhand exports on-hand quantities across all companies:

class DEVIntegTutorialExportBulkInventOnhand extends DEVIntegExportBulkBase
{
public void exportData()
{
container lineData;
InventSum inventSum;

this.initCSVStream();

lineData = ['Company', 'ItemId', 'InventLocationId', 'LastUpdDatePhysical', 'AvailPhysical'];
this.writeHeaderLine(lineData);

while select crosscompany sum(AvailPhysical), maxof(LastUpdDatePhysical) from inventSum
group by ItemId, InventLocationId
where inventSum.AvailPhysical
{
lineData = [inventSum.DataAreaId, inventSum.ItemId, inventSum.InventLocationId,
inventSum.LastUpdDatePhysical, inventSum.AvailPhysical];
this.writeDataLine(lineData);
}

this.sendFileToStorage();
}

public str getExportDescription()
{
return "Tutorial export Onhand to CSV";
}
}

sendFileToStorage picks the transport from the connection type — SFTP or Azure File Share — so the same class works over either. (sendFileToStorageNotEmpty skips sending an empty file.)

Query + parameters

For exports the user should be able to filter, define a default query in exportQueryInit() and default file settings in initDefaultParameters(); exportData() then runs whatever query the user saved, falling back to your default, via exportQueryGet(). The sample DEVIntegTutorialExportBulkOnhandPricesQuery does this:

public Query exportQueryInit() // default query, editable by the user
{
Query query = new Query();
QueryBuildDataSource qBDS = query.addDataSource(tableNum(InventTable));

qBDS = qBDS.addDataSource(tableNum(InventItemGroupItem));
qBDS.relations(true);
qBDS.addRange(fieldNum(InventItemGroupItem, ItemGroupId)).value('Audio');
return query;
}

public DEVIntegMessageTypeTableOutbound initDefaultParameters(DEVIntegMessageTypeTableOutbound _messageType)
{
DEVIntegMessageTypeTableOutbound res = super(_messageType);
res.FileNameParameterD = 'yyyyMMdd_HHmm';
res.FileName = 'OnhandPrices_%d.csv';
res.FileColumnDelimiter = '|';
return res;
}

It also shows the Warning status — for when the data is questionable but the export should still run. If a row can't be fully resolved (say, no price is found), flag it without failing the run:

warning(strFmt("Price is not found for %1", inventTable.ItemId));
this.setExportStatus(DEVIntegExportBulkStatus::Warning);

The Skipped counter (incSkipCount()) works the same way for lines you deliberately leave out. Both appear in the Export log.

Incremental exports

An incremental export sends only what changed since last time. The simplest, most supportable approach — used by the sample DEVIntegTutorialExportBulkCustInvEDIInc — is to add two fields to the source document, IsExported and ExportedDateTime, and update them after a successful send. Avoid tracking by CreatedDateTime: a long transaction can commit a record with an earlier timestamp after you have already moved the watermark past it.

In Incremental mode the query filters to not-yet-exported records; once the file is sent, mark them:

if (exportType == DEVIntegExportBulkIncrementalType::Incremental)
{
qBDS.addRange(fieldNum(CustInvoiceJour, IsExported)).value(queryValue(NoYes::No));
}
// ... build the file, collecting the exported RecIds in a temp table ...
this.sendFileToStorageNotEmpty();

if (! isTestRun) // a Test run must not advance the watermark
{
ttsbegin;
update_recordset custInvoiceJour
setting IsExported = NoYes::Yes, ExportedDateTime = exportStartDateTime
exists join tmpExportedMark where tmpExportedMark.RefRecId == custInvoiceJour.RecId;
ttscommit;
}

Because the state lives in visible fields on the document, re-exporting is easy (clear the flag) and users can see each document's export status directly.

Other targets: Azure Service Bus

A periodic export is not limited to files. Instead of the CSV helpers, exportData() can build a JSON message (for example with the DEVIntegJsonWriter helper) and hand it to sendMessageToServiceBus() — the same base class, delivering to a queue rather than a folder:

str json = jsonWriter.getJsonString();
this.sendMessageToServiceBus(json, this.getServiceBusDefaultLabel());

Tutorial