Skip to content
iQBus HomeDocumentationBlogContact
Updated September 7, 2026
Tutorials/Getting started/Daily sales report

Daily sales report

Acme’s managers want to start each day knowing how the business did yesterday. Before they are at their desks, they want one short email in their inbox: yesterday’s takings and how many orders came in. So every morning, very early, something has to look back over the previous day’s orders, add up the total and the count, write them into a tidy email, and send it to the management group. The risk to design against is the quiet one: an email that simply never turns up, which nobody notices until the numbers are missed in a meeting. A run that fails has to tell the operations team straight away, and leave the failure somewhere they can act on it.

So the goal is simple. Once a day, early, total up yesterday’s orders, write the figures into a readable email, and send it to the managers. One report a morning, and if something breaks, an alert to operations rather than silence.

A sales summary in every manager’s inbox Early morning before the day starts Total yesterday’s orders takings and order count Write the report a readable email Managers’ inbox one report each morning if it fails, alert operations

Here is how iQBus builds that. 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. The trigger is a clock. 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 timer) fires in the six o’clock hour and publishes an empty tick, typed SalesReportTick. 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 reporting procedure:

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

That procedure aggregates the previous day into a single row and ends in FOR JSON, so its result comes back to the port as the JSON SqlCallerResult. A map (a named rule that turns one message shape into another) reshapes it into the canonical DailySalesSummary document and publishes it to the bus. You do not write that map by hand: the AI Accelerator generates it from a plain-language description, and the working code is on this page as a fallback. A second send port, bound to the O365 Mail Sender, subscribes to the summary and sends the email, with the figures lifted out of the payload by promotions:

EXPRESSION
{{Message.MessageType}} == "DailySalesSummary"
Scheduler one tick, 6 AM window BUS tick SQL Caller (two-way) total, then map the row Sales DB DailySalesSummary O365 Mail Sender ReportMail Managers failed SalesReportFailed routed to a failures table Activity Notification fallback, emails operations

When it fails. A report that silently does not arrive is the failure mode to design against, because nobody notices a missing email until the numbers are needed. Two ports can fail here, the pick-up and the send, and both carry the same Exception Message Type, SalesReportFailed. 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 <Summary> document is a report that was never sent. A third send port subscribes to it and writes each failure into a failures table operations works from, so the failure is handled and the run does not suspend:

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

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 6 builds both and explains why you want both.

The clock decides when, the query decides what. The procedure always totals one fixed day, yesterday, rather than "everything since the last run". So a tick that fires twice, or a run you replay a day later to see what went out, produces the same figures rather than a different set. The worst a duplicate tick can do is send the same email twice, which is why the schedule below is built to fire exactly once.

Build it, step by step. The steps follow the integration itself, from the clock to the managers’ inbox, not the order that is quickest to click through. Every Authentication, message type 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 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 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 morning’s run and see exactly what was totalled, mapped and mailed. Once the flow is proven and moves to production, drop it to Only on Error if you wish. The report reads the dbo.Orders table the order intake flow fills, so that database is the one external system this flow needs.
Application
SettingValue
NameAcmeReports
NamespaceAcmeReports
TrackingEnabled + Body
DescriptionDaily sales summary emailed to management
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
NameMorningReportClock
ApplicationAcmeReports (preset)
Port TypeReceive
AdapterScheduler
Adapter Message TypeSalesReportTick, 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). There is no "run at 06:00" setting here, and it is worth understanding why before you fill the fields in. The Scheduler’s cadence is a plain interval, Repeat Every so many Repeat Units, and it is counted from the moment you enable the port. Set it to every 1 Days and it does fire once a day, but at whatever time of day you happened to press Start, which is no use for a morning report. What gives you a time of day is the Daily Window, and that does not schedule anything either: it only suppresses the ticks that fall outside Daily Start Window to Daily End Window.

Put the two together and the answer appears. Run a short interval around the clock, and open the window for exactly one interval’s width:

