Intermediate~20 min setupCommunication & CRMVerified April 2026
Slack logo
Zoho CRM logo

How to Send Daily Sales Digests from Zoho CRM to Slack with n8n

A scheduled n8n workflow that pulls deal and pipeline data from Zoho CRM every morning and posts a formatted sales summary to one or more Slack channels.

Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.

Best for

Sales teams of 5-50 people who want a consistent daily snapshot of pipeline health without anyone manually pulling Zoho CRM reports.

Not ideal for

Teams that need real-time deal alerts — use a webhook-based Zoho CRM trigger instead of a scheduled digest.

Sync type

scheduled

Use case type

reporting

Real-World Example

💡

A 12-person B2B SaaS sales team uses this to post a 7am digest to #sales-daily every weekday. Before automation, the sales manager exported a Zoho CRM report manually each morning, copied numbers into Slack, and the whole process took 15 minutes — and got skipped on busy days. Now the digest appears automatically: deals closed yesterday, pipeline value by stage, and the top 5 open deals by expected close date.

What Will This Cost?

Drag the slider to your expected monthly volume.

/mo
505005K50K

Each platform counts differently — Zapier: 1 task per trigger. Make: 1 operation per module per record. n8n: 1 execution per run.

Prices shown for annual billing. Based on published pricing as of April 2026.

Estimated ROI

1000

min saved/mo

$583

labor value/mo

Free

no platform cost

Based on ~2 min manual effort per operation at $35/hr fully loaded labor cost.

Implementation

Skip the setup

Import this workflow directly into n8n

Copy the pre-built n8n blueprint and paste it straight into n8n. All modules, filters, and field mappings are already configured — you just need to connect your accounts.

Before You Start

Make sure you have everything ready.

Zoho CRM account with an API-enabled user role — free plans do not support API access; you need at minimum the Standard plan
Zoho CRM user used for OAuth must have 'Deals' module read permission and access to all deal records (not just owned)
Slack workspace admin access or permission to install apps — needed to authorize the n8n Slack OAuth app
n8n instance running v1.0+ (cloud or self-hosted) — the Zoho CRM node with COQL support requires v1.0 or later
Target Slack channel already created and the n8n Slack bot invited to it with /invite before you test

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Deal NameDeal_Name
Deal AmountAmount
StageStage
Closing DateClosing_Date
Deal OwnerOwner
4 optional fields▸ show
Account NameAccount_Name
ProbabilityProbability
Lead SourceLead_Source
Modified TimeModified_Time

Step-by-Step Setup

1

n8n Dashboard > Workflows > + New Workflow

Create a new workflow in n8n

Log into your n8n instance (cloud at app.n8n.cloud or self-hosted). Click the '+ New Workflow' button in the top-right corner of the Workflows list. Give it a clear name like 'Daily Sales Digest — Zoho to Slack'. You'll land on a blank canvas with a default Start node — delete that node, you won't need it here.

  1. 1Click '+ New Workflow' in the top-right
  2. 2Click the workflow title at the top and rename it to 'Daily Sales Digest — Zoho to Slack'
  3. 3Click the default Start node and press Delete to remove it
What you should see: You should see a blank canvas with no nodes and your new workflow name displayed at the top.
2

Canvas > + Node > Schedule Trigger

Add a Schedule trigger node

Click the '+' button in the center of the canvas to open the node picker. Search for 'Schedule' and select the 'Schedule Trigger' node. In the right panel, set 'Trigger Interval' to 'Days', set 'Trigger at Hour' to 7 (for 7am), and set 'Trigger at Minute' to 0. Make sure the timezone matches your team's timezone — n8n defaults to UTC, which will catch most teams off guard.

  1. 1Click the '+' button on the canvas
  2. 2Type 'Schedule' in the search box
  3. 3Select 'Schedule Trigger'
  4. 4Set 'Trigger Interval' to 'Days'
  5. 5Set 'Trigger at Hour' to 7 and 'Trigger at Minute' to 0
  6. 6Open the 'Settings' tab inside the node and set your team's timezone
