Detect AWS cost spikes with SQL
This tutorial builds a flow that keeps its own history of AWS spend and queries it with SQL. A custom schedule runs it every morning, a Date/time transform computes yesterday's date, an AWS node fetches per-service spend from Cost Explorer, and three Datastore nodes record the day, detect spikes with a single SQL statement, and prune old rows. A Filter and a Notification node turn the result into a Slack alert.
Goal and objectives
-
Goal: Get a Slack alert naming any AWS service whose spend yesterday ran more than 50% above its trailing 30-day average, with no external database and no warehouse job.
-
Objectives: In this tutorial, you'll learn how to:
-
Build a rolling history table with a Datastore Insert node.
-
Write a
SELECTwith the Datastore Run SQL action and bind a value from an earlier step with a:nameparameter. -
Reference the query's output columns in a filter and a Slack message.
-
Keep the table bounded with a scheduled
DELETE.
-
Below is the complete flow:

Every AWS call in this flow is read-only (ce:GetCostAndUsage). The only writes happen inside your own Datastore table.
Before you begin
-
Make sure your DoiT account has the CloudFlow Editor or CloudFlow Manager permission. See CloudFlow permissions.
-
Create an AWS CloudFlow connection for the account whose spend you want to watch. The role needs the
ce:GetCostAndUsagepermission. -
Connect Slack and identify the channel that should receive the alerts.
Create the history table
The flow needs one table to accumulate daily spend.
-
On the CloudFlow landing page, select Tables, then select Create table.
-
In Table name, enter
daily_service_spend. -
Define three columns:
Column Data type Notes dayDate The usage date, one row per service per day. serviceText The AWS service name, for example AWS WAF.costText Cost Explorer returns amounts as strings. The SQL casts this to a number when it does the math. -
Select Save.
cost is a Text column on purpose. Cost Explorer returns Metrics.UnblendedCost.Amount as a string, and a Numeric column only accepts numeric references. Storing the raw string and casting it in SQL (cost::numeric) keeps the Insert node simple.
Create the flow
-
Sign in to the DoiT console, select Automation and operations from the top navigation mega menu, and then select CloudFlow.
-
Select Create flow, give it a name such as
Cost spike sentinel, and add a description.
Configure the schedule trigger
-
Add a Scheduled trigger.
-
Set the frequency to Daily, pick a start date, and choose a run time and time zone. Early morning works well, because Cost Explorer has finalized most of the previous day's usage by then.
Compute yesterday's date
Cost Explorer needs a date, and so does the SQL later on. One transform produces both.
-
Add a Date/time transform node after the trigger.
-
Set Input to the trigger's
startTime. -
Add a Subtract transformation of
1Day and name the outputyesterdayTs. -
Chain a Format transformation on the previous step with the pattern
YYYY-MM-DDand name the outputyesterday.
The flow now has a yesterday string that every downstream node can reference.
Fetch yesterday's spend per service
-
Add an AWS node, select the Cost Explorer service and the
GetCostAndUsageoperation, and choose your AWS connection and account. -
Configure the parameters:
-
TimePeriod → Start: reference
yesterdayfrom the Date/time transform. -
TimePeriod → End: reference
currentDatefrom the trigger. Cost Explorer treatsEndas exclusive, so this returns exactly one day. -
Granularity:
DAILY. -
Metrics 1:
UnblendedCost. -
GroupBy 1: Key
SERVICE, TypeDIMENSION.

-
-
Open the Test tab and select Run test, keeping Save as test data selected. The response contains
ResultsByTime[0].Groups, one entry per service, which the next node maps into the table.
Record the day in the table
-
Add a Datastore node, select the
daily_service_spendtable, and keep the Insert action. -
Map each column to the Cost Explorer output. The node inserts one row per group automatically.
-
Day:
ResultsByTime.TimePeriod.Start -
Service:
ResultsByTime.Groups.Keys -
Cost:
ResultsByTime.Groups.Metrics, then enterUnblendedCostas the map key and selectAmount