Daily End Window = Daily Start Window + Repeat Every, one second less. A five minute window cannot hold two ticks that are five minutes apart, and it cannot hold none, because the ticks never stop. So exactly one tick lands inside it each morning, whatever time of day you enabled the port and however often you restart it afterwards. Outside the window the port costs nothing worth counting: it wakes, sees it is not the six o’clock hour, and goes back to sleep.
Adapter configuration
SettingValue
Time Zonethe company’s; the two Windows are read in it, the interval itself is time-zone agnostic
Repeat Every5
Repeat UnitMinutes
Start Date Windowoff; the report runs from now on, with no start date to wait for
End Date Windowoff; and it does not stop on a date either
Daily Windowon; this is what confines the tick to the morning
Daily Start Window06:00:00 AM
Daily End Window06:04:59 AM, the start plus the five minute interval, one second less
Payloadempty; the tick is only a wake-up call. The message still carries the standard envelope, and that is all the pick-up port needs

Both ways of getting the width wrong have a cost worth naming. A window wider than the interval holds two ticks, so the managers get two reports in one morning. A window narrower than the interval can fall between two ticks and hold none, so a morning passes with no report at all, which is exactly the silence this flow exists to prevent. Match the two and neither can happen.

The same arithmetic gives you any delivery time you like. For a report that lands within a minute of six, use Repeat Every 1 Minutes with a window of 06:00:00 AM to 06:00:59 AM. For one that just has to arrive some time in the six o’clock hour, use 1 Hours with 06:00:00 AM to 06:59:59 AM. The five minute pairing below is a good default: precise enough for a morning report, and loose enough that you can watch it happen.

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
NameSalesReportTick
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 SalesReportTick, which completes the General table above. Save the port. MorningReportClock is complete, stopped, and waiting; it is the last port you start, in Step 7.


2
Step Two
Create the reporting procedure

The sales 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. The report reads the orders table the order intake tutorial fills; if you have not built that flow, this is the table it creates, and a few rows of your own are enough to report on:

The orders table this reads
SQL
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()
);
The reporting procedure

Now the procedure the pick-up port in Step 3 calls. It totals one fixed day, yesterday, so the figures never depend on when the run happened or how many times it ran. The SELECT ends in FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, which returns the single row as one JSON object rather than an array of one, and that object is what the map in Step 4 consumes.

SQL
CREATE OR ALTER PROCEDURE dbo.GetDailySalesSummary
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @day DATE = CAST(DATEADD(DAY, -1, SYSUTCDATETIME()) AS DATE);

    SELECT
        CONVERT(CHAR(10), @day, 23)                        AS ReportDate,
        CAST(ISNULL(SUM(OrderTotal), 0) AS DECIMAL(18,2))  AS Revenue,
        COUNT(*)                                            AS OrderCount
    FROM dbo.Orders
    WHERE CAST(ReceivedAt AS DATE) = @day
    FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
END;
A quiet day still sends a report. The aggregate wraps the sum in ISNULL and counts rows, so a day with no orders returns one row reading zero and zero rather than no rows at all. That matters more than it looks: an email saying zero tells the managers the pipeline is alive and yesterday was quiet, while no email at all is indistinguishable from a broken report. It also means the flow never has to reason about an empty result.
The SQL Caller returns what the SELECT asks for. This query ends in FOR JSON, so the result is JSON, and the map you build in Step 4 takes a JSON source to match. A plain SELECT would also have returned JSON; FOR XML would have returned XML, and then a JSON-sourced map would match nothing and republish an empty document. 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 Step 4 does its work.

