Case studies
CASE STUDY — AI

What will AI automation cost this month — per-workflow token tracking and a hard budget stop in n8n

Deployed at All teams / AI adoption4 days to build
n8nOpenAIPostgresSlack

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.

ai-cost-guard.workflowLIVE
CRDaily 09:00PGAggregate usage$Check vs budgetSLWarn · block

Why you only find out afterwards

  • Nobody stores the `usage` that arrives with every response. Most workflows pull choices[0].message.content and 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.
Hence two design rules for this case. Rates live in a table, not in code — with the date they were checked. And cost is split per workflow. A total only produces anxiety; a per-workflow breakdown produces a decision.

How it works

  1. Every workflow that calls AI gets one Postgres node immediately after its AI node, pulling input and output tokens out of the response's usage and writing a single row to ai_usage.
  2. 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.
  3. A Schedule Trigger wakes the guard every day at 09:00.
  4. A Postgres node 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.
  5. A Code node 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%).
  6. 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 be host.docker.internal, not localhost.
  • A Slack Bot User OAuth Token — scopes chat:write and channels: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);
Always fill 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;
The 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.

  1. Before the AI node, add a Postgres node with operation Execute Query and the query below — it returns a single month-to-date figure.
  2. After it, add an IF node with the condition {{ $json.mtd_cost_usd }} Number / Smaller than your budget (e.g. 100).
  3. Wire the IF node's true output to the original AI node. Under budget, everything behaves as before.
  4. Wire the false output to a Stop and Error node with a message like Monthly AI budget exceeded — halting. Because the run is then recorded as an error, your Error 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());
Apply blocking with care. Put this in front of a customer-facing workflow and a budget overrun becomes a service outage. Block batch and internal workflows; warn only on anything a person is waiting for.
⬇︎ Download the workflow (ai-cost-guard.json)
One Postgres credential, one Slack credential, a channel name, and your monthly budget figure — that's the whole configuration. The Sticky Note at the top includes the table-creation SQL.
Being precise about what was verified. The aggregation query was executed on PostgreSQL 17 against fixtures confirming that two models with different rates are priced correctly, that a model missing from the rate table is separated out as 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)

  1. Create the tables — run the Step 1 SQL. You should see CREATE TABLE twice and CREATE INDEX once.
  2. 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 in updated_at.
  3. Register the Postgres credential — n8n → Credentials → Add credential → Postgres → connection details → Test for a green check.
  4. Create the Slack app — api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → add chat:write and channels:read to Bot Token Scopes → Install to Workspace → copy the xoxb- token.
  5. Register the Slack credential — n8n → Credentials → Slack API → paste → Test.
  6. Invite the bot/invite @your-bot-name in your alert channel, or you'll hit not_in_channel.
  7. 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.
  8. 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 its usage object.
  9. Import the guard — download the JSON above, then n8n → Workflows → ...Import from File.
  10. Set the budget — open Budget Check + Format and change MONTHLY_BUDGET_USD at the top to your real monthly budget.
  11. Set the channel and credentials — replace REPLACE_WITH_YOUR_CHANNEL_NAME_OR_ID in Post to #ai-cost, and attach the credentials to both nodes.
  12. Test — hit Execute Workflow. With low usage and a healthy verdict, nothing happening is correct. To see the message, temporarily set MONTHLY_BUDGET_USD to something tiny like 0.01, run it, then set it back.
  13. 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 true to 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, usage may 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.
Per workflow
cost attribution
80%
early warning point
Daily 09:00
check cadence
30 min
setup time
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

Your operation belongs
in here, too.

Tell us the most repetitive task you have. We'll map an automation scenario for it.