The most common question about adopting AI automation isn't about quality. It's "what does that cost per month?" — and most teams cannot answer until the first invoice lands.
This is a bookkeeping problem, not a technical one. The token counts are already inside the AI response. Every OpenAI response ships a usage field alongside the content. What's missing is the habit of storing it next to a workflow name, multiplying by a rate, and comparing against a budget. This case automates those three things.
Why you only find out afterwards
- Nobody stores the `usage` that arrives with every response. Most workflows pull
choices[0].message.contentand discard the rest. The cost data passes in front of you on every call and is thrown away. - Rates differ per model and keep changing. Input and output tokens are usually priced differently, often several times apart. Hard-code those numbers and you will quietly be reporting wrong figures a few months later.
- Provider dashboards don't break spend down by workflow. They show a monthly total but not which automation is responsible — so when cost spikes, you still can't decide what to cut.
How it works
- Every workflow that calls AI gets one Postgres node immediately after its AI node, pulling input and output tokens out of the response's
usageand writing a single row toai_usage. - Rates live in
ai_model_prices, one row per model: USD per million input tokens, USD per million output tokens, and the date you checked. - A
Schedule Triggerwakes the guard every day at 09:00. - A
Postgresnode aggregates this month's usage per workflow and joins the rate table to compute real cost. Models missing from the rate table are not counted as zero — they're surfaced separately as unpriced. - A
Codenode projects the month-end total from spend so far (spend ÷ days elapsed × days in month), computes the ratio against budget, and grades it healthy / approaching (80%) / over (100%). - If everything is healthy and nothing is unpriced, it returns an empty array and ends quietly. A daily "all good" message trains everyone to ignore the channel. Slack only hears about approaching, over, or unpriced calls.
What you need
- n8n — self-hosted or Cloud. Four core nodes only:
Schedule Trigger,Postgres,Code,Slack. - One Postgres database — two small tables. One row per AI call, so even thousands a day stay trivial.
- A Postgres credential — n8n → Credentials →
Postgres. If n8n runs in Docker and the database is on the host, the host must behost.docker.internal, notlocalhost. - A Slack Bot User OAuth Token — scopes
chat:writeandchannels:read, registered under n8n → Credentials →Slack API. - At least one workflow already calling AI — this case doesn't build a new AI workflow. It attaches a meter to one you already run.
- Current published rates for the models you use — read them off your provider's pricing page. You need two numbers per model: per-million input and per-million output. This document deliberately does not quote rates for any specific model, because printing them here would make them wrong.
Step 1 — create the two tables
-- One row per AI call. This is the ledger.
CREATE TABLE IF NOT EXISTS ai_usage (
id bigserial PRIMARY KEY,
workflow_name text NOT NULL, -- which automation spent it
model text NOT NULL, -- joins to the rate table
called_at timestamptz NOT NULL DEFAULT now(),
input_tokens integer NOT NULL DEFAULT 0,
output_tokens integer NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS ai_usage_time_idx ON ai_usage (called_at DESC);
-- Rates live here, not in code.
CREATE TABLE IF NOT EXISTS ai_model_prices (
model text PRIMARY KEY,
input_per_1m numeric NOT NULL, -- USD per 1M input tokens
output_per_1m numeric NOT NULL, -- USD per 1M output tokens
updated_at date NOT NULL -- the day you checked
);
-- Look up your models on your provider's pricing page and insert them.
-- The line below is a shape example, not real pricing.
-- INSERT INTO ai_model_prices VALUES ('your-model-name', 0.00, 0.00, CURRENT_DATE);updated_at. Six months from now, when a report looks wrong, the freshness of the rate table is the first thing to suspect — and without a date you can't make that judgement.Step 2 — attach the meter after the AI node
Open a workflow that calls AI and add a Postgres node directly after the AI node. Leave the operation on Execute Query and paste the following, replacing cs-triage with that workflow's name.
INSERT INTO ai_usage
(workflow_name, model, input_tokens, output_tokens)
VALUES (
'cs-triage',
'{{ $json.model }}',
{{ $json.usage.prompt_tokens }},
{{ $json.usage.completion_tokens }}
);Where those values come from — a response from the OpenAI Chat Completions API (https://api.openai.com/v1/chat/completions) carries model and usage right alongside the content at choices[0].message.content. usage.prompt_tokens is input, usage.completion_tokens is output. No extra API call is involved: you are simply picking up data you were already throwing away. On another provider, change only the field names for its usage object.
Step 3 — the month-to-date query
This sits in the guard's Month-to-Date Cost by Workflow node and already ships inside the downloadable JSON. It returns one row per workflow, most expensive first.
WITH priced AS (
SELECT u.workflow_name,
u.input_tokens,
u.output_tokens,
-- Unknown model => NULL, never 0. Zero would hide the spend.
CASE WHEN p.model IS NULL THEN NULL
ELSE (u.input_tokens::numeric / 1000000) * p.input_per_1m
+ (u.output_tokens::numeric / 1000000) * p.output_per_1m
END AS cost_usd
FROM ai_usage u
LEFT JOIN ai_model_prices p ON p.model = u.model
WHERE u.called_at >= date_trunc('month', now()) -- since the 1st
)
SELECT workflow_name,
count(*) AS calls,
count(*) FILTER (WHERE cost_usd IS NULL) AS unpriced_calls,
COALESCE(sum(input_tokens), 0) AS input_tokens,
COALESCE(sum(output_tokens), 0) AS output_tokens,
round(COALESCE(sum(cost_usd), 0), 4) AS cost_usd
FROM priced
GROUP BY workflow_name
ORDER BY cost_usd DESC;LEFT JOIN plus CASE WHEN p.model IS NULL matters more than it looks. A plain JOIN would drop calls whose model is missing from the rate table — those calls would vanish from the report entirely and spend would be understated. What you don't know should read as unknown, not as zero.Step 4 — actually stopping at the limit
Everything so far reports. To block, put a gate *before* the AI node. Add it only to the expensive workflows.
- Before the AI node, add a
Postgresnode with operationExecute Queryand the query below — it returns a single month-to-date figure. - After it, add an
IFnode with the condition{{ $json.mtd_cost_usd }}Number / Smaller than your budget (e.g.100). - Wire the
IFnode's true output to the original AI node. Under budget, everything behaves as before. - Wire the false output to a
Stop and Errornode with a message likeMonthly AI budget exceeded — halting. Because the run is then recorded as an error, yourError Trigger— or the watchdog from our other case — picks it up immediately.
SELECT round(COALESCE(sum(
(u.input_tokens::numeric / 1000000) * p.input_per_1m +
(u.output_tokens::numeric / 1000000) * p.output_per_1m
), 0), 4) AS mtd_cost_usd
FROM ai_usage u
JOIN ai_model_prices p ON p.model = u.model
WHERE u.called_at >= date_trunc('month', now());unpriced_calls, and that last month's rows never leak into this month's totals. The Code node was then executed in Node.js against that query's real output, exercising all three bands — healthy, approaching (86%), over (101%) — plus the empty-array path when there's nothing worth saying. Node types and versions match our already-published workflows (scheduleTrigger 1.2 / postgres 2.5 / code 2 / slack 2.3). Posting to a live Slack workspace, and reconciling against a real OpenAI invoice, were not verified at the time of writing. Last verified: 2026-08-15.Setup (30 minutes)
- Create the tables — run the Step 1 SQL. You should see
CREATE TABLEtwice andCREATE INDEXonce. - Enter your rates — look up the current published rates for your models and INSERT them into
ai_model_prices. Input and output are priced differently, so check both. Put today's date inupdated_at. - Register the Postgres credential — n8n → Credentials → Add credential →
Postgres→ connection details → Test for a green check. - Create the Slack app — api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → add
chat:writeandchannels:readto Bot Token Scopes → Install to Workspace → copy thexoxb-token. - Register the Slack credential — n8n → Credentials →
Slack API→ paste → Test. - Invite the bot —
/invite @your-bot-namein your alert channel, or you'll hitnot_in_channel. - Attach the meters — open each AI-calling workflow and add the Step 2 Postgres node after its AI node, with the right workflow name. Nothing accumulates until this is done.
- Confirm rows are landing — run one of those workflows, then
SELECT * FROM ai_usage ORDER BY called_at DESC LIMIT 5;. Token counts of 0 mean the response path differs — open the AI node's output JSON and locate itsusageobject. - Import the guard — download the JSON above, then n8n → Workflows →
...→ Import from File. - Set the budget — open
Budget Check + Formatand changeMONTHLY_BUDGET_USDat the top to your real monthly budget. - Set the channel and credentials — replace
REPLACE_WITH_YOUR_CHANNEL_NAME_OR_IDinPost to #ai-cost, and attach the credentials to both nodes. - Test — hit Execute Workflow. With low usage and a healthy verdict, nothing happening is correct. To see the message, temporarily set
MONTHLY_BUDGET_USDto something tiny like0.01, run it, then set it back. - Activate — flip the Activate toggle. It now checks daily at 09:00.
Tuning the thresholds
- `MONTHLY_BUDGET_USD` (default 100) — your monthly budget, and realistically the only value you'll ever change.
- `WARN_RATIO` (default 0.8) — what share of budget the month-end projection must cross before warning. Drop it to 0.7 if you need more reaction time.
- `ALWAYS_REPORT` (default false) — set
trueto receive the digest daily even when healthy. Worth turning on for the first month to build intuition, then off. - `USD_TO_KRW` (default 1380) — used only for the parenthetical KRW figure; it never affects the verdict. Trust your provider's invoice for exact billing.
What happens in edge cases
- A model missing from the rate table — it is never hidden as zero cost. Slack reports "N unpriced calls," and this alone will send a message even when the verdict is healthy. Switching models and forgetting to update rates is a genuinely common mistake.
- Early in the month, with a thin sample — straight-line projection swings wildly on day 1 or 2. Treat the first few days' overrun warnings as informational; judge from day 5 onward.
- Duplicate charges from retries — when n8n retries a failed AI call, tokens are consumed twice and two rows are recorded. That isn't a bug: you really are billed twice. Frequent retries are themselves a cost problem.
- Streaming responses — with streaming,
usagemay arrive only in the final chunk or not at all. This case assumes non-streaming Chat Completions calls. Streaming calls need separate handling. - Embeddings, images, and other non-token pricing — calls not priced per token won't compute correctly in this schema. Record them under a distinct model name, leave them out of the rate table, and let them stand out as unpriced.
- Exchange-rate drift — the KRW figure uses a fixed rate and will diverge from actual billing. Budget verdicts are computed in USD only, so decisions are unaffected.
I only recently learned what RAG is, and I want to build one with n8n. But I have no idea which AI I'm supposed to pay for.— Korean developer community