# Morning Brief (`intimate_hourglass/morning-brief-prefetcher`) Actor

Fetch your daily context from Slack, Gmail, Google Calendar, iCal, and Notion — then generate a formatted morning brief via Claude API and deliver it to Slack. Supports up to 3 accounts per integration. Schedule it before you start your day for a fully automated brief in Slack. No code required.

- **URL**: https://apify.com/intimate\_hourglass/morning-brief-prefetcher.md
- **Developed by:** [Ondra Mrstny](https://apify.com/intimate_hourglass) (community)
- **Categories:** AI, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 data fetcheds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Morning Brief

**Your morning brief, waiting when you open Slack.**

Fetch your daily context from Slack, Gmail, Google Calendar, iCal, and Notion — generate a formatted brief via Claude API — deliver it to Slack. Schedule it once and forget it.

> Typical run: all integrations fetched, brief generated, and delivered to Slack in under 30 seconds.

---

### What you get

Every morning, a brief that looks like this lands in your Slack DM before you start work:

````

⚡ *AT A GLANCE*
Heavy meeting day — 4 syncs before noon. One customer escalation needs a response before the 11:00 standup.

📆 *TODAY — Wednesday*

• *09:00* — Adoption weekly sync (Google Meet)

• *11:00* — Standup

• *14:00* — 1:1 with Lukas

• *16:30* — Quarterly review prep

📆 *TOMORROW — Thursday*

• *10:00* — Customer onboarding call — Acme Corp

🔴 *NEEDS ACTION*

• *Customer escalation: Acme Corp* — Lena escalated an issue in #customer-success. Needs acknowledgment before 11:00.

• *DEGIRO tax document* — Arrived by email yesterday, download before April 30 deadline.

📢 *FYI*

• *Adoption experiment — A/B test* — Results posted in #adoption-team, 12% lift on aha moment for guided-tour variant.

📧 *INBOX*

• *Lena Novak* — Re: Acme Corp onboarding issue (waiting on response)

• *DEGIRO* — Your tax document for 2025 is ready

✅ *ACTIVE TODOS — top 5*

• Respond to Acme Corp escalation before standup

• Review A/B test results from adoption experiment

• Download DEGIRO tax document

````

---

### What it does

Each run:

1. Fetches recent Slack messages from all channels and DMs you are a member of, plus mentions
2. Fetches unread Gmail messages from the last N hours
3. Fetches Google Calendar events for today and tomorrow
4. Fetches iCal feeds — any calendar with a secret URL, no OAuth needed
5. Fetches Notion database entries and specific pages you configure
6. Stores a raw JSON snapshot in a named key-value store (`morning-brief-latest`)
7. **(Optional)** Calls the Claude API to generate a formatted brief from the snapshot
8. **(Optional)** Posts the formatted brief to a Slack channel or DM

Steps 7 and 8 require an Anthropic API key. Without it, only the raw snapshot is saved — you can still read it via the Apify MCP server and ask Claude to generate the brief on demand.

---

### Quick start

1. Add credentials for the integrations you use (Slack, Gmail/Calendar, iCal, Notion)
2. Add your Anthropic API key and target Slack channel in **Brief generation**
3. Run the Actor once to verify — your brief should appear in Slack within 30 seconds
4. Set a schedule: in Apify Console → your Actor → **Schedules** → `0 7 * * *` for 7am daily

That's it. The brief runs automatically every morning from then on.

---

### Schedule it

In Apify Console, open your Actor → **Schedules** → **New schedule**.

| Goal | Cron expression | Description |
|---|---|---|
| Daily at 7am | `0 7 * * *` | Standard morning brief |
| Weekdays only at 7:30am | `30 7 * * 1-5` | Skip weekends |
| Catch-up on Mondays at 8am | `0 8 * * 1` | With `hours_lookback: 72` |

Set your local timezone in the schedule settings so the brief arrives before you start work.

---

### Input configuration

#### General settings

**`hours_lookback`** — How many hours back to fetch Slack messages and Gmail. Default: `24`. Use `48` or `72` to catch up after a weekend.

---

#### Slack

Supports up to 3 separate Slack workspaces (e.g. personal and work).

**Token types:**

| Type | Prefix | Reads channels + DMs | Reads mentions |
|---|---|---|---|
| Bot token | `xoxb-` | Yes | No |
| User token | `xoxp-` | Yes | Yes |

User tokens are recommended — they are the only way to get @mention results via Slack's search API.

**How to get a Slack token:**

1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → From scratch
2. Choose the workspace you want to read from
3. Go to **OAuth & Permissions** and add the following scopes under **Bot Token Scopes** (or **User Token Scopes** for a user token):
   - `channels:history`, `channels:read` — public channels
   - `groups:history`, `groups:read` — private channels
   - `im:history`, `im:read` — direct messages
   - `mpim:history`, `mpim:read` — group DMs
   - `search:read` — mention search (user tokens only)
4. Click **Install to Workspace** and copy the token from the confirmation screen

Fill in **Account label** (e.g. `personal`, `work`) and **Bot or user token** for each workspace. Accounts 2 and 3 are optional.

---

#### Gmail and Google Calendar

Both services use the same OAuth credentials. One account entry covers Gmail and Calendar together.

**How to get Google OAuth credentials:**

1. Go to [console.cloud.google.com](https://console.cloud.google.com/) and create a new project (or use an existing one)
2. Enable two APIs: **Gmail API** and **Google Calendar API** (APIs & Services → Library)
3. Go to **APIs & Services → Credentials → Create Credentials → OAuth client ID**
4. Set Application type to **Desktop app** — give it any name
5. Note down your **Client ID** and **Client Secret**
6. Run the included setup script to get a refresh token:

```bash
pip install google-auth-oauthlib
python3 google_auth_setup.py
````

A browser window opens for authorization. The script prints your refresh token when complete. Copy it into the **Refresh token** field.

**Per-account settings:**

| Field | Description |
|---|---|
| **Account label** | Name for this account, e.g. `work` or `personal` |
| **Client ID** | Ends in `.apps.googleusercontent.com` |
| **Client secret** | From the same OAuth client |
| **Refresh token** | Generated by `google_auth_setup.py` |
| **Calendar IDs** | One per line. `primary` = your main calendar. Find others in Google Calendar → Settings → specific calendar → Calendar ID |
| **Fetch Gmail** | Enable/disable Gmail for this account |
| **Fetch Calendar** | Enable/disable Calendar for this account |

Supports up to 3 Google accounts. A common setup: Account 1 = work (Gmail + Calendar), Account 2 = personal (Calendar only).

**Note:** Google refresh tokens expire if unused for 6 months or if you revoke access. Re-run `google_auth_setup.py` to generate a new one.

***

#### iCal calendar feeds

No OAuth required. Works with any calendar app that provides a secret iCal URL. The simplest way to add a Google Workspace calendar when the account blocks third-party OAuth consent.

**How to get your iCal URL from Google Calendar:**

1. Open Google Calendar in a browser
2. Click the **⋮** menu next to the calendar you want → **Settings**
3. Scroll to **Integrate calendar → Secret address in iCal format**
4. Copy the URL (it looks like `https://calendar.google.com/calendar/ical/.../private-xxxx/basic.ics`)

The URL acts as a secret token. Anyone with it can read your calendar — treat it like a password and keep it out of shared config files.

**Per-account settings:**

| Field | Description |
|---|---|
| **Account label** | Name for this feed, e.g. `work` or `personal` |
| **iCal URLs** | One URL per line. Each URL fetches one calendar. All events are merged into a single list for this account. |

Supports up to 3 iCal accounts.

***

#### Notion

Fetches database entries and full page content from any Notion workspace you connect.

**How to get a Notion integration token:**

1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations) → **New integration**
2. Give it a name (e.g. "Morning Brief"), associate it with your workspace
3. Set access to **Read content** only
4. Copy the **Internal Integration Secret** (starts with `secret_`)

**Connect each database or page you want to include:**

1. Open the database or page in Notion
2. Click **...** in the top-right → **Add connections** → select your integration
3. Copy the database ID or page URL

**Finding a database ID:**
Open the database. The URL looks like `notion.so/Page-Title-abc123def456789012345678901234ab`. The last 32 characters (before `?v=`) are the database ID.

**Per-account settings:**

| Field | Description |
|---|---|
| **Account label** | Name for this workspace, e.g. `main` |
| **Integration token** | Starts with `secret_` |
| **Database IDs** | One per line — the 32-character ID from the database URL |
| **Page URLs** | Full Notion page URLs, one per line — the ID is extracted automatically |

Supports up to 3 Notion accounts.

***

#### Brief generation

Requires an Anthropic API key from [console.anthropic.com](https://console.anthropic.com). The brief is generated using the data snapshot from the same run — no second fetch needed.

| Field | Default | Description |
|---|---|---|
| **Anthropic API key** | — | Starts with `sk-ant-`. Required for brief generation. Without it, only the raw snapshot is saved. |
| **Claude model** | `claude-haiku-4-5-20251001` | Model used for generation. Haiku is fast and cheap. Use `claude-sonnet-4-6` for higher quality. |
| **Slack channel or DM** | — | Channel ID (`C...`) or DM ID (`D...`) to post the brief. Right-click a channel or DM in Slack → **View channel details** → copy the ID at the bottom. Leave empty to skip delivery. |
| **Slack token for delivery** | — | Token used to post the brief. Defaults to Slack Account 1 if empty. Requires `chat:write` scope. |
| **Custom brief instructions** | — | Plain-text instructions appended to the system prompt. Adjust tone, add skip rules, focus specific sections, or give context about your role. |

**Custom instructions examples:**

```
Skip any Slack messages about food, lunch orders, or office plants.
I am based in Prague (CET) — convert all times to CET.
If there are no urgent emails, write "(nothing actionable)" for the INBOX section.
Flag any message mentioning "churn" or "cancellation" as NEEDS ACTION.
```

***

### Brief format

The generated brief uses Slack mrkdwn with these fixed sections. Custom instructions can adjust content and tone but not the section structure.

| Section | What it contains |
|---|---|
| ⚡ AT A GLANCE | 1-2 sentences. What kind of day is it? Any urgent watch-outs? |
| 📆 TODAY | All meetings with other people: `• HH:MM — Event name (location)` |
| 📆 TOMORROW | Same format, next day |
| 🔴 NEEDS ACTION | Items requiring a response or decision today — from Slack, email, or Notion |
| 📢 FYI | Worth knowing, no action needed |
| 📧 INBOX | Real, actionable emails only: `• Sender — Subject (context)` |
| ✅ ACTIVE TODOS | Top 5 priorities for today, sourced from Notion Todos first, then Slack/email |

***

### Raw data snapshot

The full snapshot is always stored in `morning-brief-latest` → key `brief_data`, regardless of whether brief generation is enabled. Use this with the Apify MCP server to generate briefs on demand, or access it directly via API.

**Snapshot structure:**

```json
{
  "fetched_at": "2026-05-13T07:00:00Z",
  "period_hours": 24,
  "slack": {
    "work": {
      "channels": [
        {
          "channel": "adoption-team",
          "messages": [
            { "user": "Lena", "text": "...", "ts": "..." }
          ]
        }
      ],
      "dms": [...],
      "mentions": [...]
    }
  },
  "gmail": {
    "work": [
      { "from": "lena@company.com", "subject": "Re: Acme onboarding", "snippet": "..." }
    ]
  },
  "calendar": {
    "work": {
      "primary": [
        { "summary": "Standup", "start": "2026-05-13T11:00:00+02:00", "end": "2026-05-13T11:30:00+02:00" }
      ]
    }
  },
  "ical_calendar": {
    "work": [
      { "summary": "Customer call", "start": "2026-05-14T10:00:00+02:00", "end": "2026-05-14T11:00:00+02:00" }
    ]
  },
  "notion": {
    "main": {
      "databases": {
        "abc123...": {
          "title": "Adoption team board",
          "pages": [
            { "title": "Q2 experiment", "status": "In progress", "assignee": ["Ondrej"] }
          ]
        }
      },
      "pages": {
        "https://notion.so/...": {
          "title": "Todos",
          "content": "- Respond to Acme escalation\n- Review A/B test results"
        }
      }
    }
  }
}
```

***

### Using with the Apify MCP server

If you prefer to generate the brief on demand rather than automatically, skip the Anthropic API key. The raw snapshot is always saved.

With the [Apify MCP server](https://mcp.apify.com) connected to Claude or another AI:

- *"Give me my morning brief"* — Claude reads the snapshot and generates a formatted brief
- *"What meetings do I have today?"* — Calendar-only summary
- *"Any urgent Slack messages?"* — Slack-focused filter
- *"What did I miss while I was out?"* — Full briefing with `hours_lookback: 72`

The MCP server reads directly from `morning-brief-latest`. No additional configuration needed.

***

### Use via API

Run the Actor and retrieve the output programmatically using the Apify API or SDK.

**Python:**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("ondrej.mrstny/morning-brief-prefetcher").call(run_input={
    "hours_lookback": 24,
    "slack_1_label": "work",
    "slack_1_token": "xoxp-...",
    "anthropic_api_key": "sk-ant-...",
    "brief_slack_channel": "D...",
})

## Get the brief text
store = client.key_value_store(run["defaultKeyValueStoreId"])
brief = store.get_record("brief_text")["value"]
print(brief)
```

**JavaScript:**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('ondrej.mrstny/morning-brief-prefetcher').call({
  hours_lookback: 24,
  slack_1_label: 'work',
  slack_1_token: 'xoxp-...',
  anthropic_api_key: 'sk-ant-...',
  brief_slack_channel: 'D...',
});

const store = await client.keyValueStore(run.defaultKeyValueStoreId);
const brief = await store.getRecord('brief_text');
console.log(brief.value);
```

**cURL:**

```bash
## Start a run
curl -X POST \
  "https://api.apify.com/v2/acts/ondrej.mrstny~morning-brief-prefetcher/runs?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hours_lookback": 24,
    "slack_1_label": "work",
    "slack_1_token": "xoxp-...",
    "anthropic_api_key": "sk-ant-..."
  }'

## Get the brief text (replace STORE_ID from run response)
curl "https://api.apify.com/v2/key-value-stores/STORE_ID/records/brief_text?token=YOUR_APIFY_TOKEN"
```

***

### Cost

The Actor uses per-event billing. You are charged each time the data is successfully fetched and stored — the brief generation via Claude API is billed separately by Anthropic on your own API key.

There is no charge for failed runs or runs with no accounts configured.

***

### Data security

**Your credentials never leave your Apify account.**

- All tokens and secrets are stored in your Actor input and encrypted at rest using per-user RSA keys
- Fetched data is stored in your private Apify key-value store — only accessible to you and systems you authorize
- The Actor runs in an isolated container with no network access beyond the APIs it explicitly calls
- All integrations use read-only scopes — the Actor cannot send messages, modify emails, create calendar events, or edit Notion pages
- Your brief data is not transmitted to the Actor author or any third party
- Apify is SOC 2 Type II certified

**Recommended practices:**

- Use a dedicated Google Cloud project for this Actor's OAuth credentials
- Create a Notion integration with read-only access
- Prefer a Slack bot token over a user token for any workspace where you do not need mention search
- Rotate tokens periodically via each provider's settings

***

### Limitations

- Slack channel history is limited to channels the bot/user is a member of
- Gmail fetches metadata and snippet only — full email body is not included to keep output size manageable
- Notion page content fetches one level deep — nested sub-pages are not recursed
- Notion database rows fetch visible properties only — formula results and rollups may be absent depending on API access
- Google refresh tokens expire after 6 months of non-use — re-run `google_auth_setup.py` to generate a fresh token
- iCal recurring events are expanded from the `.ics` file at fetch time — RSVP status is not available
- Brief generation quality depends on the selected Claude model — Haiku is fast but may miss nuance; Sonnet gives higher-quality output

***

### FAQ

**Does this work with multiple Slack workspaces?**
Yes. Configure up to 3 separate Slack accounts with different tokens. Each account fetches its own channels, DMs, and mentions independently.

**Can I have Gmail from one account and Calendar from another?**
Yes. Each Google account entry has separate toggles for Gmail and Calendar. Configure Account 1 for work Gmail + Calendar, Account 2 for personal Calendar only.

**Why use iCal instead of Google OAuth for Calendar?**
Google Workspace managed accounts sometimes block OAuth consent for third-party apps. The iCal URL bypasses OAuth entirely — it is a direct subscription link. No app approval needed.

**The brief is generating incorrect todos. How do I fix it?**
Add a custom brief instruction like: `"Todos must come only from the Notion Todos page I share. Do not infer todos from Slack messages."` The Todos section defaults to starting from Notion and backfilling from Slack/email if the Notion page is sparse.

**Can I use this without the Anthropic API key?**
Yes. Without a key, the Actor fetches all data and stores the raw snapshot. You can then read it via the Apify MCP server and ask Claude to generate the brief on demand in your chat.

**What Slack permissions does the delivery token need?**
The token used to post the brief needs `chat:write` scope. If using a bot token, the bot must also be added to the target channel. For a DM, this happens automatically.

**How fresh is the data?**
The snapshot is generated at run time — there is no caching. If the Actor runs at 7am, all data is from the 24 hours prior to 7am.

**Can I run this more than once a day?**
Yes. Each run generates a fresh snapshot and overwrites the `morning-brief-latest` key-value store. Multiple schedules work — e.g. a 7am daily brief and a 4pm end-of-day brief with `hours_lookback: 9`.

**What if one integration fails?**
The Actor continues fetching from all other integrations. Failed integrations are logged with the error message and stored in the output as `{ "error": "..." }`. The brief is still generated from whatever data was successfully retrieved.

***

### Integrations

| Platform | How to use |
|---|---|
| **Slack** | Set `brief_slack_channel` and a token with `chat:write`. The brief is posted as a formatted Slack mrkdwn message. |
| **Apify MCP server** | Connect Claude or another AI to the MCP server. The brief data is accessible as `morning-brief-latest`. |
| **Zapier / Make** | Trigger on Actor run completion. Read `brief_text` from the key-value store and forward to email, Notion, or another service. |
| **Apify API** | Full programmatic access — run via REST API or the Python/JavaScript SDK. See **Use via API** above. |
| **Claude Code** | A local Morning Brief skill (`morning-brief.md`) is available for generating the brief directly in Claude Code by saying "good morning". |

***

### Troubleshooting

**No brief in Slack after running.**
Check that `brief_slack_channel` is set to a channel or DM ID (not the channel name). Verify the token has `chat:write` scope. For channels, confirm the bot is added to the channel.

**Slack fetches zero messages.**
Confirm the bot/user token is active and has not been revoked. Bot tokens require the app to be installed in the workspace and the bot to be added to each private channel. Use a user token for broader access.

**Google Calendar shows no events.**
Verify the calendar ID is correct — use `primary` for your main calendar. Check that the **Fetch Calendar** toggle is enabled for the account. If the OAuth credentials are expired, re-run `google_auth_setup.py`.

**Notion database returns empty results.**
Confirm the integration has been connected to the database via **... → Add connections**. Check that the database ID is the 32-character string before `?v=`, not the full URL.

**Brief quality is low — wrong section content.**
Switch from Haiku to `claude-sonnet-4-6` in the **Claude model** field. Add custom instructions to give the model more context about your role and what to prioritize.

***

### Responsible use

- This Actor reads from your own accounts using credentials you provide — use it only on accounts you own or have permission to access
- Fetched data may contain personal information — store and transmit it accordingly
- Anthropic API usage is subject to [Anthropic's usage policies](https://www.anthropic.com/legal/usage-policy)
- Do not use this Actor to aggregate or monitor other people's communications without their consent

# Actor input Schema

## `hours_lookback` (type: `integer`):

How many hours back to fetch messages and emails. Default is 24 (daily brief). Use 48 to catch up after a weekend.

## `slack_1_label` (type: `string`):

A name for this account, used to identify it in the output (e.g. personal, work).

## `slack_1_token` (type: `string`):

Starts with xoxb- (bot) or xoxp- (user). Only user tokens support mention search.

## `slack_2_label` (type: `string`):

A name for this account (e.g. work).

## `slack_2_token` (type: `string`):

Starts with xoxb- (bot) or xoxp- (user).

## `slack_3_label` (type: `string`):

A name for this account.

## `slack_3_token` (type: `string`):

Starts with xoxb- (bot) or xoxp- (user).

## `google_1_label` (type: `string`):

A name for this account (e.g. work, personal).

## `google_1_client_id` (type: `string`):

From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com

## `google_1_client_secret` (type: `string`):

From the same OAuth client in Google Cloud Console.

## `google_1_refresh_token` (type: `string`):

Generated by running google\_auth\_setup.py and authorizing in the browser.

## `google_1_calendar_ids` (type: `string`):

Which calendars to fetch events from. Use 'primary' for your main calendar. Find other IDs in Google Calendar → Settings → specific calendar → Calendar ID.

## `google_1_fetch_gmail` (type: `boolean`):

Fetch unread emails from the last N hours.

## `google_1_fetch_calendar` (type: `boolean`):

Fetch events for today and tomorrow.

## `google_2_label` (type: `string`):

A name for this account (e.g. personal).

## `google_2_client_id` (type: `string`):

From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com

## `google_2_client_secret` (type: `string`):

From the same OAuth client in Google Cloud Console.

## `google_2_refresh_token` (type: `string`):

Generated by running google\_auth\_setup.py and authorizing in the browser.

## `google_2_calendar_ids` (type: `string`):

Which calendars to fetch events from. Use 'primary' for your main calendar.

## `google_2_fetch_gmail` (type: `boolean`):

Fetch unread emails from the last N hours.

## `google_2_fetch_calendar` (type: `boolean`):

Fetch events for today and tomorrow.

## `google_3_label` (type: `string`):

A name for this account.

## `google_3_client_id` (type: `string`):

From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com

## `google_3_client_secret` (type: `string`):

From the same OAuth client in Google Cloud Console.

## `google_3_refresh_token` (type: `string`):

Generated by running google\_auth\_setup.py and authorizing in the browser.

## `google_3_calendar_ids` (type: `string`):

Which calendars to fetch events from. Use 'primary' for your main calendar.

## `google_3_fetch_gmail` (type: `boolean`):

Fetch unread emails from the last N hours.

## `google_3_fetch_calendar` (type: `boolean`):

Fetch events for today and tomorrow.

## `ical_1_label` (type: `string`):

A name for this calendar feed (e.g. work, personal).

## `ical_1_urls` (type: `string`):

One or more iCal feed URLs. Each URL fetches one calendar. All events are merged into a single list for this account.

## `ical_2_label` (type: `string`):

A name for this calendar feed.

## `ical_2_urls` (type: `string`):

One or more iCal feed URLs.

## `ical_3_label` (type: `string`):

A name for this calendar feed.

## `ical_3_urls` (type: `string`):

One or more iCal feed URLs.

## `notion_1_label` (type: `string`):

A name for this Notion workspace (e.g. main, work).

## `notion_1_token` (type: `string`):

Starts with secret\_. Found in your integration settings at notion.so/my-integrations.

## `notion_1_database_ids` (type: `string`):

The 32-character ID at the end of a database URL. Open the database in Notion and copy the ID from the browser address bar.

## `notion_1_page_urls` (type: `string`):

Full Notion page URLs. The Actor extracts the page ID automatically. Share each page with your integration first.

## `notion_2_label` (type: `string`):

A name for this Notion workspace.

## `notion_2_token` (type: `string`):

Starts with secret\_. Found in your integration settings at notion.so/my-integrations.

## `notion_2_database_ids` (type: `string`):

The 32-character ID at the end of a database URL.

## `notion_2_page_urls` (type: `string`):

Full Notion page URLs. The Actor extracts the page ID automatically.

## `notion_3_label` (type: `string`):

A name for this Notion workspace.

## `notion_3_token` (type: `string`):

Starts with secret\_. Found in your integration settings at notion.so/my-integrations.

## `notion_3_database_ids` (type: `string`):

The 32-character ID at the end of a database URL.

## `notion_3_page_urls` (type: `string`):

Full Notion page URLs. The Actor extracts the page ID automatically.

## `anthropic_api_key` (type: `string`):

Your Anthropic API key (starts with sk-ant-). Required to generate the formatted morning brief. Without this, only raw data is stored.

## `brief_model` (type: `string`):

Claude model used for brief generation. Default: claude-haiku-4-5-20251001 (fast and cheap). Use claude-sonnet-4-6 for higher quality.

## `brief_slack_channel` (type: `string`):

Channel ID or DM ID to post the formatted brief to. Find the ID by right-clicking a channel or DM in Slack → View channel details → copy the ID at the bottom (starts with C for channels, D for DMs). Leave empty to skip delivery.

## `brief_slack_token` (type: `string`):

Slack bot or user token to use for posting the brief. Defaults to Slack Account 1 token if left empty.

## `brief_instructions` (type: `string`):

Extra instructions appended to the system prompt. Use this to adjust tone, add skip rules, change section focus, or give context about your role. Plain text, no special format needed.

## Actor input object example

```json
{
  "hours_lookback": 24,
  "slack_1_label": "personal",
  "google_1_label": "work",
  "google_1_calendar_ids": "primary",
  "google_1_fetch_gmail": true,
  "google_1_fetch_calendar": true,
  "google_2_calendar_ids": "primary",
  "google_2_fetch_gmail": true,
  "google_2_fetch_calendar": true,
  "google_3_calendar_ids": "primary",
  "google_3_fetch_gmail": true,
  "google_3_fetch_calendar": true,
  "ical_1_label": "work",
  "notion_1_label": "main",
  "brief_model": "claude-haiku-4-5-20251001"
}
```

# Actor output Schema

## `dataset` (type: `string`):

Full JSON snapshot stored as a dataset item. Contains the same data as the key-value store output. Useful for browsing run history.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("intimate_hourglass/morning-brief-prefetcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("intimate_hourglass/morning-brief-prefetcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call intimate_hourglass/morning-brief-prefetcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=intimate_hourglass/morning-brief-prefetcher",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Morning Brief",
        "description": "Fetch your daily context from Slack, Gmail, Google Calendar, iCal, and Notion — then generate a formatted morning brief via Claude API and deliver it to Slack. Supports up to 3 accounts per integration. Schedule it before you start your day for a fully automated brief in Slack. No code required.",
        "version": "0.1",
        "x-build-id": "3uhfchkgfcKW4n12N"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/intimate_hourglass~morning-brief-prefetcher/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-intimate_hourglass-morning-brief-prefetcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/intimate_hourglass~morning-brief-prefetcher/runs": {
            "post": {
                "operationId": "runs-sync-intimate_hourglass-morning-brief-prefetcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/intimate_hourglass~morning-brief-prefetcher/run-sync": {
            "post": {
                "operationId": "run-sync-intimate_hourglass-morning-brief-prefetcher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "hours_lookback": {
                        "title": "Hours lookback",
                        "minimum": 1,
                        "maximum": 168,
                        "type": "integer",
                        "description": "How many hours back to fetch messages and emails. Default is 24 (daily brief). Use 48 to catch up after a weekend.",
                        "default": 24
                    },
                    "slack_1_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account, used to identify it in the output (e.g. personal, work).",
                        "default": "personal"
                    },
                    "slack_1_token": {
                        "title": "Bot or user token",
                        "type": "string",
                        "description": "Starts with xoxb- (bot) or xoxp- (user). Only user tokens support mention search."
                    },
                    "slack_2_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account (e.g. work)."
                    },
                    "slack_2_token": {
                        "title": "Bot or user token",
                        "type": "string",
                        "description": "Starts with xoxb- (bot) or xoxp- (user)."
                    },
                    "slack_3_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account."
                    },
                    "slack_3_token": {
                        "title": "Bot or user token",
                        "type": "string",
                        "description": "Starts with xoxb- (bot) or xoxp- (user)."
                    },
                    "google_1_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account (e.g. work, personal).",
                        "default": "work"
                    },
                    "google_1_client_id": {
                        "title": "Client ID",
                        "type": "string",
                        "description": "From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com"
                    },
                    "google_1_client_secret": {
                        "title": "Client secret",
                        "type": "string",
                        "description": "From the same OAuth client in Google Cloud Console."
                    },
                    "google_1_refresh_token": {
                        "title": "Refresh token",
                        "type": "string",
                        "description": "Generated by running google_auth_setup.py and authorizing in the browser."
                    },
                    "google_1_calendar_ids": {
                        "title": "Calendar IDs (one per line)",
                        "type": "string",
                        "description": "Which calendars to fetch events from. Use 'primary' for your main calendar. Find other IDs in Google Calendar → Settings → specific calendar → Calendar ID.",
                        "default": "primary"
                    },
                    "google_1_fetch_gmail": {
                        "title": "Fetch Gmail",
                        "type": "boolean",
                        "description": "Fetch unread emails from the last N hours.",
                        "default": true
                    },
                    "google_1_fetch_calendar": {
                        "title": "Fetch Calendar",
                        "type": "boolean",
                        "description": "Fetch events for today and tomorrow.",
                        "default": true
                    },
                    "google_2_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account (e.g. personal)."
                    },
                    "google_2_client_id": {
                        "title": "Client ID",
                        "type": "string",
                        "description": "From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com"
                    },
                    "google_2_client_secret": {
                        "title": "Client secret",
                        "type": "string",
                        "description": "From the same OAuth client in Google Cloud Console."
                    },
                    "google_2_refresh_token": {
                        "title": "Refresh token",
                        "type": "string",
                        "description": "Generated by running google_auth_setup.py and authorizing in the browser."
                    },
                    "google_2_calendar_ids": {
                        "title": "Calendar IDs (one per line)",
                        "type": "string",
                        "description": "Which calendars to fetch events from. Use 'primary' for your main calendar.",
                        "default": "primary"
                    },
                    "google_2_fetch_gmail": {
                        "title": "Fetch Gmail",
                        "type": "boolean",
                        "description": "Fetch unread emails from the last N hours.",
                        "default": true
                    },
                    "google_2_fetch_calendar": {
                        "title": "Fetch Calendar",
                        "type": "boolean",
                        "description": "Fetch events for today and tomorrow.",
                        "default": true
                    },
                    "google_3_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this account."
                    },
                    "google_3_client_id": {
                        "title": "Client ID",
                        "type": "string",
                        "description": "From Google Cloud Console → APIs & Services → Credentials. Ends in .apps.googleusercontent.com"
                    },
                    "google_3_client_secret": {
                        "title": "Client secret",
                        "type": "string",
                        "description": "From the same OAuth client in Google Cloud Console."
                    },
                    "google_3_refresh_token": {
                        "title": "Refresh token",
                        "type": "string",
                        "description": "Generated by running google_auth_setup.py and authorizing in the browser."
                    },
                    "google_3_calendar_ids": {
                        "title": "Calendar IDs (one per line)",
                        "type": "string",
                        "description": "Which calendars to fetch events from. Use 'primary' for your main calendar.",
                        "default": "primary"
                    },
                    "google_3_fetch_gmail": {
                        "title": "Fetch Gmail",
                        "type": "boolean",
                        "description": "Fetch unread emails from the last N hours.",
                        "default": true
                    },
                    "google_3_fetch_calendar": {
                        "title": "Fetch Calendar",
                        "type": "boolean",
                        "description": "Fetch events for today and tomorrow.",
                        "default": true
                    },
                    "ical_1_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this calendar feed (e.g. work, personal).",
                        "default": "work"
                    },
                    "ical_1_urls": {
                        "title": "iCal URLs (one per line)",
                        "type": "string",
                        "description": "One or more iCal feed URLs. Each URL fetches one calendar. All events are merged into a single list for this account."
                    },
                    "ical_2_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this calendar feed."
                    },
                    "ical_2_urls": {
                        "title": "iCal URLs (one per line)",
                        "type": "string",
                        "description": "One or more iCal feed URLs."
                    },
                    "ical_3_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this calendar feed."
                    },
                    "ical_3_urls": {
                        "title": "iCal URLs (one per line)",
                        "type": "string",
                        "description": "One or more iCal feed URLs."
                    },
                    "notion_1_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this Notion workspace (e.g. main, work).",
                        "default": "main"
                    },
                    "notion_1_token": {
                        "title": "Integration token",
                        "type": "string",
                        "description": "Starts with secret_. Found in your integration settings at notion.so/my-integrations."
                    },
                    "notion_1_database_ids": {
                        "title": "Database IDs (one per line)",
                        "type": "string",
                        "description": "The 32-character ID at the end of a database URL. Open the database in Notion and copy the ID from the browser address bar."
                    },
                    "notion_1_page_urls": {
                        "title": "Page URLs (one per line)",
                        "type": "string",
                        "description": "Full Notion page URLs. The Actor extracts the page ID automatically. Share each page with your integration first."
                    },
                    "notion_2_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this Notion workspace."
                    },
                    "notion_2_token": {
                        "title": "Integration token",
                        "type": "string",
                        "description": "Starts with secret_. Found in your integration settings at notion.so/my-integrations."
                    },
                    "notion_2_database_ids": {
                        "title": "Database IDs (one per line)",
                        "type": "string",
                        "description": "The 32-character ID at the end of a database URL."
                    },
                    "notion_2_page_urls": {
                        "title": "Page URLs (one per line)",
                        "type": "string",
                        "description": "Full Notion page URLs. The Actor extracts the page ID automatically."
                    },
                    "notion_3_label": {
                        "title": "Account label",
                        "type": "string",
                        "description": "A name for this Notion workspace."
                    },
                    "notion_3_token": {
                        "title": "Integration token",
                        "type": "string",
                        "description": "Starts with secret_. Found in your integration settings at notion.so/my-integrations."
                    },
                    "notion_3_database_ids": {
                        "title": "Database IDs (one per line)",
                        "type": "string",
                        "description": "The 32-character ID at the end of a database URL."
                    },
                    "notion_3_page_urls": {
                        "title": "Page URLs (one per line)",
                        "type": "string",
                        "description": "Full Notion page URLs. The Actor extracts the page ID automatically."
                    },
                    "anthropic_api_key": {
                        "title": "Anthropic API key",
                        "type": "string",
                        "description": "Your Anthropic API key (starts with sk-ant-). Required to generate the formatted morning brief. Without this, only raw data is stored."
                    },
                    "brief_model": {
                        "title": "Claude model",
                        "type": "string",
                        "description": "Claude model used for brief generation. Default: claude-haiku-4-5-20251001 (fast and cheap). Use claude-sonnet-4-6 for higher quality.",
                        "default": "claude-haiku-4-5-20251001"
                    },
                    "brief_slack_channel": {
                        "title": "Slack channel or DM to deliver the brief",
                        "type": "string",
                        "description": "Channel ID or DM ID to post the formatted brief to. Find the ID by right-clicking a channel or DM in Slack → View channel details → copy the ID at the bottom (starts with C for channels, D for DMs). Leave empty to skip delivery."
                    },
                    "brief_slack_token": {
                        "title": "Slack token for delivery (optional)",
                        "type": "string",
                        "description": "Slack bot or user token to use for posting the brief. Defaults to Slack Account 1 token if left empty."
                    },
                    "brief_instructions": {
                        "title": "Custom brief instructions (optional)",
                        "type": "string",
                        "description": "Extra instructions appended to the system prompt. Use this to adjust tone, add skip rules, change section focus, or give context about your role. Plain text, no special format needed."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