What you should see: The Schedule Trigger node appears on the canvas showing '0 7 * * *' or your equivalent cron expression in the node summary.
Common mistake — n8n uses UTC by default. If your team is in US Eastern time, set the hour to 12 (UTC) to fire at 7am ET. Check this before going live or the digest will arrive at odd hours.
n8n
+
click +
search apps
Slack
SL
Slack
Add a Schedule trigger node
Slack
SL
module added
3

Canvas > + Node > Zoho CRM > Credentials > Create New

Connect Zoho CRM and authenticate

Click the '+' after the Schedule Trigger to add a new node. Search for 'Zoho CRM' and select it. In the 'Credential' dropdown, click 'Create New Credential'. A popup will ask for your Zoho CRM Data Center region (US, EU, IN, AU, CN) — pick the one matching your Zoho account. Click 'Connect to Zoho CRM', which opens an OAuth popup. Sign into Zoho with an account that has at least read access to Deals and Reports.

  1. 1Click '+' after the Schedule Trigger node
  2. 2Search for 'Zoho CRM' and select it
  3. 3In 'Resource' dropdown, leave it on 'Deal' for now
  4. 4Click 'Credential' > 'Create New Credential'
  5. 5Select your Zoho Data Center region from the dropdown
  6. 6Click 'Connect to Zoho CRM' and complete OAuth in the popup
What you should see: You should see a green 'Connected' badge next to the credential name inside the Zoho CRM node.
Common mistake — If you pick the wrong data center region (e.g., US instead of EU), OAuth will complete but every API call will return a 400 or empty dataset. Double-check your Zoho account URL — accounts at zoho.eu use the EU region.
4

Zoho CRM Node > Resource: Deal > Operation: Search

Fetch yesterday's closed deals from Zoho CRM

In the Zoho CRM node, set 'Resource' to 'Deal' and 'Operation' to 'Search'. In the 'Criteria' field, enter a COQL query to pull deals where Closing_Date equals yesterday and Stage equals 'Closed Won'. Use the expression editor to build yesterday's date dynamically — this is where n8n's expression syntax earns its keep. This node will return all matching deal records as individual items.

  1. 1Set 'Resource' to 'Deal'
  2. 2Set 'Operation' to 'Search'
  3. 3Enable 'Use COQL Query' toggle
  4. 4Paste the COQL query: SELECT Deal_Name, Amount, Stage, Owner, Account_Name FROM Deals WHERE Closing_Date = '{{$today.minus({days:1}).format('yyyy-MM-dd')}}' AND Stage = 'Closed Won'
  5. 5Set 'Max Records' to 200 to capture even high-volume days
What you should see: After clicking 'Test Step', you should see a list of deal records from yesterday in the Output panel. If nothing closed yesterday, you'll see an empty array — that's fine.
Common mistake — Zoho CRM COQL date literals must use the format yyyy-MM-dd. Using MM/dd/yyyy will silently return zero results with no error message.
5

Canvas > + Node > Zoho CRM > Resource: Deal > Operation: Get All

Fetch open pipeline deals for today's snapshot

Add a second Zoho CRM node after the first. This one fetches all deals NOT in a closed stage, so the team can see overall pipeline health. Set 'Operation' to 'Get All' and 'Resource' to 'Deal'. Use the 'Fields' option to request only the fields you need: Deal_Name, Amount, Stage, Closing_Date, Owner. Fetching all fields on 500+ deals will slow the workflow down noticeably.

  1. 1Click '+' after the first Zoho CRM node
  2. 2Add another 'Zoho CRM' node
  3. 3Set 'Resource' to 'Deal' and 'Operation' to 'Get All'
  4. 4Under 'Options', add a filter: Stage not_equal 'Closed Won' AND Stage not_equal 'Closed Lost'
  5. 5Under 'Fields', specify: Deal_Name, Amount, Stage, Closing_Date, Owner
  6. 6Set 'Return All' to true if your pipeline has more than 200 deals
What you should see: Test output shows multiple deal records representing your current open pipeline. Each item should contain Amount as a number, not a string.
6

