What you will take away from this lesson
Lesson 607 brought events in from other systems. This lesson sends results the other way. An automation that finds something worth knowing has two kinds of reader: people, who read a message in their inbox or their chat client, and systems, which open a ticket or update a record when a message of the right type arrives.
Plexara reaches both through notification channels. This lesson follows two channels on the demo tenant: an email list for the store operations team, and a webhook channel that posts signed JSON to a receiving system, which in this case is the demo’s own webhook source, so every delivery can be read back as a row.
Learning Objectives
- 01Name the three kinds of notification channel, what each carries, and where an administrator manages them.
- 02Say who may send to a channel, and why a channel holds no credential of its own.
- 03Send to a channel from a session and from an automation, and explain why a send is accepted for delivery rather than delivered.
- 04Read the delivery history, and state exactly how a delivery is retried and when it stops.
- 05Read the JSON envelope a webhook channel sends a system, and dedupe and route on it.
- 06Choose how a connection signs what it sends, and say where the secret lives and where it never appears.
- 07Deliver from one deployment to another deployment’s webhook source.
Where this lesson sits
Lesson 607 ended with an automation that reads the events another system sends. This lesson closes the loop in the other direction: telling people and systems what an automation found. It uses the script loop from 602, the outputs from 603, and the triggers from 604.
600 Series: Automations
- 601Do not spend AI on what a script can doThe automation gap, what integration platforms and laptop scripts each cost, and the division of labor: a script for the deterministic part, the model for writing it and for judgment.
- 602The agent writes the first scriptThe loop from a recorded draft through tests to the save, what a save requires, what validate and a dry run report, the dialect’s deliberate absences, the traps that fail a draft, and the script’s page in the portal.
- 603Outputs: feeds, reports, and dashboards that refresh themselvesOutput identity across runs, tables and documents, the semi-dynamic dashboard and its data region, the referencing pattern, version caps, and delivery to a bucket drop.
- 604Running it: by hand, from the portal, and on a scheduleThree triggers, the cadence builder, the pinned fire date, what a schedule guarantees, and the run history with every trigger, duration, output, and log.
- 605What a run may do, and the record it leavesThe authority a run carries, what a save refuses, why the language cannot reach out, the lifecycle, ownership and transfer, and the administrator’s view.
- 606Scripts as skills: the weekly review the agent runs on your businessThree scripts attached to one prompt, the agent running them and reading the outputs against the knowledge graph, and a weekly list of action items with the figures behind them.
- 607Events in: a webhook source, its table, and an automation that reads itCreating a source, how a sender proves itself and what it is told, the table that serves events from acknowledgement, compaction and retention, the Webhooks page, and an automation that reads only what is new.
- 608Results out: notification channels and signed delivery to other systemsChannel kinds and who may send to them, sending from a session and from an automation, the delivery history and the retry rule, the JSON envelope a system routes on, signed requests, and delivery to another deployment.
The 600 series picks up the registered table from the 500 series and the prompt mechanics from the 400 series. It is the last numbered series in the curriculum.
Three kinds of channel
A channel is a destination that is not one person. Each kind suits a different reader, and each needs something different to reach it.
The three kinds of channel
- Email list
- One to twenty recipient addresses, and no connection.
- A subject, the body, and a button that opens the link. Each recipient gets their own copy, so each keeps their own delivery mode, daily digest, and unsubscribe link: one person opting out silences nobody else.
- Mattermost
- An API connection holding the bot’s token and the site address, and the id of the chat channel to post in.
- A title, the markdown body, and the link, as a post from the bot.
- Incoming webhook
- An API connection whose address is the webhook’s URL, and a format: Text or JSON.
- Text posts the title and body as one message, the form chat incoming webhooks accept. JSON posts an envelope for a system: every field, the body whole, and the sender’s structured data.
Every kind receives the same thing from a sender: a title, a markdown body, and an optional link back to whatever produced it. Each shows as much as it can, and past its own size limit it shows an excerpt and the link, so a long report arrives as a summary and a pointer instead of a wall of text or a message cut off mid-sentence.
The channels on the demo tenant
Every trace in this lesson comes from two channels on the ACME demo tenant, one for each kind of reader, and the webhook source that receives the second.
The channels on the demo tenant
People
store-ops
- An email list: “Store operations team: failed automations and daily sales alerts”. Immediate delivery, up to 60 messages an hour.
Systems
ops-events
- An incoming webhook in JSON format: “Operations event feed for systems”. It delivers through the API connection acme-ops-events, which signs every request.
The receiver
ops-events webhook source
- A source created under Admin > Webhooks that accepts only requests carrying a valid signature and a fresh timestamp, takes the envelope’s id as the event id and its type as the event type, and writes each delivery to scratch.uploads.webhook_ops_events.
An automation
ACME returns alert (lesson 608)
- Written for this lesson. Each run reads the last 24 hours of the point-of-sale feed from lesson 607 and tells both channels which stores reached a threshold of returns.
Pointing the webhook channel back at the same tenant is a demonstration convenience. The receiver would normally be another team’s system, or another deployment’s webhook source, and nothing on the sending side changes when it is: the connection’s address and secret are the only things that name the receiver.
Creating a channel
Channels are created and edited by an administrator on your team, in the portal under Admin > Settings > Notification Channels.
Admin > Settings > Notification Channels
- 1
Name and description
The name is what a person or a script sends to. The description is shown to anyone choosing between channels, so it says who reads the channel and what belongs in it.
- 2
Kind, and what the kind needs
Email list, Mattermost, or Incoming webhook. An email list takes its recipients; the other two take the API connection they deliver through, plus the chat channel id for Mattermost and the format for a webhook.
- 3
Enabled
A disabled channel refuses what is sent to it instead of collecting messages nobody will deliver.
- 4
Daily digest and messages per hour
A channel can collect a day’s messages into one bulletin instead of delivering each as it is sent; the channel decides, and a sender cannot override it. Messages per hour caps how much one channel carries.
- 5
Send test
Delivers a test message at once through the channel’s own path and reports what the receiving end said, so a wrong address, a revoked token, or a receiver that rejects the signature shows in the editor, not in somebody’s run.
Creating a channel is an administrator’s action in the portal. The agent has no tool for it, so a channel exists because a person on your team decided where messages may go. On the demo, Send test on ops-events came back “the channel’s upstream accepted the test message”, and the receiving source holds the test as an event of type test.
The channels screen on the demo tenant
The two channels as an administrator sees them, and the webhook channel open in the editor just after a test send.


