Recipes

Send a weekly report

A Monday-morning report from your analytics API, on a schedule.

Every Monday at 09:00 in Tokyo, read last week's figures from your analytics API, write a short report and send it to Slack or email. The agent reads through a connection that allows only GET, so it can look at your numbers but cannot change them.

The run takes about 10 seconds. Most of the cost is compute, billed at $0.0002 per second, so four runs a month cost about a cent. The share of the model depends on the model you choose; read cost_micros on the first runs to see it.

  1. Gather what you need

    • An API key with the admin role, because only owners and admins create connections and schedules. See roles and keys.
    • Your analytics API over HTTPS, and a read-only key for it. This recipe assumes it has GET /metrics?period=…, where period is last_week or an ISO week such as 2026-W40. Adjust the paths to your API.
    • A Slack incoming webhook URL, or an endpoint of yours that sends email.
  2. Create the read-only connection

    The connection keeps the API key inside Ephemeral and allows GET only. The instructions tell the agent what the API offers.

    curl -X PUT $EPHEMERAL_URL/v1/connections/analytics-api \
      -H "Authorization: Bearer $EPHEMERAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "http",
        "url": "https://analytics.example.com/v2",
        "auth": { "type": "header", "header": "X-API-Key", "token": "an_live_…" },
        "allowed_methods": ["GET"],
        "instructions": "GET /metrics?period=last_week or period=2026-W40 returns signups, active_users, revenue_cents and churned_accounts for that ISO week, with the same figures for the week before. Amounts are in cents."
      }'
  3. Save the workflow

    The prompt reads the period from the run's input. The agent does not have a reliable calendar, so pass a period your API can resolve, such as last_week, instead of asking the agent to work out dates.

    curl -X PUT $EPHEMERAL_URL/v1/workflows/weekly-report \
      -H "Authorization: Bearer $EPHEMERAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Write the weekly business report for input.period. Call GET /metrics?period=<input.period> on the analytics connection and use only what it returns. Reply in Markdown, at most 150 words: a one-line headline, then signups, active users, revenue in dollars and churned accounts, each with the change from the week before. If the call fails, say so instead of estimating.",
        "connections": { "analytics": { "ref": "analytics-api", "allowed_methods": ["GET"] } },
        "shell": false,
        "timeout_seconds": 120
      }'

    There is no web setting, so the run has no internet access at all; its only reach is the analytics connection. A workflow cannot hold input, a budget or a callback; those come with each run.

  4. Schedule it for Monday morning

    0 9 * * 1 is 09:00 on Mondays, read in the schedule's timezone. The schedule's input is what every run it starts receives, so this is where you set the period. Preview it first.

    curl -X POST $EPHEMERAL_URL/v1/schedules/preview \
      -H "Authorization: Bearer $EPHEMERAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "cron": "0 9 * * 1", "timezone": "Asia/Tokyo" }'

    The times are in UTC: 00:00 UTC is 09:00 in Tokyo. Create the schedule:

    curl -X POST $EPHEMERAL_URL/v1/schedules \
      -H "Authorization: Bearer $EPHEMERAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "workflow": "weekly-report",
        "name": "Monday report",
        "cron": "0 9 * * 1",
        "timezone": "Asia/Tokyo",
        "input": { "period": "last_week" },
        "max_cost_micros": 300000
      }'

    The budget of $0.30 is reserved at each start and released when the run settles. Because the schedule owns the input, you can add another schedule on the same workflow with a different input, or change this one later with PATCH /v1/schedules/{id} and a new input. To see it work before Monday, start one now with POST /v1/schedules/{id}/run or ephemeral schedules run ID.

  5. Deliver the report

    Scheduled runs do not send callbacks, so deliver by collecting the result. This script, run every ten minutes from cron or CI, asks for the schedule's newest succeeded run, sends its output once and remembers the run ID. Replace the Slack call with a call to your mail service if you prefer email.

    report.mjs (Node.js 18 or later)
    import { readFile, writeFile } from "node:fs/promises";
    
    const { EPHEMERAL_URL, EPHEMERAL_API_KEY, SCHEDULE_ID, SLACK_WEBHOOK_URL } = process.env;
    const stateFile = "report-state.json";
    const { sent } = JSON.parse(await readFile(stateFile, "utf8").catch(() => "{}"));
    
    const response = await fetch(`${EPHEMERAL_URL}/v1/runs?schedule=${SCHEDULE_ID}&status=succeeded&limit=1`, {
      headers: { Authorization: `Bearer ${EPHEMERAL_API_KEY}` },
    });
    if (!response.ok) throw new Error(`Ephemeral returned ${response.status}`);
    const [run] = (await response.json()).items;
    if (!run || run.id === sent) process.exit(0); // nothing new since the last report
    
    await fetch(SLACK_WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text: run.output }),
    });
    await writeFile(stateFile, JSON.stringify({ sent: run.id }));

    A run that failed sends nothing, so look at GET /v1/runs?schedule={id}&status=failed&limit=1 too, or watch the schedule's last_error, so that a missing report does not go unnoticed.

  6. Rerun a report for a past week

    "Start now" reuses the schedule's input, so for another week start the workflow directly with the period you want. This time you can ask for a signed callback, which delivers the result to your endpoint as soon as it is ready. Use an idempotency key that names the week, so a repeated request never sends a second report.

    curl $EPHEMERAL_URL/v1/runs \
      -H "Authorization: Bearer $EPHEMERAL_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: weekly-report-2026-W38" \
      -d '{
        "workflow": "weekly-report",
        "input": { "period": "2026-W38" },
        "max_cost_micros": 300000,
        "callback": {
          "url": "https://example.com/hooks/ephemeral",
          "secret": "'"$EPHEMERAL_CALLBACK_SECRET"'"
        }
      }'

    Your callback endpoint verifies the signature and forwards run.output; the receiver is in the payment investigation recipe. A run started this way does not carry the schedule's ID, so it does not appear under ?schedule=.

Good to know

TopicBehavior
Time zoneTimes are read in the schedule's timezone, so a report set for 09:00 stays at 09:00 local time across daylight saving changes.
Missed startsIf Ephemeral was unavailable at 09:00, the report runs once when it is back, then the timetable continues.
OverlapWith skip_if_running on, a start is skipped while the previous run is still going.
Pausingephemeral schedules pause ID before a holiday and resume after.
Why it's safe
The connection allows only GET, and the key never reaches the microVM. The workflow has no web access and no shell, so the run can read your analytics API and nothing else. The budget caps each run at $0.30. Use a read-only analytics key, so that even the connection could not write.
© 2026 EphemeralProduced by erni.works
esc

Nothing matches “”.