Canvas > + Node > Code

Aggregate and calculate pipeline totals with a Code node

Add a 'Code' node after the second Zoho CRM node. This node takes the raw deal arrays and produces a single summary object: total pipeline value, deal count by stage, and the top 5 deals by amount. Without this step, you'd be passing raw JSON to Slack, which is unreadable. The Code node uses JavaScript — see the Pro Tip section below for the full script to paste in.

  1. 1Click '+' after the second Zoho CRM node
  2. 2Search for 'Code' and select the 'Code' node
  3. 3Set 'Mode' to 'Run Once for All Items'
  4. 4Paste the aggregation script from the Pro Tip section below
  5. 5Click 'Test Step' to verify the output shows a single summary item
What you should see: The Code node output shows one item containing totalPipelineValue, dealCountByStage (object), closedYesterdayCount, closedYesterdayValue, and topDeals (array of 5).
Common mistake — Set Mode to 'Run Once for All Items' — not 'Run Once for Each Item'. If you leave it on the default per-item mode, the script runs 200+ times and produces 200+ summary objects.

Paste this into the first Code node (set to 'Run Once for All Items'). It reads closed-yesterday deals from the first Zoho CRM node and open-pipeline deals from the second, then produces a single clean summary object. Reference it in the next Code node via $('Aggregate Summary').item.json to build your Slack blocks.

JavaScript — Code Node// Code node: 'Aggregate Summary'
▸ Show code
// Code node: 'Aggregate Summary'
// Mode: Run Once for All Items
const closedDeals = $('Zoho CRM - Closed Yesterday').all().map(item => item.json);

... expand to see full code

// Code node: 'Aggregate Summary'
// Mode: Run Once for All Items

const closedDeals = $('Zoho CRM - Closed Yesterday').all().map(item => item.json);
const pipelineDeals = $('Zoho CRM - Open Pipeline').all().map(item => item.json);

// Sum closed revenue from yesterday
const closedYesterdayValue = closedDeals.reduce((sum, deal) => {
  return sum + parseFloat(deal.Amount || 0);
}, 0);

// Sum total open pipeline value
const totalPipelineValue = pipelineDeals.reduce((sum, deal) => {
  return sum + parseFloat(deal.Amount || 0);
}, 0);

// Count deals by stage
const dealCountByStage = {};
for (const deal of pipelineDeals) {
  const stage = deal.Stage || 'Unknown';
  dealCountByStage[stage] = (dealCountByStage[stage] || 0) + 1;
}

