| title | Messages |
|---|---|
| description | Headline numbers to Slack, Teams, Google Chat and email, built from a report's own queries. |
| section | build-reports |
| position | 4 |
The complete headline project includes query SQL, output YAML, conditional sending and profiles. The report reference lists shared keys; the glossary defines message versus file output.
Many people who depend on a report never open the file: they want yesterday's revenue, how
many payments failed, whether the month closed. A message output turns a report's query
results into a short headline, a title plus a few lines of text, and posts it to Slack, Microsoft
Teams, Google Chat or an inbox. It needs no plugin of its own: the message format is built
into DRE. New in 0.3.
A message is written to the target path as a .md file too, so any file destination (a
Databricks Volume, S3, SFTP, a local folder) delivers it unchanged and keeps a history of what
was sent.
Aggregate in SQL, and let the message read the result:
-- reports/finance/daily/headline.sql
select
sum(amount) filter (where day = date '{{ run.date.prev_day.iso }}') as revenue,
sum(amount) filter (where day = date '{{ run.date.prev_day.iso }}') /
nullif(sum(amount) filter (where day = date '{{ run.date.add(days=-8).iso }}'), 0) - 1 as change
from {{ source('sales', 'orders') }}# reports/finance/daily/daily.yml
queries: [headline]
output:
format: message
title: "Revenue {{ run.date.prev_day.iso }}"
text: |
Revenue yesterday: **{{ results.headline.value | currency('EUR') }}**
({{ results.headline.first.change | percent | signed }} on the same day last week)
destination:
- {profile: team_slack, channel: "#finance"}In Slack this arrives as:
Revenue 2026-10-03 Revenue yesterday: €12,340 (+4.1% on the same day last week)
format: message alone, with no text:, already works: the default template writes one block
per query (a single value as revenue: 12,340, one row as column: value lines, several rows as
a list of up to ten with + N more).
For each query of the output, results.<query> has:
value |
the first column of the first row |
first.<column> |
a column of the first row |
rows |
the rows, each row.<column> or row[0] (at most max_rows, default 1,000) |
row_count |
the true number of rows, even past max_rows |
columns |
the column names |
sets[n] |
a result set by index (one .sql file makes one, so sets[-1] is the same as the query) |
Values keep their types: numbers stay numbers, dates are DRE dates, nulls are none. The number
filters make them readable: number, percent, signed, currency and compact, in the
project's locale:. var(), run.* and your macros work as everywhere.
The text is a small Markdown subset that every service can show: **bold**, *italic*,
[links](https://...), `code` and - bullets. No headings or tables. Each destination
translates it, and every value the template prints is escaped, so a customer called
acme_corp* can't break the formatting. Keep a longer template in a file with file: messages/daily.md instead of text:.
output: can be a list. The queries run once, every output is made from the same results, and
file outputs are delivered before messages, so a message can link to what was delivered:
queries: [headline, detail]
output:
- name: workbook
format: xlsx
queries: [detail] # the headline query isn't a tab in the workbook
destination:
- {profile: reports_s3, path: "s3://reports/daily-{{ run.date.yyyymmdd }}.xlsx"}
- name: headline
format: message
queries: [headline] # the message doesn't read the 10k-row detail
text: |
Revenue yesterday: **{{ results.headline.value | currency('EUR') }}**
Full report: {{ outputs.workbook.location }}
destination:
- {profile: team_slack, channel: "#finance"}
- {profile: finance_mail, to: cfo@example.com, attach: [workbook]}outputs.<name> has location (where it was delivered), files and status. attach: sends
another output's files along with the message, to destinations that take both: the email
arrives with the headline in its body and the workbook attached; in Slack the file goes in the
same post. Teams and Google Chat take messages only, so link the file instead.
A per-row message (for_each) attaching a
per-row output with the same driver and key sends each row its own file.
Any output can carry when:, a condition over the results. When it's false the output is
skipped: nothing is written or sent, the run output says so, and run_results.json records it as
skipped. It's not a failure.
queries: [failed_payments]
output:
format: message
when: "results.failed_payments.row_count > 0"
title: "Failed payments {{ run.date.iso }}"
text: |
**{{ results.failed_payments.row_count }}** payments failed:
{% for p in results.failed_payments.rows[:10] %}- {{ p.customer }}: {{ p.amount | currency('EUR', 2) }}
{% endfor %}
destination: {profile: ops_teams}A message whose text renders empty is skipped the same way, so {% if %} inside text: works as
a condition. when: works on file outputs too ("only send the CSV if there are rows").
Each run decides from its own results. DRE keeps nothing between runs: there's no "alert once", no deduplication and no escalation. A report that should say "revenue dropped" says it every day the data shows it. Alerting on whether a run succeeded belongs to your orchestrator or the Platform.
invalid-output-option identifies a malformed message template or a query name absent from
the output. invalid-destination-option can identify attach naming no file output, or a
message-only destination that cannot accept it. A runtime render-failed often means the
message expects a non-null first row; aggregate in SQL or handle none explicitly.
See error codes and preview after repairing the cause.
dre validatechecks every template compiles, thatresults.<x>names one of the output's queries andoutputs.<x>another output, and that a file output orattach:doesn't go to a destination that only takes messages.dre run <report> --previewprints each message (title, text, its length against each destination's limit, and whetherwhen:passed) and sends nothing. Numbers then come from the row sample.dre validate -s <report>lists every output with its destinations; production targets stand out.
A real run logs one line per message sent, and run_results.json keeps its full title and text.
| Destination | Takes | Limit | |
|---|---|---|---|
slack |
messages and files | 4,000 characters | over the limit: cut short, full .md attached |
email |
messages and files | HTML body with a plain-text alternative; subject from the title | |
teams |
messages only | 15,000 characters | a Workflows webhook |
google_chat |
messages only | 4,000 characters | a space webhook |
| any other | the .md file |
s3, sftp, databricks Volumes, local, ... |
See the message format for every option, and
practices for advice.
Previous: Lookups · Next: Per-row outputs