Skip to content
You're reading the docs for DRE 0.10 (v0.10.0). This page for 0.11

Messages

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 validate checks every template compiles, that results.<x> names one of the output’s queries and outputs.<x> another output, and that a file output or attach: doesn’t go to a destination that only takes messages.
  • dre run <report> --preview prints each message (title, text, its length against each destination’s limit, and whether when: 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.