Tutorial: Email Digest Agent
Build an agent that summarizes your unread Gmail and posts the digest to Slack every weekday morning.
Goal
You will build an agent that:
- Reads your unread Gmail from the last 24 hours.
- Summarizes each email and ranks them by priority.
- Posts the digest to a Slack channel.
- Returns a structured result.
- Runs by itself at 8am on weekdays, and tells your own system if it fails.
It uses two integrations (Gmail and Slack), a prompt, an input, an output schema, a schedule, and a webhook. Credentials never reach the agent: it calls both services through the run's sidecar, which injects the OAuth token. See Architecture.
Prerequisites
- A running Appstrate instance and an account in it (Get Started quickstart).
- A model the agent can run on. In Organization settings, open Models, add one with Add model, and make it the default with Set as default.
- A Google account for Gmail and a Slack workspace where you may add an app.
- The Gmail (API) and Slack (API) integrations active in your space, with an OAuth client for each. On a deployment that ships shared OAuth apps, such as Appstrate Cloud, there is nothing to configure. On a self-hosted instance, an administrator registers an OAuth client per integration, as in step 2.
Steps
Check the integrations
Open Integrations in the sidebar. It lists the integrations active in your space. Look for Gmail (API) and Slack (API). If one is missing or marked inactive, a space administrator has to activate it (the Activate button, or the package library). Each integration has its own page, with tabs such as Connections, Configuration, and Tools.
Register OAuth clients (self-hosted only)
An OAuth integration needs a client registered with the vendor. If the integration page says No OAuth client configured, an administrator opens it and chooses Register OAuth client. The dialog shows the Redirect URI to register at the provider: copy it into your Google Cloud OAuth client, or your Slack app, then paste the Client ID and Client Secret back in. The integration page also carries a setup guide that links to the vendor's console. One client per integration serves every user of the space, or of the whole organization once it is promoted.
Create the agent
Click New agent in the Agents list. In the editor, fill the tabs below. The JSON tab shows the manifest these choices produce, if you want to check your work.
General: set the Identifier (slug) to email-daily-digest, the Display name to Email daily digest, and a short Description. The package scope is your organization's, set automatically, so the agent's id becomes @your-org/email-daily-digest.
Files: write the agent's prompt in prompt.md:
You summarize unread emails and post a digest to Slack.
1. With the Gmail integration's api_call tool, list unread messages from the
last 24 hours:
GET https://gmail.googleapis.com/gmail/v1/users/me/messages?q=is:unread%20newer_than:1d&maxResults=25
2. For each message id, read its headers and snippet:
GET https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}?format=metadata&metadataHeaders=From&metadataHeaders=Subject
3. Summarize each email in 2 or 3 lines: sender, subject, key points, and any
action required. Sort by priority, urgent first.
4. With the Slack integration's api_call tool, post the digest to the channel
named by the `channel` input:
POST https://slack.com/api/chat.postMessage
with the header Content-Type: application/json and a JSON body
{"channel": "<channel>", "text": "<the digest>"}.
Use Slack mrkdwn: _bold_ for the title and priorities, a bullet per email.
5. Return the result with the output tool.
If there are no unread emails, post "No unread emails in the last 24h" and
return emailCount 0.Schemas: under Input, click + Add field. Set key to channel, default to daily-digest, and a description such as Slack channel to post in. The output schema comes in step 6.
Integrations: at the top of this tab, the Runtime tools group lists the platform tools by id. Tick output (it is required for an output schema) and, if you like, log, which lets the agent report progress in real time. Below it, tick Gmail (API) and Slack (API). For each, tick Generic API call (api_call), which lets the agent call the vendor's HTTP API with the credential injected for it. For Slack, also open Advanced: pin extra scopes and pin Send messages (chat:write): the integration's default scopes only read channels and users. Gmail needs nothing extra, because its default scopes already include read access (its consent screen also lists permission to send mail, which this agent never uses).
Click Create to save the agent. If the editor reports that the initial version could not be published, fix what it names and publish from the agent page with Create version.
Connect your accounts
Open the agent. Its Connections tab lists the integrations it depends on. Click Connect next to Gmail, sign in with Google, and approve read access. Do the same for Slack and choose your workspace. In Slack, invite the app to the channel you will post in (/invite @your-app in the channel), or the post is refused.
You can also start a connection from the API. This returns a short-lived hosted link to open in a browser:
curl -X POST "http://localhost:3000/api/integrations/@appstrate/gmail/auths/primary/connect/session" \
-H "Authorization: Bearer $APPSTRATE_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# → { "connect_url": "https://…", "expiresAt": "…" }Repeat for @appstrate/slack, this time with { "scopes": ["chat:write"] } so that consent covers posting. Gmail needs no body, since its default scopes already cover reading. The connection belongs to whoever completed the consent, and a run uses the connections of the person (or end-user, or schedule owner) it runs as.
Run it
On the agent page, click Run. The parameters dialog shows the channel input. Confirm, and the run starts. Open it to follow along:
- Execution shows the logs live: each tool call, with its arguments and result.
- Outcome shows the structured output and any files.
The run executes in its own sandbox with its own sidecar (a container, or a microVM on the Firecracker backend; a plain host subprocess on the default process backend), and that sandbox is torn down when the run ends. If a connection is missing or lacks a scope, Run opens a dialog that names it and lets you fix it, then re-run.
From the API, replace @your-org with your organization's scope:
# Launch. Answers 201 with the run resource.
curl -X POST "http://localhost:3000/api/agents/@your-org/email-daily-digest/run" \
-H "Authorization: Bearer $APPSTRATE_KEY" \
-H "Content-Type: application/json" \
-d '{ "input": { "channel": "daily-digest" } }'
# Wait for it to finish (up to 55 seconds per call).
curl "http://localhost:3000/api/runs/$RUN_ID?wait=true" \
-H "Authorization: Bearer $APPSTRATE_KEY"
# Or stream it.
curl -N "http://localhost:3000/api/realtime/runs/$RUN_ID?token=$APPSTRATE_KEY"Add an output schema
An output schema makes the result structured and validated. Open the editor's JSON tab, add an output member at the top level of the manifest, and click Apply to form. Excerpt of the agent manifest:
"output": {
"schema": {
"type": "object",
"properties": {
"emailCount": { "type": "integer", "description": "Number of emails processed" },
"slackMessageSent": { "type": "boolean" },
"summaries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": { "type": "string" },
"subject": { "type": "string" },
"summary": { "type": "string" },
"priority": { "type": "string", "enum": ["urgent", "normal", "low"] }
},
"required": ["from", "subject", "summary", "priority"]
}
}
},
"required": ["emailCount", "slackMessageSent", "summaries"]
}
}The agent returns it through the output runtime tool. If the result does not match the schema, the run is marked failed and the validation errors are in the run's error. The value appears as result.output on the run and in the Outcome tab. Publish a new version after changing the agent.
Schedule it
On the agent page, open the actions menu and choose New schedule (an agent with no schedule also shows Add in its Schedules tab). The form preselects the first agent of the space, so pick Email daily digest in it. Set a Name (optional), the Cron expression 0 8 * * 1-5, and your Timezone. The page also offers presets, such as Mon-Fri 9am. The schedule shows its Next run.
From the API:
curl -X POST "http://localhost:3000/api/agents/@your-org/email-daily-digest/schedules" \
-H "Authorization: Bearer $APPSTRATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Email digest, 8am",
"cron_expression": "0 8 * * 1-5",
"timezone": "America/Toronto",
"input": { "channel": "daily-digest" }
}'A scheduled run executes as the person who created the schedule, with their connections. If that person's access ends, or one of the connections they used is deleted, the schedule is disabled with a reason you can read on its page, rather than failing silently. See Scheduling.
Get notified when it fails (optional)
A webhook calls your server when a run ends badly. Create one for the space the agent lives in. Find the space id with GET /api/spaces (it starts with spc_):
curl -X POST "http://localhost:3000/api/webhooks" \
-H "Authorization: Bearer $APPSTRATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"level": "space",
"spaceId": "spc_...",
"url": "https://your-app.com/webhooks/appstrate",
"events": ["run.failed", "run.timeout"],
"packageId": "@your-org/email-daily-digest",
"payloadMode": "summary"
}'Save the whsec_... secret from the response and verify signatures on your side. See Webhooks. The dashboard has a Webhooks page for the same thing.
Going further
- Memory. Enable the
noteandpinruntime tools so the agent can remember the last email it processed and skip duplicates. See Memory. - More inputs. Make the time window or the label to read configurable with more input fields.
- A skill. Attach a skill with your own rules for what counts as urgent.
- End-users. Run the same agent for each of your customers, with their own mailboxes, using the
Appstrate-Userheader. See End-user impersonation. - From your terminal.
appstrate run @your-org/email-daily-digestlaunches it from the CLI. See the CLI guide.