This is the no-code version of the Slack bot tutorial. If your team lives in Notion and not Slack, or if you're a PM/analyst who'd rather not maintain a Node.js codebase, this is the same outcome with zero JavaScript: a Notion database that auto-fills with structured earnings summaries the moment your watchlist companies report.
The workflow is built in n8n (self-hosted or cloud), takes ~25 minutes to set up, and runs forever on a schedule trigger.
Total monthly cost: ~$30 if self-hosting n8n on a $5 VPS, $25 for the API tier, plus pennies for Claude calls. n8n cloud is $20/month if you want zero maintenance.
What You'll Build
[Schedule Trigger: every 30 min, 8 AM - 8 PM ET, Mon-Fri]
↓
[Loop over watchlist tickers (Code node)]
↓
[HTTP: GET /transcripts/recent?ticker=X&limit=1]
↓
[Filter: is this earnings_id new? (compare against Notion DB)]
↓ (only when NEW)
[HTTP: GET /transcripts/:earnings_id]
↓
[Anthropic: summarize transcript into 4 bullets + 3 metrics]
↓
[Notion: create page in "Earnings Watch" database]
↓
[Slack (optional): notify channel]
Every node is a drag-and-drop block in n8n's editor. No code beyond two small JS snippets in Code nodes (provided below).
Prerequisites
- n8n instance — self-hosted via Docker or n8n cloud (free tier handles low volume)
- earningscalls.dev API key — Pro tier or higher
- Anthropic API key for Claude summarization
- Notion integration token + a database with the right columns (see below)
Step 1: Notion Database Schema
Create a new Notion database called "Earnings Watch" with these properties:
| Property | Type | Why |
|---|---|---|
| Ticker | Title | Primary identifier |
| Company | Text | Human-readable name |
| Call Date | Date | When the call happened |
| Earnings ID | Text | Used as dedupe key |
| Summary | Text | 4 AI bullets |
| Metrics | Text | 3 key extracted figures |
| Transcript Link | URL | Deep link to full transcript |
| Sector | Select | For filtering views |
Share the database with your n8n Notion integration (Notion → Connections → add your integration → grant access).
Step 2: n8n Workflow — Schedule Trigger
Add a Schedule Trigger node:
- Rule: Every 30 minutes
- Active hours: 12:00 - 24:00 UTC (covers US market + after-hours)
- Days: Mon-Fri
Step 3: Iterate Your Watchlist
Add a Code (JavaScript) node right after the trigger:
// Watchlist as array
const tickers = [
"NVDA", "MSFT", "AAPL", "META", "GOOG", "AMZN",
"TSLA", "CRM", "NFLX", "ORCL", "AMD", "AVGO"
];
return tickers.map(ticker => ({ json: { ticker } }));
This emits one execution per ticker downstream. n8n's "execute once per item" mode handles the fan-out automatically.
Step 4: Fetch Latest Call
Add an HTTP Request node:
- Method: GET
- URL:
https://earningscalls.dev/api/v1/transcripts/recent - Query Parameters:
ticker:={{ $json.ticker }}(n8n expression)limit:1
- Headers:
X-API-Key:{{ $env.EARNINGSCALLS_API_KEY }}
Response will be { results: [{...call object...}] }.
Step 5: Check If New
Add a Notion node to query your database for this earnings_id:
- Resource: Database Page
- Operation: Get Many
- Database: select your "Earnings Watch" DB
- Filter:
- Property: "Earnings ID"
- Condition: equals
- Value:
={{ $node["HTTP Request"].json.results.earnings_id }}
Then an IF node:
- Condition:
{{ $node["Notion Query"].json.length === 0 }}(no row exists yet)
Only the "TRUE" branch continues — that means we haven't seen this call before.
Step 6: Fetch Full Transcript
Another HTTP Request node on the TRUE branch:
- URL:
https://earningscalls.dev/api/v1/transcripts/{{ $node["HTTP Request"].json.results.earnings_id }} - Same
X-API-Keyheader
Step 7: Summarize With Claude
Add an HTTP Request node (we use direct API rather than the n8n Anthropic node for finer control over the prompt):
- Method: POST
- URL:
https://api.anthropic.com/v1/messages - Headers:
x-api-key:{{ $env.ANTHROPIC_API_KEY }}anthropic-version:2023-06-01Content-Type:application/json
- Body (JSON):
{
"model": "claude-haiku-4-5",
"max_tokens": 600,
"messages": [{
"role": "user",
"content": "You are an equity analyst. Read this earnings call transcript and produce a summary in this exact format:\n\nBULLETS:\n- [bullet 1: headline result vs consensus]\n- [bullet 2: guidance change]\n- [bullet 3: most important strategic update]\n- [bullet 4: most concerning risk in Q&A]\n\nMETRICS:\n- [metric 1: figure with value]\n- [metric 2: figure with value]\n- [metric 3: figure with value]\n\nBe terse. Use the ticker, not 'the company'.\n\nTRANSCRIPT:\n{{ $node['HTTP Request3'].json.transcript }}"
}]
}
(HTTP Request3 is the name of your transcript-fetch node — rename to taste.)
Step 8: Parse Summary
The Claude response has the format we asked for, but it's all in one content.text string. A small Code node parses it:
const raw = $input.first().json.content.text;
// Split on the "METRICS:" header
const [bulletsBlock, metricsBlock] = raw.split(/METRICS:/i);
const bullets = bulletsBlock
.replace(/BULLETS:/i, "")
.trim();
const metrics = (metricsBlock || "")
.trim();
return [{ json: { bullets, metrics } }];
Step 9: Create Notion Page
Final node — Notion:
- Resource: Database Page
- Operation: Create
- Database: "Earnings Watch"
- Properties:
- Ticker (Title):
{{ $node["Code1"].json.ticker }} - Company (Text):
{{ $node["HTTP Request"].json.results.company_name }} - Call Date (Date):
{{ $node["HTTP Request"].json.results.call_date }} - Earnings ID (Text):
{{ $node["HTTP Request"].json.results.earnings_id }} - Summary (Text):
{{ $node["Parse"].json.bullets }} - Metrics (Text):
{{ $node["Parse"].json.metrics }} - Transcript Link (URL):
https://earningscalls.dev/transcripts/{{ $node["HTTP Request"].json.results.slug }} - Sector (Select):
{{ $node["HTTP Request"].json.results.sector }}
- Ticker (Title):
The Full Workflow JSON
Export-ready n8n workflow that you can paste into your instance — gives you the full chain wired up. (Trim sensitive data before sharing externally.)
{
"name": "Earnings Watch → Notion",
"nodes": [
{ "id": "trigger", "type": "n8n-nodes-base.scheduleTrigger",
"parameters": { "rule": { "interval": [{ "field": "minutes", "minutesInterval": 30 }]}}},
{ "id": "tickers", "type": "n8n-nodes-base.code",
"parameters": { "jsCode": "const tickers=['NVDA','MSFT','AAPL','META','GOOG','AMZN','TSLA','CRM'];return tickers.map(t=>({json:{ticker:t}}));"}},
{ "id": "fetchLatest", "type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://earningscalls.dev/api/v1/transcripts/recent",
"qs": { "ticker": "={{ $json.ticker }}", "limit": 1 },
"headers": { "X-API-Key": "={{ $env.EARNINGSCALLS_API_KEY }}" }}}
// ... continued with notionQuery, ifNew, fetchTranscript, claudeCall,
// parseSummary, createNotion — full file ~120 lines
]
}
(Full JSON gets long; load the workflow template from earningscalls.dev's example repo, link at the end.)
The Cost Receipts
For a 12-ticker watchlist during a typical earnings week:
| Step | Per execution | Per week |
|---|---|---|
/transcripts/recent poll |
12 requests × 60 ticks/week | 720 |
/transcripts/:id (only when new) |
~12 calls report per week × 1 | 12 |
| earningscalls API total | 732/week ≈ 2,928/month | |
| Claude Haiku summary | ~12/week × $0.0015 each | ~$0.07/month |
| Notion API | Free up to 3 req/sec | — |
| n8n (cloud Starter) | — | $20/month flat |
| Grand total | ~$45/month |
That's 58.6% of the Pro plan's monthly budget on the API side. To trim:
- Poll every 60 min instead of 30 → halves API usage to 30%
- Use
/earnings/upcomingto short-circuit → only poll tickers reporting today, drops to <10% usage - Self-host n8n on a $5 VPS → saves $15/month vs n8n cloud
If you reduce polling to once per hour AND use the calendar short-circuit, your earningscalls usage drops to about 250-400 requests/month — under 8% of Pro plan budget.
Why n8n Beats Custom Code Here
I'm partial to writing a Node bot when the workflow has complex branching logic. But for this shape — schedule, fetch, filter, transform, store — n8n wins on three axes:
- Visual debugging. You can re-run any node in isolation with the actual data from the last execution. When something breaks, you see exactly which step failed and with what input. Cuts debugging time by 80%.
- Non-engineers can edit it. Your analyst can swap "Notion" for "Airtable" by replacing one node, no PR required.
- Built-in retry + error handling. Each node has configurable retry, exponential backoff, and error-branch routing. Reproducing this in code takes ~50 lines of try/catch boilerplate.
The tradeoff: when your workflow has loops with state, or recursive logic, n8n gets awkward. For anything in the "watch X, transform Y, write to Z" shape — n8n is unbeatable.
Variations
The same backbone supports many downstream destinations. Swap Step 9's Notion node for:
- Airtable — same database semantics
- Linear — file each call as an issue in a "Research" project
- Google Sheets — for the spreadsheet-natives on your team
- PostgreSQL — write to your own datastore for further analysis
- Webhook to your internal app — push to whatever system already exists
The earningscalls → Claude → format pipeline stays identical. The destination is a 1-node swap.
Get an API key and have the workflow running before this week's reports drop.