ERPNext v15 Slack Notifications for Stock Movements

{"time":1776378046135,"version":"2.28.2","blocks":[{"type":"paragraph","data":{"text":"If you are running ERPNext v15 and want real-time Slack notifications whenever stock movements occur, this step-by-step guide shows you exactly how to set up webhook-based alerts for Material Issues, Material Transfers, and Purchase Receipts. You will also learn how to avoid the common Jinja templating error that catches most people off guard during setup."}},{"type":"paragraph","data":{"text":"This method uses ERPNext’s built-in Webhook DocType paired with Slack Incoming Webhooks. No custom app, third-party plugin, or server script is required — just a few minutes of configuration inside your ERPNext instance."}},{"type":"header","data":{"text":"How It Works","level":2}},{"type":"paragraph","data":{"text":"ERPNext includes a built-in Webhook DocType that fires HTTP POST requests whenever a document is created, submitted, updated, or deleted. In this setup, you configure webhooks that trigger on the on_submit event for stock-related documents. When a user submits a Stock Entry or Purchase Receipt, ERPNext renders a Jinja template into a JSON payload and sends it as a POST request to a Slack Incoming Webhook URL. Slack then displays a neatly formatted message in your chosen channel, including the stock entry type, item details, quantities, and warehouse locations."}},{"type":"paragraph","data":{"text":"There is one critical gotcha you need to understand before writing your Jinja templates. In ERPNext’s webhook context, the doc object behaves like a Python dictionary rather than a Frappe document object. This means that if you write doc.items in your Jinja template to access line items, Python interprets it as a call to the dictionary’s built-in .items() method — not as the child table field named “items.” The result is the error TypeError: 'builtin_function_or_method' object is not iterable, and your webhook silently fails. The fix is straightforward: use doc.get('items', []) instead, which safely retrieves the child table without any conflict."}},{"type":"paragraph","data":{"text":"You will also need two separate webhooks because Material Issue and Material Transfer are types within the Stock Entry DocType, while Purchase Receipt is an entirely separate DocType with its own data structure."}},{"type":"header","data":{"text":"Step 1 — Create a Slack Incoming Webhook","level":2}},{"type":"list","data":{"style":"ordered","items":["Go to https://api.slack.com/apps.","Create a new Slack app or open an existing one.","Under Features, enable Incoming Webhooks.","Click Add New Webhook to Workspace and select the channel where you want stock alerts to appear.","Copy the generated Webhook URL — you will need it in the next steps."]}},{"type":"paragraph","data":{"text":"Security note: Treat this URL like a password. Anyone who possesses it can post messages to your Slack channel. Never commit it to version control or share it in a public forum."}},{"type":"header","data":{"text":"Step 2 — Create the Stock Entry Webhook (Material Issue + Material Transfer)","level":2}},{"type":"paragraph","data":{"text":"In ERPNext, navigate to the search bar, type Webhook, and click New to open a blank webhook form."}},{"type":"header","data":{"text":"Webhook Settings","level":3}},{"type":"table","data":{"withHeadings":true,"content":[["Field","Value"],["Document Type","Stock Entry"],["Doc Event","on_submit"],["Request Method","POST"],["Request URL","Your Slack webhook URL"],["Enabled","Checked"]]}},{"type":"header","data":{"text":"Condition","level":3}},{"type":"paragraph","data":{"text":"Add a condition to ensure this webhook only fires for Material Issues and Material Transfers, and not for every Stock Entry type in the system:"}},{"type":"code","data":{"code":"doc.stock_entry_type in (\"Material Issue\", \"Material Transfer\")"}},{"type":"paragraph","data":{"text":"Without this condition, you will receive Slack alerts for every stock entry type — manufacture entries, repack entries, material receipts, and more — which quickly creates notification fatigue and makes alerts meaningless."}},{"type":"header","data":{"text":"Request Header","level":3}},{"type":"paragraph","data":{"text":"Add one header row so Slack correctly interprets the incoming data:"}},{"type":"table","data":{"withHeadings":true,"content":[["Key","Value"],["Content-Type","application/json"]]}},{"type":"header","data":{"text":"JSON Payload","level":3}},{"type":"paragraph","data":{"text":"Paste the following template into the Request Body field. Note that this template uses doc.get() throughout to avoid the doc.items dictionary conflict described earlier:"}},{"type":"code","data":{"code":"{\n \"text\": \"📦 *Stock Entry Submitted*\\n*Type:* {{ doc.get('stock_entry_type', '—') }}\\n*ID:* {{ doc.get('name', '—') }}\\n*Company:* {{ doc.get('company', '—') }}\\n*Posting:* {{ doc.get('posting_date', '—') }} {{ doc.get('posting_time', '') }}\\n*From WH:* {{ doc.get('from_warehouse') or '—' }}\\n*To WH:* {{ doc.get('to_warehouse') or '—' }}\\n\\n*Items:*\\n{% for i in doc.get('items', []) %}• {{ i.get('item_code', '—') }} — Qty: {{ i.get('qty', 0) }} {{ i.get('uom', '') }} ({{ i.get('s_warehouse') or i.get('t_warehouse') or '—' }})\\n{% endfor %}\"\n}"}},{"type":"header","data":{"text":"Step 3 — Create a Separate Webhook for Purchase Receipt","level":2}},{"type":"paragraph","data":{"text":"Purchase Receipt is a distinct DocType in ERPNext — it is not a subtype of Stock Entry. You cannot handle it by adding another condition to the Stock Entry webhook. A second, dedicated webhook is required."}},{"type":"header","data":{"text":"Webhook Settings","level":3}},{"type":"table","data":{"withHeadings":true,"content":[["Field","Value"],["Document Type","Purchase Receipt"],["Doc Event","on_submit"],["Request Method","POST"],["Request URL","Your Slack webhook URL"],["Enabled","Checked"]]}},{"type":"paragraph","data":{"text":"Add the same Content-Type: application/json header as before. No condition is necessary unless you want to filter alerts by a specific supplier, company, or warehouse."}},{"type":"header","data":{"text":"JSON Payload","level":3}},{"type":"code","data":{"code":"{\n \"text\": \"🧾 *Purchase Receipt Submitted*\\n*ID:* {{ doc.get('name', '—') }}\\n*Supplier:* {{ doc.get('supplier', '—') }}\\n*Company:* {{ doc.get('company', '—') }}\\n*Posting:* {{ doc.get('posting_date', '—') }} {{ doc.get('posting_time', '') }}\\n\\n*Items Received:*\\n{% for i in doc.get('items', []) %}• {{ i.get('item_code', '—') }} — Qty: {{ i.get('qty', 0) }} {{ i.get('uom', '') }} ({{ i.get('warehouse') or '—' }})\\n{% endfor %}\"\n}"}},{"type":"header","data":{"text":"Step 4 — Test the Webhooks","level":2}},{"type":"paragraph","data":{"text":"Run a quick test for each document type to confirm that Slack messages are delivered correctly and contain the expected information:"}},{"type":"list","data":{"style":"ordered","items":["Create and submit a Material Transfer Stock Entry. Check your Slack channel for the alert and verify the item details are correct.","Create and submit a Material Issue Stock Entry. Confirm the alert appears and the source warehouse is populated.","Create and submit a Purchase Receipt. Confirm it triggers the second webhook and displays the supplier and received items."]}},{"type":"header","data":{"text":"Troubleshooting","level":3}},{"type":"paragraph","data":{"text":"If no messages appear in Slack, check the ERPNext backend logs for errors. Look specifically for Jinja rendering errors or HTTP request failures. If you are running ERPNext in Docker, use one of the following commands:"}},{"type":"code","data":{"code":"docker compose logs -f backend"}},{"type":"paragraph","data":{"text":"Or from inside the bench container:"}},{"type":"code","data":{"code":"bench --site yoursite tail"}},{"type":"paragraph","data":{"text":"The most common causes of failure are an incorrect or expired Slack webhook URL, a malformed JSON payload (such as unescaped newlines or missing brackets), and Jinja template errors caused by using doc.items instead of doc.get('items', [])."}},{"type":"header","data":{"text":"Optional — Add a Clickable Link to the ERP Record","level":2}},{"type":"paragraph","data":{"text":"You can make your Slack alerts even more actionable by including a direct link to the submitted document. Add the following line inside the text field of your JSON payload, replacing erp.yourdomain.com with your actual ERPNext domain:"}},{"type":"paragraph","data":{"text":"For Stock Entries:"}},{"type":"code","data":{"code":"https://erp.yourdomain.com/app/stock-entry/{{ doc.get('name') }}"}},{"type":"paragraph","data":{"text":"For Purchase Receipts:"}},{"type":"code","data":{"code":"https://erp.yourdomain.com/app/purchase-receipt/{{ doc.get('name') }}"}},{"type":"paragraph","data":{"text":"Slack automatically converts plain URLs into clickable hyperlinks, so no additional Slack Block Kit formatting is needed. Team members can jump directly from the Slack alert to the relevant ERPNext record with a single click."}},{"type":"header","data":{"text":"Summary","level":2}},{"type":"paragraph","data":{"text":"You now have automatic, real-time Slack alerts for Material Issues, Material Transfers, and Purchase Receipts — all triggered on submission and formatted with item-level detail. Here are the key points to remember as you manage and expand these webhooks:"}},{"type":"list","data":{"style":"unordered","items":["Always use doc.get('field') instead of doc.field in ERPNext webhook Jinja templates. This prevents the doc.items conflict with Python’s built-in dictionary method and eliminates the most common webhook error.","Material Issue and Material Transfer are subtypes within the Stock Entry DocType — use a condition filter to target only those types and avoid notification noise.","Purchase Receipt is a separate DocType entirely and requires its own dedicated webhook configuration.","Treat your Slack webhook URL as a sensitive credential — store it securely and never expose it in public repositories or documentation.","Add clickable ERPNext record links to your Slack messages to help your team act on alerts quickly without searching for the document manually."]}}]}