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.
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:
{{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:
{{Message.MessageType}} == "DailySalesSummary"
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:
{{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.
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.
| Setting | Value |
|---|---|
| Name | AcmeReports |
| Namespace | AcmeReports |
| Tracking | Enabled + Body |
| Description | Daily sales summary emailed to management |
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.
| Setting | Value |
|---|---|
| Name | MorningReportClock |
| Application | AcmeReports (preset) |
| Port Type | Receive |
| Adapter | Scheduler |
| Adapter Message Type | SalesReportTick, created below, at this field |
| Exception Message Type | empty; a Scheduler tick has nothing to fail on the way in, the failures this flow handles are on the send ports |
| Way | One (the Scheduler offers nothing else; there is no caller waiting for a reply) |
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:
| Setting | Value |
|---|---|
| Time Zone | the company’s; the two Windows are read in it, the interval itself is time-zone agnostic |
| Repeat Every | 5 |
| Repeat Unit | Minutes |
| Start Date Window | off; the report runs from now on, with no start date to wait for |
| End Date Window | off; and it does not stop on a date either |
| Daily Window | on; this is what confines the tick to the morning |
| Daily Start Window | 06:00:00 AM |
| Daily End Window | 06:04:59 AM, the start plus the five minute interval, one second less |
| Payload | empty; 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.
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.
| Setting | Value |
|---|---|
| Name | SalesReportTick |
| Format | TXT |
| Sample data | empty, the tick carries no body |
| Promotions | none, 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.
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:
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() );
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.
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;
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.
| Setting | Value |
|---|---|
| Name | SalesAggregate |
| Application | AcmeReports (preset) |
| Port Type | Send |
| Adapter | SQL Caller |
| Adapter Message Type | SqlCallerResult, the type of the result coming back; a send port carries one only in two-way mode, and Step 4 sets it |
| Exception Message Type | SalesReportFailed, created next, at this field |
| Way | Two |
| Subscription | {{Message.MessageType}} == "SalesReportTick" |
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.
| Setting | Value |
|---|---|
| Name | SalesReportFailed |
| Format | XML |
| Sample data | none; the body is whatever was in flight when the run failed |
| Promotions | none; the route in Step 6 takes the whole body, nothing is read out of it |
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:
| Setting | Value |
|---|---|
| Open Variables | empty; there is nothing in an empty tick worth capturing for later steps |
| Map | empty; the tick has no body to reshape, and the procedure reads nothing from it |
| Pipeline | empty; nothing has to happen to the message on its way to the database |
| Close Variables | empty; nothing was opened |
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:
| Setting | Value |
|---|---|
| Name | SalesDbSql |
| Adapter | SQL Caller (preset) |
| Definition | SQL Server Connection |
| Connection String (Database Config) | one of the two below |
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:
| Method | Connection String | Use it when |
|---|---|---|
| SQL authentication | Server=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 identity | Server=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:
| Setting | Value |
|---|---|
| Auth Config | SalesDbSql, just created |
| Command Type | Stored Procedure |
| Command Text | dbo.GetDailySalesSummary, from Step 2 |
| Input Parameters | none; the procedure decides for itself which day to total |
Leave the port open; the next step continues on its Response side.
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:
| Setting | Value |
|---|---|
| Adapter Message Type | SqlCallerResult, selected below, or created at this field |
| Pipeline | empty; 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 |
| Map | ResultToDailySummary, created below, at this field |
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:
{ "ReportDate": "2026-06-04", "Revenue": 48230.00, "OrderCount": 312 }| Setting | Value |
|---|---|
| Name | SqlCallerResult |
| Format | JSON |
| Sample data | the result JSON above |
| Promotions | none; nothing routes on the raw result, and the values the email needs are promoted on the summary instead |
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:
| Setting | Value |
|---|---|
| Name | ResultToDailySummary |
| Source message type | SqlCallerResult, just created |
| Target message type | DailySalesSummary, created next, at the Target field |
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:
<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.
| Setting | Value |
|---|---|
| Name | DailySalesSummary |
| Format | XML |
| Sample data | the Summary XML above |
| Promotion | Path | Read by |
|---|---|---|
| ReportDate | /Summary/ReportDate | the Subject and the Body of the mail port, Step 5 |
| Revenue | /Summary/Revenue | the Body of the mail port, Step 5 |
| OrderCount | /Summary/OrderCount | the Body of the mail port, Step 5 |
| Currency | /Summary/Currency | the 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.
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:
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.
<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>
Save the map, and the row on the Response side’s Map step reads:
| Source Message Type | Map | Target Message Type |
|---|---|---|
| SqlCallerResult | ResultToDailySummary | DailySalesSummary |
Save the port. SalesAggregate is complete: every tick it picks up becomes one DailySalesSummary message on the bus.
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:
| Setting | Value |
|---|---|
| Name | ReportMail |
| Application | AcmeReports (preset) |
| Port Type | Send |
| Adapter | O365 Mail Sender |
| Adapter Message Type | none; one-way. In two-way mode this adapter publishes an empty acknowledgement back to the bus, and nothing here waits for one |
| Exception Message Type | SalesReportFailed, from Step 3 |
| Way | One |
| Subscription | {{Message.MessageType}} == "DailySalesSummary" |
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:
| Setting | Value |
|---|---|
| Open Variables | empty; every value the mail needs is already promoted on the type |
| Map | empty; the summary is the final shape, and the email is composed by the adapter, not by a map |
| Pipeline | empty; nothing has to happen to the message before it is sent |
| Close Variables | empty; nothing was opened |
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:
| Setting | Value |
|---|---|
| Name | O365Reports |
| Adapter | O365 Mail Sender (preset) |
| Definition | Microsoft Graph |
| Client Id | the app registration’s Application (client) id |
| Tenant Id | the Directory (tenant) id |
| Client Secret Value | the secret’s Value, not its id; Entra shows it once, so copy it when you create it |
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:
| Setting | Value |
|---|---|
| Auth Config Type | Static |
| Auth Config | O365Reports, just created |
| From | reports@acme.example |
| To | management@acme.example |
| Subject | Sales report, {{Promoted.DailySalesSummary.ReportDate}} |
| Body | the HTML below |
| Send as File | off; the figures belong in the message where a phone will show them, not in an attachment somebody has to open |
| Attachment Name | empty; it only applies when Send as File is on |
<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>
Save the port. Nothing has been sent yet; the flow is not running until Step 7 starts the ports.
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.
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.
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 );
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;
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.
| Setting | Value |
|---|---|
| Name | FailedReportToDeadLetter |
| Application | AcmeReports (preset) |
| Port Type | Send |
| Adapter | SQL Caller |
| Adapter Message Type | none; one-way, the insert has nothing to return |
| Exception Message Type | empty, on purpose |
| Way | One |
| Subscription | {{Message.MessageType}} == "SalesReportFailed" |
| Setting | Value |
|---|---|
| Open Variables | empty; the parameter below reads the body directly |
| Map | empty; the failure is stored exactly as it was in flight, which is what makes it worth keeping |
| Pipeline | empty |
| Close Variables | empty |
| Setting | Value |
|---|---|
| Auth Config | SalesDbSql, from Step 3 |
| Command Type | Stored Procedure |
| Command Text | dbo.RecordSalesReportFailure |
| Parameter | Value |
|---|---|
| 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.
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:
| Setting | Value |
|---|---|
| Name | SalesReportErrors |
| Application | AcmeReports (preset) |
| On Error Only | On |
| Subscription | All |
| Mode | On Event |
| Recipients | one row: Name Operations, Email ops@acme.example |
| Is Enabled | Enabled |
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.
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.
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:
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.
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.
Open Tracking, find the run the tick started, and step through it, opening each message body as you go:
| Step in Tracking | What to confirm |
|---|---|
| Scheduler fires | an empty message typed SalesReportTick is published |
| SalesAggregate picks it up | the subscription matched and dbo.GetDailySalesSummary was called |
| SQL Caller responds | the body is the one-object JSON result, typed SqlCallerResult, with the figures you saw in the database |
| Map ResultToDailySummary | the body is now the <Summary> document, with Revenue carrying two decimals and Currency reading EUR |
| Publish | one DailySalesSummary is on the bus and ReportMail picked it up |
| O365 Mail Sender | the 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.
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.
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.