ERP Event Messaging Outbox Table SQL Generator
If an ERP posts a message directly to WhatsApp or Teams inside a transaction, a slow API call holds locks and a failed one loses the notification. A transactional outbox solves it: write the message to a table with the business change, and let a worker send it. This generator produces the SQL.
Fill in
PostgreSQL: table
-- Generic names: adapt column names and keys to your own schema. CREATE TABLE IF NOT EXISTS messaging_outbox ( id BIGSERIAL PRIMARY KEY, channel VARCHAR(20) NOT NULL, event_type VARCHAR(60) NOT NULL, dedupe_key VARCHAR(120) NOT NULL, recipient VARCHAR(120) NOT NULL, payload JSONB NOT NULL, status VARCHAR(12) NOT NULL DEFAULT 'pending', attempts INT NOT NULL DEFAULT 0, next_attempt TIMESTAMPTZ NOT NULL DEFAULT now(), provider_id VARCHAR(120), last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), sent_at TIMESTAMPTZ, CONSTRAINT uq_messaging_outbox_dedupe UNIQUE (channel, dedupe_key) ); CREATE INDEX IF NOT EXISTS ix_messaging_outbox_due ON messaging_outbox (next_attempt) WHERE status = 'pending';
PostgreSQL: enqueue in the same transaction as the business change
INSERT INTO messaging_outbox (channel, event_type, dedupe_key, recipient, payload)
VALUES ('whatsapp', 'shipment_dispatched', 'SO-104522-dispatch', '919800000000',
'{"order":"SO-104522","eta":"2026-10-21"}')
ON CONFLICT (channel, dedupe_key) DO NOTHING;PostgreSQL: worker claims a batch
WITH due AS ( SELECT id FROM messaging_outbox WHERE status = 'pending' AND next_attempt <= now() AND attempts < 6 ORDER BY next_attempt LIMIT 20 FOR UPDATE SKIP LOCKED ) UPDATE messaging_outbox o SET status = 'sending', attempts = o.attempts + 1 FROM due WHERE o.id = due.id RETURNING o.id, o.channel, o.recipient, o.payload;
PostgreSQL: record the result
-- On success
UPDATE messaging_outbox SET status = 'sent', sent_at = now(), provider_id = 'wamid.XXXX' WHERE id = 1;
-- On transient failure: exponential backoff, back to pending
UPDATE messaging_outbox
SET status = 'pending', last_error = 'HTTP 429',
next_attempt = now() + (power(2, attempts) * interval '30 seconds')
WHERE id = 1;
-- Give up after the maximum attempts
UPDATE messaging_outbox SET status = 'failed' WHERE status = 'pending' AND attempts >= 6;SQL Server: worker claim equivalent
WITH due AS ( SELECT TOP (20) * FROM messaging_outbox WITH (UPDLOCK, READPAST, ROWLOCK) WHERE status = 'pending' AND next_attempt <= SYSUTCDATETIME() AND attempts < 6 ORDER BY next_attempt ) UPDATE due SET status = 'sending', attempts = attempts + 1 OUTPUT inserted.id, inserted.channel, inserted.recipient, inserted.payload;
Table and column names here are generic, not an ERP vendor schema. Insert the outbox row in the same database transaction as the business change, then let a separate worker send. Delivery is at least once, so rely on the dedupe key and provider message IDs to avoid duplicates. SQL Server needs a datetime2 type for next_attempt and nvarchar(max) for payload; adjust types. Reset rows stuck in sending after a timeout.
Outbox design review
Optional review of your ERP notification pipeline for reliability and duplicates.
Optional. The tool and every export work without this. No spam.
How to use the SQL
Run the table script in a database you control, not inside a vendor ERP schema unless your vendor supports custom tables. Have your integration or a trigger insert a row when the business event commits. Run one or more workers that claim a batch with the locking query, call the messaging API, and write the result. Choose the PostgreSQL or SQL Server variant that matches your platform.
- The unique dedupe key stops the same event queuing twice.
- SKIP LOCKED or READPAST lets several workers run without blocking.
- Backoff pushes next_attempt out after each transient failure.
- A failed status gives support a list of messages that need a human.
Why the outbox pattern
It separates the business commit from the network call. If the messaging API is down, the ERP transaction still succeeds and the queue drains later. If the worker crashes after sending but before updating, the message may be sent again, so delivery is at least once and the provider message ID or dedupe key is your protection.
Worked example
A shipment is posted at 16:05. The same transaction inserts an outbox row with the dedupe key SO-104522-dispatch. A worker claims it at 16:05:10, calls the WhatsApp template send, stores the returned message ID and marks it sent. If the call returns 429, the row returns to pending with next_attempt 60 seconds later and attempts incremented.
Common mistakes
Forgetting an index on due rows makes the polling query slow once the table grows. Never purging sent rows lets the table balloon, so archive or delete after a retention period. Leaving rows in sending forever after a worker crash silently drops messages.
Frequently Asked Questions
What is a transactional outbox?
It is a table where an application writes the messages it needs to send, in the same transaction as its business data. A separate process reads the table and delivers the messages, so a failure in delivery never rolls back or loses the business change.
Why use SKIP LOCKED or READPAST?
They let several workers pull rows from the same table without waiting on each other. Each worker skips rows another worker has locked, which gives parallel throughput without double sending the same row.
Does this give exactly once delivery?
Not strictly. It gives at least once delivery with strong duplicate protection from the dedupe key and stored provider IDs. True exactly once across an external API is not generally achievable.
Can I add this to a vendor ERP database?
Check your ERP vendor's support terms first. Many teams put the outbox in a separate integration database and replicate or call into it, to avoid upgrade and support conflicts with the vendor schema.
If you want a reliable outbox and worker layer between your ERP and messaging channels, Netray can design and run it with your DBA team.
Related Tools
Messaging Webhook Retry Backoff Schedule Calculator
Work out how long an exponential backoff retry policy keeps trying before a WhatsApp, Teams or Slack message is parked for review.
Developer ToolsMessaging Throughput and Rate Limit Headroom Calculator
Estimate peak message rate, how much headroom you have under a rate limit, how many workers you need and how long a burst takes to drain.
Developer ToolsWhatsApp Cloud API Send Template Request Generator
Builds the Graph API request, JSON payload and a Python equivalent for sending an approved template with up to three body variables.
Developer ToolsWebhook Event Test Case Worksheet
A test plan grid for messaging webhooks: inbound text, status receipts, duplicates, bad signatures and ERP failures. Exports to Excel.
More Developer Tools tools
See all- SAP Sales Order ABAP SELECT Generator (VBAK, VBAP, VBFA)
- SAP Sales Order Create BAPI ABAP Generator (BAPI_SALESORDER_CREATEFROMDAT2)
- SAP ABAP Open Items SELECT Generator
- SAP ACDOCA SQL Query Generator
- SAP QM Inspection Lot ABAP SELECT Generator
- SAP Quality Notification Pareto SQL Query Generator
- SAP QM Usage Decision BAPI Skeleton
- ABAP Custom Exception Class Generator
- ABAP Unit Test Class Generator