Send port
SettingValue
NameSalesAggregate
ApplicationAcmeReports (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 TypeSalesReportFailed, created next, at this field
WayTwo
Subscription{{Message.MessageType}} == "SalesReportTick"
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 mail port in Step 5 that is the <Summary> document, and for this port it is the empty tick, which reads as nothing either way. Step 6 gives the type its route.

Message type, the failure
SettingValue
NameSalesReportFailed
FormatXML
Sample datanone; the body is whatever was in flight when the run failed
Promotionsnone; the route in Step 6 takes the whole body, nothing is read out of it
The tick going out

The port’s outbound settings come before the adapter in the editor, and on this port there is nothing for any of them to do: the tick is an empty message, and the procedure it triggers takes no parameters. Leaving them empty is the correct configuration, not an omission:

SettingValue
Open Variablesempty; there is nothing in an empty tick worth capturing for later steps
Mapempty; the tick has no body to reshape, and the procedure reads nothing from it
Pipelineempty; nothing has to happen to the message on its way to the database
Close Variablesempty; nothing was opened
Configure the adapter

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

Authentication, sales database
SettingValue
NameSalesDbSql
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 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.GetDailySalesSummary, and in Step 6 on dbo.RecordSalesReportFailure 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 ConfigSalesDbSql, just created
Command TypeStored Procedure
Command Textdbo.GetDailySalesSummary, from Step 2
Input Parametersnone; the procedure decides for itself which day to total

Leave the port open; the next step continues on its Response side.


4
Step Four
Shape the result into the summary the email reads

Still inside SalesAggregate, 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. Three settings live on this side, and only two of them are used:

Response side
SettingValue
Adapter Message TypeSqlCallerResult, selected below, or created at this field
Pipelineempty; the result is a single row, so there is nothing to split or reshape before the map. A query that returned many rows would need a splitter component here, as the shipment status push tutorial builds
MapResultToDailySummary, created below, at this field
The result the SQL Caller returns

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. Because the procedure ends in FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, the single aggregate row arrives as one JSON object:

JSON
{ "ReportDate": "2026-06-04", "Revenue": 48230.00, "OrderCount": 312 }
Message type, the result
SettingValue
NameSqlCallerResult
FormatJSON
Sample datathe result JSON above
Promotionsnone; nothing routes on the raw result, and the values the email needs are promoted on the summary instead
Create the map

Now the Map step on the same side. 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; the source exists now, the target does not, so it is created from the map dialog’s own Target field:

Map
SettingValue
NameResultToDailySummary
Source message typeSqlCallerResult, just created
Target message typeDailySalesSummary, created next, at the Target field
The canonical DailySalesSummary

This is the shape the email reads, and every value the mail port needs is lifted out of it by a promotion. A promotion is a named path into the payload (an XPath for an XML type, a JPath for a JSON one), so before naming any, look at the payload the map will produce:

XML
<Summary>
  <ReportDate>2026-06-04</ReportDate>
  <Revenue>48230.00</Revenue>
  <OrderCount>312</OrderCount>
  <Currency>EUR</Currency>
</Summary>

Paste that XML into the type’s Sample data first. Then, for each promotion, add a row, open its ellipsis menu and choose Generate Path: it shows the sample, you click the element you want, and the path is written for you. Click Revenue and you get /Summary/Revenue. You can also type the paths by hand, but the tool removes the guesswork on deeper shapes.

Message type, the canonical summary
SettingValue
NameDailySalesSummary
FormatXML
Sample datathe Summary XML above
Promotions on DailySalesSummary
PromotionPathRead by
ReportDate/Summary/ReportDatethe Subject and the Body of the mail port, Step 5
Revenue/Summary/Revenuethe Body of the mail port, Step 5
OrderCount/Summary/OrderCountthe Body of the mail port, Step 5
Currency/Summary/Currencythe Body of the mail port, Step 5, to label the revenue figure

Mail fields are plain strings: they bind {{…}} tokens such as {{Promoted.DailySalesSummary.Revenue}} but never evaluate a body path themselves, which is why every figure the email prints has to exist as a promotion here. Save the type; you are back in the map with both types selected.

Build it with the AI Accelerator

Now the map body, the one piece of iQBus code in this tutorial, and you do not have to write it 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 JSON and emit XML, but it sees nothing of your Application: not the two message types you just created, not the promotions, nothing but the text you give it. So the prompt carries everything the map needs: the result sample, the summary sample, and the paths the promotions read. A prompt like this is enough:

AI ACCELERATOR PROMPT
Build a map from a JSON message type named SqlCallerResult to an XML message type named DailySalesSummary.

The source is the single-object result of a SQL aggregate over one day of orders. Match its fields to the summary by meaning: the date the figures are for, the day's revenue, and how many orders there were. Format the revenue with exactly two decimal places, because it is printed straight into an email. Add one constant element, Currency, set to EUR.

The root element must be Summary and its children must be exactly ReportDate, Revenue, OrderCount and Currency, because promotions read /Summary/ReportDate, /Summary/Revenue, /Summary/OrderCount and /Summary/Currency. Output indented XML.

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

{ "ReportDate": "2026-06-04", "Revenue": 48230.00, "OrderCount": 312 }

The DailySalesSummary target must look like this:

<Summary>
  <ReportDate>2026-06-04</ReportDate>
  <Revenue>48230.00</Revenue>
  <OrderCount>312</OrderCount>
  <Currency>EUR</Currency>
</Summary>

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 result JSON as the input, run the map, and compare the output with the Summary XML above. What has to match exactly are the four element names, because the promotions read them as paths, and the two decimal places on the revenue, because the email prints whatever is there. 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 JSON, which arrives as text, so parse-json(string(.)) turns it into a map and the ?key operator reads each field; format-number pins the revenue to two decimals so a round figure still reads as money.

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="r" select="parse-json(string(.))"/>
    <Summary>
      <ReportDate><xsl:value-of select="$r?ReportDate"/></ReportDate>
      <Revenue><xsl:value-of select="format-number(number($r?Revenue), '0.00')"/></Revenue>
      <OrderCount><xsl:value-of select="$r?OrderCount"/></OrderCount>
      <Currency>EUR</Currency>
    </Summary>
  </xsl:template>
</xsl:stylesheet>
Generated output varies, so treat the code above as an example. The Accelerator does not return the exact same XSLT twice, and your draft will not 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 root is Summary, its children are ReportDate, Revenue, OrderCount and Currency, and the four promotion paths above resolve against it. With tracking on Enabled + Body, open any run in Step 7 to see the real body.

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

Map assignment
Source Message TypeMapTarget Message Type
SqlCallerResultResultToDailySummaryDailySalesSummary

Save the port. SalesAggregate is complete: every tick it picks up becomes one DailySalesSummary message on the bus.


5
Step Five
Create the report mail port

Under the application’s Ports, create a send port bound to the O365 Mail Sender. It subscribes to the summary the pick-up port publishes and sends one email. The Exception Message Type already exists from Step 3, so this time it is a selection:

Send port
SettingValue
NameReportMail
ApplicationAcmeReports (preset)
Port TypeSend
AdapterO365 Mail Sender
Adapter Message Typenone; one-way. In two-way mode this adapter publishes an empty acknowledgement back to the bus, and nothing here waits for one
Exception Message TypeSalesReportFailed, from Step 3
WayOne
Subscription{{Message.MessageType}} == "DailySalesSummary"
The summary going out

As on the pick-up port, the outbound settings before the adapter have nothing to do: the message is already the shape the email reads, and the adapter takes its values from promotions rather than from the body:

SettingValue
Open Variablesempty; every value the mail needs is already promoted on the type
Mapempty; the summary is the final shape, and the email is composed by the adapter, not by a map
Pipelineempty; nothing has to happen to the message before it is sent
Close Variablesempty; nothing was opened
Configure the adapter

Open the adapter configuration. At the Auth Config field, create the sending credential in place. This is the same shape as the platform’s own email settings: an app registration in your Entra tenant, granted Mail.Send on Microsoft Graph, whose client id, tenant id and secret you paste here. The adapter acquires and refreshes the token itself:

Authentication, report mailbox
SettingValue
NameO365Reports
AdapterO365 Mail Sender (preset)
DefinitionMicrosoft Graph
Client Idthe app registration’s Application (client) id
Tenant Idthe Directory (tenant) id
Client Secret Valuethe secret’s Value, not its id; Entra shows it once, so copy it when you create it
Two things about that app registration. First, the identity has to be allowed to send as the address you put in From, or Graph refuses the call and the run fails, which is the failure you will exercise on purpose in Step 7. Second, the secret expires: note the date and set a reminder, because an expired secret stops the morning report with no other symptom. The walkthrough under Email notification configuration shows the same registration being made screen by screen, and this Authentication is a separate one, for this port only.

Back in the adapter configuration, compose the email. Subject and Body are templated against the promotions from Step 4, and the figures come out of them rather than out of the payload:

Adapter configuration
SettingValue
Auth Config TypeStatic
Auth ConfigO365Reports, just created
Fromreports@acme.example
Tomanagement@acme.example
SubjectSales report, {{Promoted.DailySalesSummary.ReportDate}}
Bodythe HTML below
Send as Fileoff; the figures belong in the message where a phone will show them, not in an attachment somebody has to open
Attachment Nameempty; it only applies when Send as File is on
TEMPLATE
<h3>Daily sales report, {{Promoted.DailySalesSummary.ReportDate}}</h3>
<table>
  <tr><td>Revenue ({{Promoted.DailySalesSummary.Currency}})</td><td>{{Promoted.DailySalesSummary.Revenue}}</td></tr>
  <tr><td>Orders</td><td>{{Promoted.DailySalesSummary.OrderCount}}</td></tr>
</table>
The Body is HTML by nature. There is no content type to pick on this adapter: it composes the message as HTML, so a small table like the one above renders as a table. Put plain text in the same field and it simply reads as plain text, which is why nothing in the configuration has to be switched either way. A modest table of the figures reads better in a morning inbox than a payload dump.

Save the port. Nothing has been sent yet; the flow is not running until Step 7 starts the ports.


6
Step Six
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 SalesReportFailed with an empty body, or the send fails, so the summary is republished as SalesReportFailed with the <Summary> document 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 failures 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 sales database that is down takes the failures table down with it. Build the route for what you know can break; build the notification for everything else.
The failures table and procedure

The route needs somewhere to put the failure. The sales database is already in the picture, so the 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 the pick-up failed; anything else is a summary that was never mailed.

SQL
CREATE TABLE dbo.SalesReportFailures (
    FailureId  BIGINT        IDENTITY(1,1) NOT NULL PRIMARY KEY,
    Leg        VARCHAR(10)   NOT NULL,          -- PickUp or Mail
    Payload    NVARCHAR(MAX) NULL,              -- the summary that was in flight, empty for a tick
    FailedAt   DATETIME2     NOT NULL CONSTRAINT DF_SalesFail_At DEFAULT SYSUTCDATETIME(),
    ResolvedAt DATETIME2     NULL               -- operations sets this once the report has gone out
);
SQL
CREATE OR ALTER PROCEDURE dbo.RecordSalesReportFailure
    @Payload NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    INSERT INTO dbo.SalesReportFailures (Leg, Payload)
    VALUES (CASE WHEN NULLIF(LTRIM(RTRIM(@Payload)), '') IS NULL THEN 'PickUp' ELSE 'Mail' 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 SalesDbSql 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 SalesReportFailed would pick them up again, and this is exactly the case the Activity Notification below exists for.

Send port
SettingValue
NameFailedReportToDeadLetter
ApplicationAcmeReports (preset)
Port TypeSend
AdapterSQL Caller
Adapter Message Typenone; one-way, the insert has nothing to return
Exception Message Typeempty, on purpose
WayOne
Subscription{{Message.MessageType}} == "SalesReportFailed"
The failure going out
SettingValue
Open Variablesempty; the parameter below reads the body directly
Mapempty; the failure is stored exactly as it was in flight, which is what makes it worth keeping
Pipelineempty
Close Variablesempty
Adapter configuration
SettingValue
Auth ConfigSalesDbSql, from Step 3
Command TypeStored Procedure
Command Textdbo.RecordSalesReportFailure
Input Parameters
ParameterValue
Payload{{Message.Body}}

Save the port. From now on a failed send lands as a Mail row with the summary in Payload, which is everything operations needs to see what the managers did not get, 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
NameSalesReportErrors
ApplicationAcmeReports (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. It is a different credential from the O365Reports Authentication in Step 5: that one is the report’s own mailbox, this one is how the platform talks to a human.
Do not let the notification stand in for the route. An email to operations saying "a run failed" leaves the report stranded until someone reads it, finds the run and replays it. The failures row is the report, 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.

7
Step Seven
Start the ports and test
What has to be in place first

Unlike a flow that ends in a database, this one ends in a mailbox, so two credentials decide whether you see anything. The O365Reports Authentication from Step 5 has to be a real app registration, its From address has to be one that identity is allowed to send as, and To has to be an inbox you can watch: change it to your own address for the test. Separately, the platform Email notification configuration has to be done or the Activity Notification stays silent. Without the first, every run ends in the failure path; without the second, the last test below proves nothing.

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 FailedReportToDeadLetter, then ReportMail, then SalesAggregate, and last the clock, MorningReportClock. Starting each subscriber before its producer means no tick fires before the pick-up is listening, and no summary is published before the mail port 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.

Give it a day to report on

The trigger in this flow is the clock, not a request you can send, so what you prepare instead is the data the report will find. Open the sales database in SQL Server Management Studio (Azure Data Studio, or any SQL client, works the same way) and insert a few orders dated yesterday. The dates are written relative to now, so they always match the day the procedure asks for:

SQL
INSERT INTO dbo.Orders (OrderNumber, CustomerId, CustomerName, Region, OrderTotal, ReceivedAt) VALUES
    ('SO-20001', 'C-2201', 'Nordwind Retail', 'EMEA', 1249.50, DATEADD(DAY, -1, SYSUTCDATETIME())),
    ('SO-20002', 'C-3310', 'Halvorsen AS',    'EMEA',  860.00, DATEADD(DAY, -1, SYSUTCDATETIME())),
    ('SO-20003', 'C-2201', 'Nordwind Retail', 'EMEA', 2110.25, DATEADD(DAY, -1, SYSUTCDATETIME()));

Then run EXEC dbo.GetDailySalesSummary yourself once, so you know what the email should say before it arrives: with those three rows the result reads 4219.75 and 3.

Do not wait until tomorrow morning

The schedule you built fires once, in the six o’clock window, which is no way to test anything. Open MorningReportClock, turn Daily Window off and set Repeat Every to 1 Minutes, save, and Restart the port from its row in the ports list, which resets the timer. Now a tick comes every minute and you can watch a whole run end to end. When you are done, put all three settings back: Daily Window on, 06:00:00 AM to 06:04:59 AM, Repeat Every 5 Minutes.

A tick a minute is an email a minute. With the window off there is nothing left to hold the schedule back, so stop the clock as soon as you have seen what you came to see, and remember that the recipients are real people. This is the moment to have To pointed at your own address rather than the management list.
Walk the run in tracking

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 SalesReportTick is published
SalesAggregate picks it upthe subscription matched and dbo.GetDailySalesSummary was called
SQL Caller respondsthe body is the one-object JSON result, typed SqlCallerResult, with the figures you saw in the database
Map ResultToDailySummarythe body is now the <Summary> document, with Revenue carrying two decimals and Currency reading EUR
Publishone DailySalesSummary is on the bus and ReportMail picked it up
O365 Mail Senderthe send succeeded, and the promotions resolved: the subject carries the report date rather than the raw token

Then check the other end: the email is in the inbox, the subject reads Sales report, and yesterday’s date, and the body is a small table showing 4219.75 and 3. A token that arrives unresolved, printed literally as {{Promoted…}}, means the promotion name in the field does not match the one on the message type, or the map did not produce the element its path reads.

Break it on purpose, first the route

Make the send fail: open ReportMail and change From to an address the app identity is not allowed to send as, nobody@acme.example, then save and wait for the next tick. The pick-up succeeds, the summary is published, and ReportMail fails after its retries. Watch what happens next in tracking: the message is republished as SalesReportFailed, FailedReportToDeadLetter picks it up and calls the procedure, and the run finishes as handled, not suspended. In the database, SELECT * FROM dbo.SalesReportFailures shows one Mail row with the <Summary> document as its Payload, so operations can see exactly which morning’s figures nobody received. No notification email is sent, because nothing was left unhandled. Put From back.

Then the safety net

Now take away something the route itself depends on: stop the sales database, or revoke EXECUTE on dbo.GetDailySalesSummary from the login in the Authentication, and wait for the tick. The pick-up fails, the tick is republished as SalesReportFailed, 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 SalesReportErrors email from Step 6, 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 the report goes out, fail loud, never drop.

Before production. Put the clock back to Repeat Every 5 Minutes with the 06:00:00 AM to 06:04:59 AM window, point From and To at the real report mailbox and the management list, 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.