Skip to main content
Plexara
Product12 min read

608 - Results out: notification channels and signed delivery to other systems

How a result leaves the platform, to people and to systems. An administrator creates channels in the portal: an email list, a Mattermost channel, or an incoming webhook, the last two delivering through an API connection that holds the credential, so whoever may reach the connection may send. A person sends through the agent, an automation sends as itself, and an administrator sends a test. Every message is queued, shown in the delivery history, and retried on a fixed rule. A webhook channel in JSON format posts an envelope a system dedupes on and routes by, signed by its connection with the secret never in a script or a recording, and received on the demo tenant by a webhook source as one row per delivery.

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

  1. 01Name the three kinds of notification channel, what each carries, and where an administrator manages them.
  2. 02Say who may send to a channel, and why a channel holds no credential of its own.
  3. 03Send to a channel from a session and from an automation, and explain why a send is accepted for delivery rather than delivered.
  4. 04Read the delivery history, and state exactly how a delivery is retried and when it stops.
  5. 05Read the JSON envelope a webhook channel sends a system, and dedupe and route on it.
  6. 06Choose how a connection signs what it sends, and say where the secret lives and where it never appears.
  7. 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

Open index

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. 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. 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. 3

    Enabled

    A disabled channel refuses what is sent to it instead of collecting messages nobody will deliver.

  4. 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. 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.

The Notification Channels card in the portal’s admin settings, listing ops-events, an incoming webhook delivering through the acme-ops-events connection, and store-ops, an email list with one recipient, each with its description.
The demo tenant’s two channels, one for systems and one for people, with the connection or recipient count each one uses.
The ops-events channel open in the editor: its description, the acme-ops-events connection, JSON format, enabled, daily digest off, 60 messages per hour, and a green confirmation that the channel’s upstream accepted the test message beside the Save channel and Send test buttons.
Send test on ops-events: the test was signed by the connection, verified by the receiving source, and confirmed in the editor.

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 history

The 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

The acme-ops-events API connection open in the editor: base URL https://mcp-demo.plexara.io/hooks/ops-events, Auth mode HMAC signature, a masked signing secret, the Platform preset, SHA-256 algorithm and hex encoding, an X-Signature header with the sha256= prefix, and the timestamp and body as the signed content.
The demo’s signing connection: the Platform preset fills in every part of the signature, and the secret shows only as a mask.

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

A New Connection form, not saved, for an API at https://bi.example.com with Auth mode Session sign-in: sign-in URL /api/auth/signin with method POST, a JSON sign-in body with a {{secret}} placeholder, application/json body content type, an empty Secret field, and an Accept header for the sign-in request.
Session sign-in in the connection editor, filled with example values and not saved: where to sign in, the body to send with the secret in place, and the secret itself, stored encrypted.

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.