Scheduled Exports
Scheduled exports turn WALDO into a recurring report engine. Pick an analysis (or a market), pick a format and the sections you want, pick a schedule, pick where it should be delivered. WALDO does the rest — every Monday morning, every quarter-end, every first-of-the-month.
TL;DR for CRE professionals: This is the Monday-morning-PDF feature. Set up one schedule and a fresh PDF / Excel / PowerPoint of your Cuyahoga County analysis (or whatever you choose) lands in your inbox — or in your team's Slack channel via webhook — on whatever cadence makes sense for the deal. No more manual exports. No more "did anyone send the latest numbers?"
What you get from this
- Recurring reports without the busywork. A weekly PDF lands in your inbox the moment the workweek starts. You read; you don't generate.
- Multiple destinations per schedule. Send the same export to your team email AND to a webhook URL that drops it into Slack / Make / your CRM.
- Big files handled gracefully. Email attachments are great until they hit 25MB. WALDO automatically switches to a signed download link when the file's too big to attach.
- Per-run history. Every scheduled run creates a row you can review — see whether the email landed, whether the webhook receiver acknowledged, what error occurred if anything failed.
- Pause and resume. Going on vacation? Pause the schedule. Coming back? Resume it. No need to delete-and-recreate.
Real-world workflows
| Schedule | Use case |
|---|---|
| Daily 7am, latest analysis on the broker's hottest market, PDF, email to the team | Ops team starts the day with the freshest numbers without anyone having to remember to export. |
| Weekly Monday 6am, specific analysis locked at a deal's stage, PowerPoint, webhook to n8n → Slack | Live deal channel gets a fresh slide deck every week reflecting the latest WALDO data. |
| Monthly 1st-of-month, latest analysis on a target acquisition market, Excel, email to the principal + acquisitions team | Leadership review cycle gets a consistent recurring artifact without anyone exporting manually. |
| Daily, latest analysis on a market, PDF, webhook URL into Make → Google Drive | The team's shared Drive folder always has yesterday's snapshot, automatically. |
Step-by-step: creating your first scheduled export
- Sign in as a Professional or Enterprise user. (Scheduled exports aren't included on Basic — upgrade in Settings → Billing if needed.)
- Go to Settings → Integrations in the left sidebar.
- Click "New schedule."
- Name it something memorable (Weekly Cuyahoga PDF, Monthly Hamilton MSA Excel).
- Pick a source:
- Specific analysis — re-export this exact analysis on the schedule. Useful when you want a deal-locked snapshot that doesn't change.
- Latest analysis on a market — each tick exports your most recently completed analysis on this market. Useful when you want the freshest numbers automatically picked up.
- Pick a format (PDF, Excel, or PowerPoint) and the sections you want included.
- Pick the schedule — frequency (daily / weekly / monthly), day-of-week or day-of-month if applicable, the time, and your timezone.
- Pick destinations — at least one of:
- Email recipients (comma-separated). Put yourself, your team, your client.
- Webhook URL — any HTTPS endpoint you want to receive a JSON envelope with a download link. Great for piping into Slack, Make / Zapier / n8n, your CRM, or a custom dashboard.
- Click "Create schedule." If you specified a webhook URL, the signing secret will be revealed once — save it now so your receiver can verify deliveries.
- Click "Run now" to test. Within ~30 seconds you'll see the run row update to Succeeded (green) or Failed (red) with a clear error if something went wrong. The email lands; the webhook fires.
Email behavior
WALDO sends one email per scheduled run, per email destination.
- Subject: text
<Your org name> export — <schedule name> (<format>) - Body: A short summary block (market, format, when it was generated) plus either:
- Attachment — when the file is < 20MB. You get the PDF/Excel/PowerPoint right there in the email.
- Signed download link — when the file is ≥ 20MB. You get a button in the email; click it to download. The link is valid for 7 days.
- The summary block uses your org's white-label branding when set.
Multiple recipients: just enter comma-separated emails. They each get the message. CCs are supported.
If your SMTP provider rejects a recipient, the run is marked partial — other destinations still succeed independently.
Webhook behavior
When you add a webhook URL as a destination, WALDO POSTs a signed JSON envelope to it after every run. The envelope is shaped like the regular outbound webhooks (same
Waldo-SignatureeventUse this when you want the export to flow into your own automation: post into Slack, attach to a AcquisitionPRO® or your CRM contact note, append to a Google Sheet via Make/Zapier, kick off a downstream job.
The webhook payload includes a download_url
Pause, resume, delete
- Pause — the schedule is kept on file but no further runs occur. Resume it any time and the next run picks up at the next scheduled occurrence.
- Delete — removes the schedule and all run history. Permanent.
- Run now — out-of-band manual run; useful for testing destinations after you've changed something. Doesn't shift .text
next_run_at
Tier requirements + cost
Scheduled exports are included on Professional and Enterprise plans. Each run consumes the same cost-units as a manual
GET /v1/analyses/:id/exportRun history retains 30 days; older runs are auto-deleted. The signed download link from any past run is valid only for 7 days from the run.
For your integrator (or AI agent)
Everything below is the technical reference. Paste a link to this page into Claude / ChatGPT / Cursor and ask the AI to wire up the receiver — it has all the information it needs.
Architecture
- A user-defined schedule lives in . Stores cron + IANA timezone, source pointer (specific analysis ID or market_code+market_type), format, sections, destinations array, signing secret,text
scheduled_exports.textnext_run_at - Vercel Cron runs every 15 minutes. Selects active rows wheretext
/api/internal/jobs/scheduled-exports, advancestextnext_run_at <= NOW()(usingtextnext_run_atwith timezone awareness), inserts atextcron-parserrow intextscheduled_export_runs, enqueues a QStash message to the worker.textpending - Worker (QStash-signed) callstext
/api/internal/jobs/run-scheduled-exportwhich generates the export viatextdispatchScheduledExport(runId), fans out to destinations, and updates the run row.textlib/export/generate.ts - Per-run history persists 30 days; cleanup cron prunes older.
Schedule shapes (created via picker)
The settings UI sends one of:
json{ "kind": "daily", "hour": 7, "minute": 0 } { "kind": "weekly", "dayOfWeek": 1, "hour": 7, "minute": 0 } { "kind": "monthly", "dayOfMonth": 1, "hour": 7, "minute": 0 }
dayOfWeekdayOfMonthhourminuteThe server converts to a 5-field POSIX cron:
<minute> <hour> <dom> * <dow>cron-parsernext_run_atDestination shapes
json{ "type": "email", "config": { "to": ["a@b.com"], "cc": ["c@d.com"] } } { "type": "webhook", "config": { "url": "https://hooks.example.com/x" } }
Webhook URLs are validated at create AND at dispatch time using the same SSRF guard as outbound webhooks (private IPs / cloud metadata / loopback / link-local rejected).
Webhook envelope
Each scheduled-export delivery to a webhook destination has:
textPOST /your-handler Content-Type: application/json User-Agent: Waldo-ScheduledExports/1.0 Waldo-Event: scheduled_export.ready Waldo-Signature: t=<unix>,v1=<hex> { "id": "<run_id>", "event": "scheduled_export.ready", "created_at": "<iso>", "data": { "scheduled_export_id": "<schedule_id>", "scheduled_export_name": "Weekly Cuyahoga PDF", "format": "pdf", "filename": "WALDO_CuyahogaCounty_2026-04-30.pdf", "byte_size": 1234567, "download_url": "https://app.waldocre.io/api/exports/scheduled/<run_id>/download?token=..." } }
Signature scheme is identical to outbound webhooks — see the Webhooks docs for verification snippets.
Email envelope
- Subject: text
<companyName> export — <scheduleName> (<FORMAT>) - Body: HTML + plain text alternative
- Attachment OR link: based on text
buffer.length < 20 * 1024 * 1024 - Branding resolved viatext
companyName— same path the export pipeline uses for white-labeltextresolveExportBranding(userId)
Signed download link
GET /api/exports/scheduled/{run_id}/download?token=<token>- No session required — the token IS the auth.
- Token format: where MAC = HMAC-SHA256 overtext
<run_id>.<expiry-unix-seconds>.<hex-hmac>keyed ontext${run_id}.${expiry}(env).textWALDO_DOWNLOAD_TOKEN_SECRET - Default TTL: 7 days from the run.
- Expired: 410 Gone. Bad signature: 403. Missing run: 404.
- The download regenerates the export on demand via rather than serving a stored buffer (zero storage cost; data may be slightly fresher than what was emailed — accepted UX trade for v1).text
generateExport()
Internal management API (Supabase JWT auth)
Same auth pattern as
/api/settings/api-keys/api/settings/webhooksvalidatePremiumAccess()| Method | Path | Description |
|---|---|---|
| GET | text | List caller's schedules |
| POST | text | Create + reveal signing secret once if any webhook destination |
| GET | text | Fetch one |
| PATCH | text | Update name, sections, schedule, destinations, active |
| DELETE | text | Delete (cascades runs) |
| POST | text | Out-of-band manual run; returns text |
| GET | text | Recent run history |
Run statuses
pendingrunning- — all destinations delivered cleanly.text
succeeded - — at least one destination succeeded, at least one failed. Each destination's outcome is recorded intext
partial(jsonb array).textdelivery_results - — all destinations failed, OR the export couldn't be generated at all.text
failed
The worker returns 500 to QStash only on total failures (DB unreachable, etc.), so QStash retries. Per-destination failures don't trigger a retry — that's the user's call (Run now, fix the receiver, etc.).
Retention + cleanup
- rows older than 30 days are deleted by the dailytext
scheduled_export_runscron at 03:30 UTC.textcleanup-export-runs - Download tokens are invalid 7 days after the run; even if you keep an old run row, the link won't work past that window.
Error shapes
The internal management API returns simple JSON errors:
json{ "error": "schedule_tz must be a valid IANA timezone" }
The signed download endpoint returns:
json{ "error": "Token expired" } // 410 { "error": "Token bad-signature" } // 403 { "error": "Run not found" } // 404