// Top 5 open deals by amount
const topDeals = [...pipelineDeals]
  .sort((a, b) => parseFloat(b.Amount || 0) - parseFloat(a.Amount || 0))
  .slice(0, 5)
  .map(deal => ({
    name: (deal.Deal_Name || 'Unnamed').substring(0, 40),
    amount: '$' + parseFloat(deal.Amount || 0).toLocaleString('en-US', { maximumFractionDigits: 0 }),
    owner: deal.Owner?.name || deal.Owner || 'Unassigned',
    closeDate: deal.Closing_Date
      ? new Date(deal.Closing_Date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
      : 'No date set',
    stage: deal.Stage || 'Unknown'
  }));

// Format currency helper
const fmt = (val) => '$' + val.toLocaleString('en-US', { maximumFractionDigits: 0 });

return [{
  json: {
    closedYesterdayCount: closedDeals.length,
    closedYesterdayValue: fmt(closedYesterdayValue),
    totalPipelineValue: fmt(totalPipelineValue),
    openDealCount: pipelineDeals.length,
    dealCountByStage,
    topDeals,
    reportDate: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
    reportTimestamp: new Date().toISOString()
  }
}];
7

Canvas > + Node > Code

Format the Slack message with a second Code node

Add another 'Code' node after the aggregation node. This one builds the Slack Block Kit JSON that makes the digest look clean — sections, bold headers, deal tables. Slack's Block Kit format requires precise JSON structure. Trying to hand-build this in an 'Expression' field inside the Slack node will drive you insane. Do it here in code and pass the pre-built blocks array to the Slack node.

  1. 1Click '+' after the aggregation Code node
  2. 2Add another 'Code' node
  3. 3Set 'Mode' to 'Run Once for All Items'
  4. 4Build the blocks array using the summary data from the previous node (reference via $('Code').item.json)
  5. 5Include a header block, a stats section, a top-deals list, and a footer with the report timestamp
What you should see: Output contains one item with a 'blocks' array. Paste the blocks value into Slack's Block Kit Builder at app.slack.com/block-kit-builder to visually verify formatting before wiring up the Slack node.
Common mistake — Slack Block Kit text fields have a 3,000-character limit per section block. If your top-deals list is long, truncate deal names to 40 characters or split into multiple section blocks.

Paste this into the first Code node (set to 'Run Once for All Items'). It reads closed-yesterday deals from the first Zoho CRM node and open-pipeline deals from the second, then produces a single clean summary object. Reference it in the next Code node via $('Aggregate Summary').item.json to build your Slack blocks.

JavaScript — Code Node// Code node: 'Aggregate Summary'
▸ Show code
// Code node: 'Aggregate Summary'
// Mode: Run Once for All Items
const closedDeals = $('Zoho CRM - Closed Yesterday').all().map(item => item.json);

... expand to see full code

// Code node: 'Aggregate Summary'
// Mode: Run Once for All Items

const closedDeals = $('Zoho CRM - Closed Yesterday').all().map(item => item.json);
const pipelineDeals = $('Zoho CRM - Open Pipeline').all().map(item => item.json);

// Sum closed revenue from yesterday
const closedYesterdayValue = closedDeals.reduce((sum, deal) => {
  return sum + parseFloat(deal.Amount || 0);
}, 0);

// Sum total open pipeline value
const totalPipelineValue = pipelineDeals.reduce((sum, deal) => {
  return sum + parseFloat(deal.Amount || 0);
}, 0);

// Count deals by stage
const dealCountByStage = {};
for (const deal of pipelineDeals) {
  const stage = deal.Stage || 'Unknown';
  dealCountByStage[stage] = (dealCountByStage[stage] || 0) + 1;
}

// Top 5 open deals by amount
const topDeals = [...pipelineDeals]
  .sort((a, b) => parseFloat(b.Amount || 0) - parseFloat(a.Amount || 0))
  .slice(0, 5)
  .map(deal => ({
    name: (deal.Deal_Name || 'Unnamed').substring(0, 40),
    amount: '$' + parseFloat(deal.Amount || 0).toLocaleString('en-US', { maximumFractionDigits: 0 }),
    owner: deal.Owner?.name || deal.Owner || 'Unassigned',
    closeDate: deal.Closing_Date
      ? new Date(deal.Closing_Date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
      : 'No date set',
    stage: deal.Stage || 'Unknown'
  }));

// Format currency helper
const fmt = (val) => '$' + val.toLocaleString('en-US', { maximumFractionDigits: 0 });

return [{
  json: {
    closedYesterdayCount: closedDeals.length,
    closedYesterdayValue: fmt(closedYesterdayValue),
    totalPipelineValue: fmt(totalPipelineValue),
    openDealCount: pipelineDeals.length,
    dealCountByStage,
    topDeals,
    reportDate: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
    reportTimestamp: new Date().toISOString()
  }
}];
8

Canvas > + Node > Slack > Credentials > Create New

Connect Slack and authenticate

Add a 'Slack' node after the formatting Code node. In the Credential dropdown, click 'Create New Credential' and choose 'Slack OAuth2 API'. This opens an OAuth flow — click 'Connect to Slack', select your workspace, and grant the bot the scopes it needs. The minimum required scopes are chat:write and channels:read. If you're posting to a private channel, also grant groups:read.

  1. 1Click '+' after the formatting Code node
  2. 2Search 'Slack' and select the Slack node
  3. 3Set 'Resource' to 'Message' and 'Operation' to 'Post'
  4. 4Click 'Credential' > 'Create New Credential' > 'Slack OAuth2 API'
  5. 5Click 'Connect to Slack' and authorize in the popup
  6. 6Confirm scopes include chat:write and channels:read
What you should see: A green 'Connected' badge appears next to the Slack credential. The 'Channel' field in the Slack node becomes an active dropdown you can search.
Common mistake — The Slack bot must be manually invited to the target channel with /invite @YourBotName before it can post there. OAuth completing does not auto-add the bot to channels.
9

Slack Node > Resource: Message > Operation: Post

Configure the Slack message post

In the Slack node, set 'Channel' to your target channel (e.g., #sales-daily). For 'Message Type', select 'Blocks'. In the 'Blocks' field, use an expression to reference the blocks array built in the previous Code node: {{ $json.blocks }}. Set 'Text' to a plain-text fallback like 'Daily Sales Digest — see thread for details' — this shows in Slack notifications and for accessibility.

  1. 1Set 'Channel' to your target Slack channel
  2. 2Set 'Message Type' to 'Blocks'
  3. 3In the 'Blocks' field, click the expression toggle and enter: {{ JSON.stringify($json.blocks) }}
  4. 4Set 'Text' (fallback) to 'Daily Sales Digest'
  5. 5Under 'Options', set 'Username' to 'Sales Bot' and optionally add a bot icon emoji
What you should see: After clicking 'Test Step', check your Slack channel — the formatted digest message should appear immediately with headers, deal stats, and the top-deals list.
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
message template
🔔 New Record: {{text}} {{user}}
channel: {{channel}}
ts: {{ts}}
#sales
🔔 New Record: Jane Smith
Company: Acme Corp
10

n8n Workflow Settings > Error Workflow > Select Workflow

Add error handling so failures notify you

Click the three-dot menu on the Slack node and select 'Add Error Workflow' — or add a separate error path. The simplest approach: add an 'Error Trigger' workflow that posts a Slack DM to the sales manager when the main workflow fails. Without this, a broken Zoho CRM token or Slack API timeout will fail silently and nobody gets a digest that morning.

  1. 1Click the three-dot menu in the top-right of the canvas
  2. 2Select 'Workflow Settings'
  3. 3Under 'Error Workflow', click the dropdown and select '+ Create New Error Workflow'
  4. 4In the new error workflow, add an 'Error Trigger' node and connect it to a Slack node that DMs the sales manager
What you should see: The main workflow shows 'Error Workflow: [your error workflow name]' in its settings panel.
Common mistake — Error workflows only fire when the main workflow has been activated (not in manual test mode). Test error handling by intentionally breaking a credential after activation.
11

n8n Canvas > Activate Toggle (top-right)

Activate and verify the live workflow

Click the 'Activate' toggle in the top-right of the canvas. The toggle turns green and the workflow status changes to 'Active'. To verify it works before the next scheduled run, click 'Execute Workflow' manually from the canvas. Check your Slack channel for the digest and review the Executions log in n8n (left sidebar > Executions) to confirm all nodes returned success status.

  1. 1Click the Activate toggle in the top-right — it should turn green
  2. 2Click 'Execute Workflow' to run it immediately as a test
  3. 3Check the target Slack channel for the digest message
  4. 4Click 'Executions' in the left sidebar and open the latest run to inspect each node's output
What you should see: The Executions log shows all nodes with green checkmarks. The Slack channel has a formatted sales digest with real deal data.

Scaling Beyond 500+ open deals in Zoho CRM pipeline+ Records

If your volume exceeds 500+ open deals in Zoho CRM pipeline records, apply these adjustments.

1

Enable 'Return All' on the pipeline fetch node

By default, Zoho CRM's API returns 200 records per page. If your pipeline has more than 200 open deals, enable 'Return All' in the Zoho CRM node options. n8n will handle pagination automatically, but each additional page is a separate API call — 1,000 deals means 5 calls.

2

Filter fields to reduce payload size

Fetching all Zoho CRM fields on 500+ records significantly increases execution time and memory use. Specify only the fields you need (Deal_Name, Amount, Stage, Closing_Date, Owner) using the 'Fields' option in the Zoho node. This alone can cut payload size by 70-80%.

3

Watch Zoho API rate limits at scale

Zoho CRM Standard plan allows 5,000 API calls per day per org. If you're running the digest twice daily plus other Zoho integrations, monitor usage in Zoho's Setup > API Usage dashboard. The open pipeline fetch on a large dataset can consume 5-10 calls per run due to pagination.

4

Limit the top-deals list to avoid Slack block limits

Slack Block Kit messages have a 50-block maximum per message. If you display more than 10-15 deals in the digest, you risk hitting this limit. Cap the top-deals list at 5-7 entries in the Code node and link to the full Zoho CRM report view for anyone who wants the complete list.

Going live

Production Checklist

Before you turn this on for real, confirm each item.

Troubleshooting

Common errors and how to fix them.

Frequently Asked Questions

Common questions about this workflow.

Analysis

VerdictWhy n8n for this workflow

Use n8n for this if you're self-hosting for data privacy reasons, need to run custom JavaScript to shape the Zoho CRM output, or want zero per-execution cost at high run frequency. The Code node is the real reason to pick n8n here — formatting Zoho's raw deal JSON into a readable Slack Block Kit message requires actual logic (sorting, summing, truncating strings), and doing that in Zapier's Formatter or Make's data mapping UI is painful. The one scenario where you'd skip n8n: if nobody on your team has even basic JavaScript comfort, Make's visual aggregation tools will get you to the same output faster and with less debugging.

Cost

n8n cloud costs $20/month for the Starter plan, which includes 2,500 workflow executions. This workflow consumes 1 execution per day — 365/year — which is nowhere near the limit. At the Pro tier ($50/month), you get 10,000 executions. Self-hosted n8n is free with no execution limits, just your server costs. Compare that to Zapier, where this workflow requires a multi-step Zap at $49/month minimum, and you're paying 4x for the same outcome. Make's Core plan at $9/month covers this workflow easily — it's cheaper than n8n cloud if cost is the only factor.

Tradeoffs

Make has a better visual aggregator for summing deal amounts without code — the Array Aggregator module handles it in a few clicks. Zapier's 'Formatter' step can do basic number formatting but falls apart when you need sorted lists or conditional logic. Power Automate has native Zoho CRM connectors only through third-party premium connectors ($1.50-$2/run), making it expensive for a daily workflow. Pipedream's Node.js environment is equally capable as n8n's Code node but requires managing deployments separately. n8n wins here because the Code node and the Zoho/Slack nodes live in the same canvas, no context switching, and the self-hosted option removes cost entirely for teams with a server already running.

Three things you'll hit after setup. First: Zoho's Owner field comes back as a nested object ({id: '...', name: 'Sarah Chen'}) in some API responses and as a plain string in others, depending on which operation you use. Always use deal.Owner?.name in your Code node and add a fallback. Second: the Slack bot silently fails to post if it's removed from the channel (which happens when channel membership is cleaned up). Add a monitoring step that checks once a week that the bot is still a member. Third: Zoho CRM's API throttles at 5,000 calls/day on Standard plans — if you add a second workflow that also hits the Deals module, you can hit this limit faster than expected. Check Zoho's API usage dashboard weekly during the first month.

Ideas for what to build next

  • Add a weekly pipeline trend comparisonExtend the workflow to run on Mondays and compare this week's pipeline value to last week's. Store last week's total in a Google Sheet or n8n's static data, then calculate the delta and include a trend arrow (↑ or ↓) in the Slack message.
  • Route digests to individual rep channelsAfter aggregating deal data, add a SplitInBatches node to group deals by Owner and post a personalized digest to each rep's Slack DM — so every salesperson sees only their own pipeline without exposing the full team's numbers.
  • Add stale deal alerts to the digestIn the Code aggregation node, flag any open deals where Modified_Time is more than 7 days ago and surface them in a 'Needs Attention' section at the bottom of the digest. This turns the digest from a passive report into an action list.

Related guides

Was this guide helpful?
Slack + Zoho CRM overviewn8n profile →