Who can send, and why a channel holds no credential
Three kinds of sender reach the same channels through the same path, and one rule decides which channels each can reach.
Who sends to a channel
A person, through their agent
The notify tool: list, send, publish
List shows the channels this session can reach. Send posts a title, a markdown body, an optional link, and for a JSON webhook optional structured data. Publish posts a saved asset: its title, an excerpt, and a link to it.
An automation
notify and publish from the script
The run sends as the script, with the roles its author held at the save. The post links back to the run that produced it unless the script names its own link. A rehearsal run does not post unless it is told it may write.
An administrator
Send test on the channel’s editor
Delivers a test at once, through the same path as every other message, and reports the receiving end’s answer.
A Mattermost or webhook channel holds no credential. It names an API connection, and the token or the signing secret is that connection’s, stored encrypted and rotated in one place. That also decides who may send: whoever may reach the connection may reach the channel, and a persona denied the connection does not see the channel listed. Restricting a channel to fewer people means giving it its own connection. An email list names no connection, so anyone who can send at all can send to it. Sending is an ordinary tool call, so persona rules, rate limits, and the audit log apply to it as to any other.
A person sends
A person asks the agent to tell the store operations team, or the event feed, about something they found. The agent lists the channels first, because channel names are not guessable and a session sees only the channels it can reach.
A person sends, through the agent
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
notify action list
-> store-ops email immediate
"Store operations team: failed automations and daily sales alerts"
notify action send
channel ops-events
title Three stores at four returns in the last 24 hours
body Stores 175, 89 and 276 each logged **4 returns** in the last
24 hours of the point-of-sale feed, the most of any store. …
data {"window_hours": 24, "stores": [{"store_id": 175, "returns": 4,
"returned_amount": 740.6}, …]}
-> queued 1
delivered false
detail accepted for delivery to "ops-events"; an administrator can
see the outcome in the platform's notification historyThe first list was taken before ops-events existed, so it shows the one channel the demo had. The answer to a send says queued, not delivered, because at that moment the message is a row in the delivery queue and nothing has reached the receiver yet. The same message went to store-ops without the data, which an email list ignores.
An automation sends
The same two messages, written once as a script, so they go out whenever the condition holds without anybody asking.
acme-returns-alert-608, its main()
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
def main():
threshold = run.params.get("threshold", 4)
rows = stores_over(threshold, run.fire_time)
if not rows:
platform.result({"stores": 0, "notified": []})
return
title, body = summary(rows, threshold)
data = {
"window_hours": 24,
"threshold": threshold,
"stores": [{"store_id": int(r["store_id"]), "returns": int(r["returns"]),
"returned_amount": float(r["returned_amount"])} for r in rows],
}
platform.notify("store-ops", title, body)
platform.notify("ops-events", title, body, data = data)
platform.result({"stores": len(rows), "notified": ["store-ops", "ops-events"]})
# stores_over reads the 24 hours before the fire time from
# scratch.uploads.webhook_pos_sales, one row per event id,
# and keeps the stores at or above the threshold.The agent wrote it through the loop from 602. Because a rehearsal does not post unless it may write, the draft was run with writes allowed, which sent real messages to both channels and recorded the run. One test replays that recording and checks both sends and the figures in the data; a second declares an empty day and checks that nothing is sent anywhere. The save reached every statement.
Returns are counted once per event id. The point-of-sale sender never retries, but a script that turns events into messages should not depend on that: a sender that retries would otherwise put the same return in a count twice.
One run of it
The saved automation, run once
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
run_script name acme-returns-alert-608
-> status succeeded
trigger tool
result {"stores": 6, "notified": ["store-ops", "ops-events"]}
log notify: send to store-ops: accepted for delivery to "store-ops"
notify: send to ops-events: accepted for delivery to "ops-events"Six stores had four returns in the day before the run. The store operations team got “6 stores at 4 or more returns in the last 24 hours” with one line per store, and the event feed got the same message with the six stores’ figures in its data. A schedule from 604 would send it every morning; on a day with no store at the threshold it sends nothing.
Queued delivery and the delivery history
A send returns once the message is accepted for delivery. What happened to it next is in the delivery history, which an administrator reads under Dashboard > Notifications.
Dashboard > Notifications on the demo tenant
- 14
- Sent
- 0
- Pending or sending
- 0
- Failed
- 30 days
- How long the history keeps a row
- Recipient
- An address for an email, channel:ops-events for a channel delivery.
- Category
- channel for these sends, beside shares, comments, mentions, and failed automation runs.
- Subject
- The message title.
- Status and attempts
- Pending, sending, sent, or failed, and how many tries.
- Error
- On a failed row, opened from the list: what the receiving end said, in its own words.
The counts were read just after the person’s sends, before the automation’s; each count filters the list beneath it. Both of the person’s messages show as sent on the first attempt, the email to [email protected] and the delivery to channel:ops-events. Every message from every sender goes through this one queue, so there is no faster path that skips the history, and an outage at the receiving end becomes a retry instead of a failed script. Each person also sees the messages addressed to them under Settings, beside their own preferences, without the error text, which concerns systems they cannot act on.
Retries
A receiver that is down for a minute should cost nothing, and a receiver that has said no should not be asked again. The queue follows six rules.
How a delivery is retried, and when it stops
- 01
Five attempts at most
A delivery that fails in a way that may pass later (a timeout, a refused connection, an error on the receiving server, a rate limit) is tried again, up to five attempts in all.
- 02
Waits of 30 seconds, then 1, 2, and 4 minutes
The wait doubles after each failed attempt. A row that fails its fifth attempt is marked failed with the last error recorded on it.
- 03
The receiver’s retry hint is honored, up to 32 minutes
A receiver that answers 429 or 503 with a Retry-After is waited for as long as it asks, up to 32 minutes, the longest wait the queue ever makes.
- 04
410 Gone stops at once
A receiver that says the destination is gone gets no further attempts, and the row records that it asked for no more deliveries.
- 05
A refusal stops at once
Any other 4xx answer, a deleted or disabled channel, a revoked token, or a chat channel the bot was never invited to fails the row on the first attempt, with an excerpt of the answer, instead of trying five times to say the same thing.
- 06
A retry carries the same id
Every attempt of one message carries the same envelope id, so a receiver that saw the first attempt arrive but whose answer was lost recognizes the second as a copy.
Any 2xx is delivered. Delivery is at least once: the queue cannot know whether a receiver that timed out had already acted, so it sends again, and the id is how the receiver tells. A message is picked up as soon as it is queued, and an attempt that a failing server abandons partway is picked up by another.
A notification for a system
A webhook channel in JSON format posts one envelope per message, for a system that branches on fields instead of a person reading text.
The envelope the receiving system got from the saved run
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
{
"id": "ntf_34",
"type": "script.finding",
"occurred_at": "…T00:34:17.628459Z",
"deployment": "Plexara",
"channel": "ops-events",
"title": "6 stores at 4 or more returns in the last 24 hours",
"body": "- Store 175: 4 returns, $740.60\n- Store 89: 4 returns, $376.57\n…",
"link": "https://mcp-demo.plexara.io/portal/automations/db041ffa…/runs/dpx_e123017f…",
"source": {"kind": "script", "script": "mcp:script:db041ffa…", "run_id": "dpx_e123017f…"},
"data": {"window_hours": 24, "threshold": 4,
"stores": [{"store_id": 175, "returns": 4, "returned_amount": 740.6}, …]}
}- id
- The same on every attempt of one message, and the field a receiver dedupes on. A test send gets a new id each time.
- type
- What produced it, and the field a receiver routes on: script.finding or script.published from an automation, notify.sent or notify.published from a person, test from Send test.
- source
- The script and the run for an automation, user for a person, admin for a test.
- data
- The sender’s structured payload, exactly as sent, up to 64 KiB. Only a JSON webhook channel carries it.
- deployment, channel
- Which deployment and which channel sent it, for a receiver fed by several.
Read back from the receiving source’s table, abbreviated where marked. A system reading this does not parse the title: it routes script.finding to whatever handles findings and reads the six stores from data. The link opens the run that sent it, so a person following up from the receiving system lands on the run’s record.
Signed requests, and where the secret lives
A receiver needs to know a request really came from your deployment. The connection a webhook channel delivers through proves it by signing every request, set in the connection editor with the auth mode HMAC signature.
Connection editor, auth mode HMAC signature: the presets
Standard Webhooks
Signs the message id, the timestamp, and the body, in base64, with the id, timestamp, and signature in the headers that standard names. A whsec_ secret is read the way the standard reads it.
GitHub
Signs the body with SHA-256 in hex, written as sha256= followed by the signature, in the header GitHub receivers check.
Stripe
Signs the timestamp and the body, and writes both into one signature header in Stripe’s layout.
Platform
Signs the timestamp and the body with SHA-256 in hex, the convention a Plexara webhook source verifies. This is what the demo’s acme-ops-events uses.
A preset fills in a receiver’s whole convention; any field set beside it overrides that part, for a receiver that signs a little differently. The signature covers the exact bytes sent. The timestamp is taken as each request goes out, so a retry is signed afresh instead of arriving stale. The same signing applies to everything that goes through the connection: a channel’s deliveries and a script’s own calls alike.
The secret is the connection’s, stored encrypted and never shown again. A script cannot sign a request itself, by design: it would need the secret in its source, and the source and every recording of its runs are readable by the people who can read the script. A receiver that takes its secret in the URL instead, as many chat incoming webhooks do, gets it from the connection’s Path secret field, which is stored the same way and never appears in a call’s recorded path, the audit log, or an error. Rotating the secret is an edit to the connection; the receiver keeps the old one valid for an overlap, as a Plexara source does.
The signing connection on the demo
On the demo, the ops-events channel delivers through the acme-ops-events connection, which signs with the Platform preset that a Plexara webhook source checks.

