| name | delivery-routing |
| description | Route research summaries to local markdown, Telegram, or Slack with approval gates and delivery-state updates. |
Delivery Routing
Send the run summary to the configured destination through cortextOS-safe
delivery paths. Mark items as delivered only after successful delivery.
When to Use
After brief-generation writes the run summary.
Input
research/output/YYYY-MM-DD/summary.md
research/output/YYYY-MM-DD/signals-selected.json (needed to mark delivered_at on success)
config.json (delivery config)
research/db/signals.db (to mark delivered_at after successful delivery)
Output
- Message sent to configured destination, OR approval request created through cortextOS approvals
delivered_at set on delivered items in research/db/signals.db
- Delivery status appended to
research/output/YYYY-MM-DD/run.log
Approval Gate
Always check research.delivery.requires_approval before sending externally.
External destinations are telegram and slack. local_markdown and none do
not leave the machine.
If true (default)
Create a cortextOS approval, write a local approval context file, notify the
user, and stop. Do not send externally until the approval is granted.
Every approval must have a parent task. If this delivery was started by a cron
or ad-hoc workflow and no task exists yet, create a run task first and mark it
in_progress.
research/output/YYYY-MM-DD/PENDING-APPROVAL.md
# Delivery Pending Approval -- YYYY-MM-DD
Run complete. Summary ready but not sent (`research.delivery.requires_approval = true`).
**Approval:** Created through `cortextos bus create-approval`.
**Summary path:** research/output/YYYY-MM-DD/summary.md
**Signals selected:** N
**Top signals:**
1. [Title] (score: X.X)
2. [Title] (score: X.X)
3. [Title] (score: X.X)
Use the normal approval workflow:
APPR_ID=$(cortextos bus create-approval \
"Send research summary for YYYY-MM-DD" \
external-comms \
"Destination: ${DESTINATION}. Summary: research/output/YYYY-MM-DD/summary.md. Pending context: research/output/YYYY-MM-DD/PENDING-APPROVAL.md")
cortextos bus send-telegram "$CTX_TELEGRAM_CHAT_ID" \
"Approval needed: research summary for YYYY-MM-DD is ready. Check dashboard approval ${APPR_ID}."
cortextos bus log-event action approval_created info \
--meta "{\"approval_id\":\"${APPR_ID}\",\"destination\":\"${DESTINATION}\"}"
If this run has a parent task, block that task on the approval. If it was cron
started and has no parent task, create one before requesting approval, then
block that task on the approval ID.
Log: Delivery pending approval. Request at research/output/YYYY-MM-DD/PENDING-APPROVAL.md approval_id=$APPR_ID
If false
Send to configured destination automatically, then mark delivered_at.
Destinations
Telegram
cortextos bus send-telegram "$CTX_TELEGRAM_CHAT_ID" "$MESSAGE"
Message format: run date, signals collected vs. selected, top 3-5 titles with scores,
any source failures, path to full output folder.
Keep under 4,096 characters. If longer, split into two messages.
Slack
Use Slack only when the workspace webhook is configured and approval policy
allows external delivery.
curl -s -X POST "${SLACK_WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-d '{"text": "MESSAGE"}'
Local Markdown
Write research/output/YYYY-MM-DD/DELIVERED-summary.md as a copy of the summary.
No external HTTP calls.
None
No delivery. Summary stays at research/output/YYYY-MM-DD/summary.md.
Message Content
Send only the summary -- not full brief text. The full briefs live on disk at
research/output/YYYY-MM-DD/briefs/.
Minimum content:
- Run date and time
- Number of signals selected out of total collected
- Top 3-5 signals: title, score
- Any source failures from run.log
- Path to full output folder
Error Handling
Retry up to 3 times with 5-minute intervals between attempts.
import time
def deliver_with_retry(send_fn, max_retries=3, delay_seconds=300):
for attempt in range(1, max_retries + 1):
try:
send_fn()
return True
except Exception as e:
log(f"delivery_failure attempt={attempt} detail={e}")
if attempt < max_retries:
time.sleep(delay_seconds)
return False
If all 3 attempts fail:
- Write the summary to
research/output/YYYY-MM-DD/DELIVERY-FAILED-summary.md.
- Log:
delivery_fallback path=research/output/YYYY-MM-DD/DELIVERY-FAILED-summary.md
- Do not raise -- delivery failure is non-fatal to the run.
Config Schema
Read delivery settings from config.json:
{
"research": {
"delivery": {
"destination": "local_markdown",
"requires_approval": true,
"summary_only": true
}
}
}
Supported destinations:
local_markdown
telegram
slack
none
Mark delivered_at (After Successful Delivery Only)
After the message sends successfully, call mark_delivered():
import sqlite3, datetime as dt, json
def mark_delivered(db_path, selected_json_path):
with open(selected_json_path) as f:
selected = json.load(f)
conn = sqlite3.connect(db_path)
now = dt.datetime.utcnow().isoformat()
for item in selected:
conn.execute(
"UPDATE items SET delivered_at=? WHERE canonical_key=?",
(now, item["canonical_key"])
)
conn.execute(
"""UPDATE daily_brief_items
SET delivered=1, delivered_at=?
WHERE item_id = (SELECT id FROM items WHERE canonical_key = ?)""",
(now, item["canonical_key"])
)
conn.commit()
conn.close()
This ensures items are only suppressed from future runs if delivery actually succeeded.
If delivery failed, they remain eligible for the next run.