-
All three columns reference the same node. A node's parameters can reference one non-trigger step at a time, which is why the day comes from the Cost Explorer response rather than from the Date/time transform.
Detect spikes with SQL
This is the node that does the analysis. It compares yesterday against every earlier day in the table.
-
Add a second Datastore node, select the same table, and set the action to Run SQL.
-
Enter the statement:
WITH baseline AS (SELECT service, AVG(cost::numeric) AS avg_costFROM daily_service_spendWHERE day < :yesterday::dateAND day >= :yesterday::date - 30GROUP BY service)SELECT s.service,ROUND(s.cost::numeric, 2) AS yesterday_cost,ROUND(b.avg_cost, 2) AS avg_cost,ROUND(s.cost::numeric / NULLIF(b.avg_cost, 0), 2) AS spike_ratioFROM daily_service_spend sJOIN baseline b ON b.service = s.serviceWHERE s.day = :yesterday::dateAND s.cost::numeric > 1.5 * b.avg_costORDER BY spike_ratio DESCThe common table expression averages each service's cost over the 30 days before yesterday, the main query joins yesterday's row against that average, and the
WHEREclause keeps only services running more than 50% above it.NULLIFavoids a division by zero for services whose history is all zeros.Both date bounds matter. Without the lower bound the average would cover every row still in the table, so the comparison window would silently widen as history accumulated.
-
Under Bind parameters, select Add parameter, name it
yesterday, then select Add additional parameters, select theyesterdaycheckbox, and confirm. -
In the Yesterday field that appears, reference
yesterdayfrom the Date/time transform.
-
Select Run query to validate. When the statement is valid, Output schema lists the columns the query returns:
service,yesterday_cost,avg_cost, andspike_ratio. Those names are what the filter and the Slack message reference.
Parameters bound to step outputs resolve to empty values when you select Run query in the editor. They resolve fully when the flow runs.
Prune old history
Without this step the table grows forever. One statement keeps it at 90 days, which is deliberately longer than the 30-day comparison window so that there is history left to inspect when an alert looks wrong.
-
Add a third Datastore node on the same table with the Run SQL action.
-
Enter the statement:
DELETE FROM daily_service_spend WHERE day < :yesterday::date - 90 -
Add the
yesterdaybind parameter the same way as the previous node and reference the Date/time transform again.
Place this node before the filter so that pruning happens on every run, including days with no spike.
Filter to real spikes
-
Add a Filter node.
-
Set Field to the spike detection node's output.
-
Add the condition
spike_ratio>1.5.
The SQL already applies this threshold, so the filter is what tells the flow whether there is anything to report.
Send the alert
-
Add a Notification node and select Slack as the provider.
-
Choose the channel that should receive alerts.
-
Write the message, typing
@to reference the filter's fields. For example:AWS cost spike detected: yesterday's spend ran more than 50% above the trailing 30-day average for the services below.@service spike ratio: @spike_ratio (yesterday @yesterday_cost vs 30-day avg @avg_cost) -
Select Don't send notification if no results. Without it, the flow posts a message with empty values on days when nothing spiked.

Publish and verify
-
Select Publish.
-
Select Run to trigger the flow manually, then open the run details to confirm every step completed.

-
Expand the spike detection step to see the rows it returned. On the first run the table has only one day of data, so the baseline is empty and no service can exceed it. That is expected.
-
Open the table from the Tables tab to confirm yesterday's rows landed.

After a few days the baseline becomes meaningful and the flow starts alerting on real outliers. Once a spike clears the threshold, the Slack message names the service, the ratio, and both figures behind it:
AWS cost spike detected: yesterday's spend ran more than 50% above the trailing
30-day average for the services below.
Amazon Elastic Compute Cloud - Compute, AWS WAF spike ratio: 1.87, 1.8
(yesterday 0.37, 0.32 vs 30-day avg 0.2, 0.18)
Adapt the pattern
The shape of this flow generalizes to anything you want to watch over time: record a measurement on a schedule, aggregate it with SQL, alert on the outliers, prune the history. A few variations worth trying:
-
Group Cost Explorer by
LINKED_ACCOUNTinstead ofSERVICEto watch accounts rather than services. -
Add a
HAVING COUNT(*) >= 7clause to the baseline so a service needs a week of history before it can trigger an alert. -
Change the multiplier from
1.5to something stricter, or compare againstMAX(cost)instead of the average. -
Keep a second table of per-service thresholds and join to it, so noisy services get their own multiplier.