Delivering to another deployment
The receiving end on the demo is a webhook source, set up exactly as lesson 607 described, with the signature check the Platform preset produces. One deployment’s automations can deliver to another’s this way, and the receiving deployment reads the deliveries as a table.
What the receiving source holds
What the agent calls. This is the exchange the agent has with Plexara on your behalf, shown for the technical reader. You ask in plain language; you never type any of it.
trino_query
connection scratch
sql SELECT event_id, event_type, json_format(json_extract(payload, '$.source')) AS source,
json_array_length(json_extract(payload, '$.data.stores')) AS stores
FROM scratch.uploads.webhook_ops_events ORDER BY received_at
-> event_id event_type source stores
ntf_test_1791419459535200988 test {"kind":"admin"} -
ntf_29 notify.sent {"kind":"user"} 3
ntf_32 script.finding {"kind":"script","run_id":"dpx_80632f9e…"} 6
ntf_34 script.finding {"kind":"script","script":"mcp:script:…", 6
"run_id":"dpx_e123017f…"}Four deliveries, four events, each with a signature the source checked before it stored anything: the administrator’s test, the person’s send with three stores, the recorded draft, and the saved run. The draft’s row names only its run, because a draft is not yet a script; the saved run’s names both. Because the source takes the envelope id as its event id, a retried delivery is one event once its window is compacted, exactly as 607 described for any sender that retries.
What a receiver does
Whether the receiver is a Plexara source or a service your own team runs, five rules make it safe to receive the same delivery twice.
What a receiving system does with each delivery
- 01
Verify over the raw body
Compute the signature over the bytes exactly as they arrived, before parsing them. Parsing and re-encoding changes the bytes and the signature no longer matches.
- 02
Refuse a stale timestamp
A request whose timestamp is more than a few minutes from the receiver’s clock is refused even when its signature is right. That makes a captured request useless to replay.
- 03
Drop an id already seen
Answer it with success and do nothing. Delivery is at least once, and the id is the same on every attempt.
- 04
Route on type, read data
The title and body are for people. A system branches on type and reads the fields it needs from data.
- 05
Answer with the right status
Any 2xx once the message is safely kept. 429 or 503 with a Retry-After to be asked again later. 410 to stop deliveries for good. Anything else is taken as a refusal and not retried.
A Plexara webhook source does all five without being written: it checks the signature and the timestamp, keeps the earliest copy of each id, and stores the envelope as a row any automation can route on. For a receiver your own team builds, these five are the whole contract, and Send test is the way to prove its signature check before the first real message.
Systems that issue sessions
The session sign-in form
The form below is filled with example values and was never saved.

Results out, end to end
A result on the demo tenant went to people by email and to a system as a signed envelope, from a person and from an automation, through one queue with one history and one retry rule.
Key terms
Five terms cover how a result leaves the platform.
Key Terms
- Notification channel
- A destination that is not one person, created by an administrator under Admin > Settings > Notification Channels: an email list, a Mattermost channel, or an incoming webhook.
- Envelope
- The JSON a webhook channel in JSON format posts for a system: an id that is the same on every attempt, a type to route on, the source, the message, and the sender’s data.
- Delivery history
- Every queued message with its recipient, status, and attempts, under Dashboard > Notifications for administrators and under Settings for each person’s own messages.
- HMAC signatureauth mode
- A connection’s way of proving its requests: a hash of the exact bytes sent, keyed with a secret the receiver also holds, so the receiver knows who sent the request and that nothing changed on the way.
- At-least-once delivery
- A message may arrive more than once, never zero times while the receiver keeps answering. The receiver dedupes on the envelope id.
