Connect Slack, GitHub, Linear, Notion and more
Your AI team is only as informed as the systems it can see. This guide is the wiring: what to create on the provider's side, what to paste into Ever Works, in which order, and how to prove the chain works end to end.
There are four different mechanisms behind the word "integration", and mixing them up is the most common reason a setup looks finished and does nothing:
- Connector plugins pull activity in on a cron and — for the ones that write back — push messages, comments and records out.
- Dedicated receivers accept signed webhook deliveries from Slack and GitHub, which do more than a poll can (chat replies, pull-request reviews).
- Notification channels are an outbound-only family for alerts, digests and agent pings, with their own settings screen and a test button.
- The event spine is the single pipeline underneath all of them: dedupe, Activity, Memory, Meetings.
Dashboard routes are written the way you type them, without the locale prefix — the address bar shows /en/plugins, this guide says /plugins. API examples use https://api.ever.works; on a self-hosted install, swap in your own API origin.
Pick the right surface first
| You want to… | Use | Set it up at |
|---|---|---|
| Ask the platform questions from Slack and get answers in the thread | The Slack app receivers | Slack app config + slack-connector settings — §1 |
| Have Slack channel messages land on your feed | The slack-connector event source | /plugins → Slack Connector — §1 |
| Get AI reviews on pull requests | The GitHub receiver | GitHub App, or a repo webhook — §2 |
| Link repositories and onboard an existing data repo | The GitHub App | /settings/github-app — §2 |
| Pull in issues, pages, deals, files, recordings | A connector plugin | /settings/plugins/connector — §3 |
| Push alerts, digests and agent pings out to chat | A notification channel | /settings/integrations/channels — §4 |
| Turn recorded calls into summaries your Agents remember | The Zoom or Google Workspace connector | /plugins — §6 |
/settings/integrations has no index page and soft-404s — the settings navigation deliberately omits a bare Integrations tab. Only its two children exist: Channels (/settings/integrations/channels) and Email Addresses (/settings/integrations/emails). Everything connector-shaped lives under Plugins: /plugins for the catalog, /settings/plugins/connector for the connector category. Bare /settings/plugins redirects to the AI Providers category, so link the category explicitly when you mean connectors.
1. Slack
The Slack app is the one integration wired all the way through: inbound messages reach the same platform chat engine the web app uses, and the answer is posted back into the thread it was asked in.
| Slack setting | Point it at | Rate limit |
|---|---|---|
| Event Subscriptions → Request URL | https://api.ever.works/api/ingest/slack/events | 300 deliveries / min |
| Slash Commands → Request URL | https://api.ever.works/api/ingest/slack/commands | 300 deliveries / min |
Both endpoints verify every delivery with your app's signing secret — HMAC v0 over v0:{timestamp}:{rawBody}, a ±300-second timestamp tolerance and a constant-time compare — and both fail closed. With no configured Slack install carrying a signing secret, everything is rejected with 401, including Slack's own url_verification handshake, which Slack signs like any other delivery. Save the secret in Ever Works before you ask Slack to verify the URL.
What the bot actually calls
Grant the Slack scopes these methods require, and invite the bot into every channel you want it to read or post in.
| Slack API method | What Ever Works uses it for |
|---|---|
auth.test | Validating the bot token, and caching the bot user id so the bot skips its own messages |
chat.postMessage | Outbound messages, mention replies and slash-command answers |
conversations.history | The connector's pullEvents sweep over the configured channels |
chat.getPermalink | The sourceUrl deep link on each ingested message |
Per Slack's own scope rules that is chat:write for posting, channels:history (or groups:history for private channels) for the history sweep, the app_mentions:read event scope for mentions, and commands for the slash command.
How to: connect a Slack workspace
-
Create a Slack app in the workspace you want to connect. Copy the Bot User OAuth token (
xoxb-…) from OAuth & Permissions and the Signing secret from Basic Information. -
In Ever Works, open Sidebar → Plugins (
/plugins), find Slack Connector, and press Enable. In the dialog, leave Also enable for all works ticked — account-level on does not cascade on its own — and press Enable again to commit. -
Open the plugin's settings page and fill it in, then press Save Settings. Save is blocked while the plugin is disabled, so enable first and paste second.
Field Required Value botTokenYes The xoxb-…token from step 1.signingSecretFor inbound The signing secret. Without it both receivers reject every delivery. appIdNo Your Slack app id. defaultChannelIdNo Where outbound messages go when a send names no channel, e.g. C0123456789.eventChannelIdsNo Comma-separated channel ids to ingest from. Defaults to the default channel only. -
Back in Slack, open Event Subscriptions, set the Request URL to
https://api.ever.works/api/ingest/slack/events, and let Slack verify it. Under Subscribe to bot events, add the events that deliverapp_mentionand channelmessagepayloads (app_mentionandmessage.channels; addmessage.groupsfor private channels). -
Open Slash Commands and create one —
/worksis the name the usage hint assumes, but any name works. Request URL:https://api.ever.works/api/ingest/slack/commands. -
Install (or reinstall) the app to the workspace so the new scopes and events take effect, then invite the bot into each channel:
/invite @YourApp. -
Test both entry points:
- Type
/works what shipped today?. You get a private ack — "On it — I am asking Ever Works now and will post the answer in this channel shortly." — and the answer arrives in the channel a moment later. - Post
@works summarise the last three PRsin a channel the bot is in. The reply lands in that thread.
- Type
What happens on each delivery
| Behaviour | Detail |
|---|---|
| Workspace attribution | The delivery's team_id (plus enterprise_id on Grid) selects which install's signing secret to verify against, so two customers' workspaces never cross-attribute. |
| Forged workspace ids | Harmless. A forged team_id only picks a secret, and that secret then fails the HMAC. |
| Unknown workspace | Refused, never guessed. Events answer 200 with { ok: true, ignored: … } so Slack stops retrying; a command answers with a private "this workspace is not connected yet". |
| Ack budget | Slack gives a slash command three seconds. The receiver verifies, resolves and acks inside that, then produces the answer detached and posts it into the channel. |
Bare /works | Answers with a usage hint — Usage: `/works <your question>` — instead of sending an empty prompt to the model. |
| Echo protection | Messages carrying a bot_id or a subtype are never routed back in, so the bot cannot answer itself. |
| What lands on your feed | slack.mention and slack.message envelopes, identified by channel:ts so webhook pushes and connector sweeps converge on one row, plus slack.command per invocation. |
Slack troubleshooting
| Symptom | What it means | Fix |
|---|---|---|
| Slack cannot verify the Request URL | The receiver is failing closed — no install with a signing secret exists. | Complete step 3 (enable + save signingSecret), then retry the verification. |
/works replies "This Slack workspace is not connected…" | The delivery verified against nothing this account owns. | Enable and configure slack-connector on the account that should own this workspace. |
| The mention is on the Activity feed but no reply appears in the thread | The chat leg is best-effort and never fails the webhook. | Check that botToken is still valid and that the bot is a member of the channel. |
| Channel messages never appear | eventChannelIds defaults to the default channel only. | List every channel id you want swept, comma-separated, and invite the bot into each one. |
2. GitHub
GitHub has two URLs and one receiver behind them. Whichever URL a delivery arrives at, the same code verifies it once and fans it out to every consumer: GitHub App installation sync, the event spine, and the AI pull-request reviewer.
POST /api/ingest/github/events | POST /api/github-app/webhooks | |
|---|---|---|
| Who points at it | A repository or organization webhook you configure yourself | The platform GitHub App — the URL is baked into the App's settings |
| Verified with | The per-user github plugin's Webhook secret, or the platform App secret | The same two credentials |
| What runs | Installation sync + event ingest + AI review | Identical |
| Unattributable delivery | 200 no-op, so GitHub stops retrying | 401 |
| Which leg can fail the request | The review leg — GitHub's retry is how a transient ingest failure recovers | The sync leg — retry is load-bearing for installation sync |
| Rate limit | 300 deliveries / min | — |
The two receivers used to be independent, which is why installing the GitHub App did not turn pull-request reviews on. They are now one receiver: an App installation drives installation sync and the ingest spine and the AI review, with no second webhook to configure.
A delivery with no x-github-event header is rejected outright, and bot-authored comments — including the reviewer's own replies — are never ingested, so the loop cannot echo itself.
How to: install the GitHub App
- Install the Ever Works GitHub App on the user account or organization, and choose the repositories it may see.
- Complete the setup redirect. GitHub hands off to
GET /api/github-app/setupand thenGET /api/github-app/callback, which bind the installation to your Ever Works account and sign you in. - Open Settings → GitHub App (
/settings/github-app). Each installation card shows its status (Active / Suspended), the account and target type, the repository count, the last sync and the app slug. With nothing linked yet the page says: "Install the Ever Works GitHub App on a repository or organization, then complete the setup redirect to have the installation linked to this workspace." - Press Sync on the installation to refresh the repository snapshot from GitHub. A fresh installation shows "This installation has no repositories stored yet. Run sync to refresh the snapshot from GitHub." until you do.
- Press Onboard next to a repository to register it as a Work.
The onboarding path analyses the repository first and only proceeds when it detects an existing data repo. Anything else comes back as "Only existing data repositories can be onboarded from GitHub App installations right now". To bring in a different kind of repository, use the Work import path instead.
The same three actions exist on the API:
curl https://api.ever.works/api/github-app/installations \
-H "Authorization: Bearer <jwt-token>"
curl -X POST https://api.ever.works/api/github-app/installations/<installationId>/sync \
-H "Authorization: Bearer <jwt-token>"
curl -X POST \
https://api.ever.works/api/github-app/installations/<installationId>/repositories/<repositoryId>/onboard \
-H "Authorization: Bearer <jwt-token>"
Self-hosting? You register your own GitHub App and wire it through environment variables:
| Variable | Purpose |
|---|---|
GITHUB_APP_ID | The App's numeric id. |
GITHUB_APP_CLIENT_ID | OAuth client id, for the user-authorization leg of the setup redirect. |
GITHUB_APP_CLIENT_SECRET | OAuth client secret. |
GITHUB_APP_PRIVATE_KEY | The App private key. Escaped newlines (\n) are unescaped on read. |
GITHUB_APP_WEBHOOK_SECRET | The App-level webhook secret the receiver accepts. |
GITHUB_APP_SLUG | The App's slug. Defaults to ever-works. |
GITHUB_APP_SETUP_URL | Overrides the default <web app URL>/api/github-app/setup. |
GITHUB_APP_CALLBACK_URL | Overrides the default <web app URL>/api/github-app/callback. |
How to: turn on review for a single repository, without the App
- Enable the GitHub plugin at
/pluginsand open its settings. - Set Webhook secret — "Leave blank to keep the receiver disabled for your account." That field is the on switch for the per-repository path. Press Save Settings.
- In the repository (or organization) on GitHub, open Settings → Webhooks → Add webhook:
- Payload URL:
https://api.ever.works/api/ingest/github/events - Content type:
application/json - Secret: the exact value from step 2
- Events: pull requests, issue comments, pull request review comments, pushes, and — for the CI auto-fix loop — check runs, check suites and workflow runs
- Payload URL:
- Save, open a pull request, and check Recent Deliveries on the webhook for a
200.
Deliveries with a missing or mismatched X-Hub-Signature-256 are rejected.
What triggers a review, and what only gets recorded
| Delivery | Result |
|---|---|
pull_request opened / synchronize | Ingested as github.pr, then reviewed. The review is keyed on the head SHA, so each pushed revision is reviewed exactly once. |
@ever-works in an issue comment or a pull-request review comment | Ingested as github.mention. The comment text rides along as the review instruction and the reply lands in that thread. |
push | Ingested as github.push and github.commit — Activity only. The code has already landed, so there is nothing to review. |
pull_request closed with merged: true | Ingested as github.merge. Also Activity only. |
check_run / check_suite / workflow_run | Ingested as github.check, and the Task's ciHeadSha (plus a RED ciState) is written from it. A completed failure can auto-resume the Task's run under a bounded retry budget — see CI Auto-Fix. |
For each review the platform matches the repository to a Work across all three repository roles, builds a byte-capped diff, adds Knowledge Base context and memory recall, makes one structured AI call, and posts the result. See Community PR Processing for the review loop in depth.
3. Enable a connector
Every connector except the Slack app is pure setup: enable, paste credentials, wait for the sweep. The event-ingest-tick cron runs every 5 minutes — there is no "sync now" button. Nine of the eleven also accept a historical backfill window.
| Connector | Plugin id | Required credentials | backfillDays |
|---|---|---|---|
| Linear | linear-connector | API key (lin_api_…) | Yes |
| Notion | notion-connector | Integration token (ntn_… / secret_…) | Yes |
| Jira | jira-connector | Site base URL, Atlassian account email, API token | Yes |
| HubSpot | hubspot-connector | Private-app access token (pat-…) | Yes |
| Pipedrive | pipedrive-connector | API token | Yes |
| Zoom | zoom-connector | Account id, client id, client secret (Server-to-Server OAuth app) | Yes |
| Google Workspace | google-workspace-connector | OAuth client id, client secret, refresh token (drive.readonly + calendar.readonly) | Yes |
| Bluesky | bluesky-connector | Handle or DID, app password (never your account password) | Yes |
| Mastodon | mastodon-connector | Instance URL, access token | Yes |
| Slack | slack-connector | Bot token; signing secret for inbound | No |
| Discord | discord-connector | Bot token | n/a |
Connectors carries the full field-by-field matrix, including every optional scoping setting.
How to: enable and configure one
- Open Sidebar → Plugins (
/plugins), or go straight to Settings → Plugins → Connectors (/settings/plugins/connector). Use Search plugins… if the catalog is long. - Press Enable on the card. In the dialog — "Configure how this plugin is enabled across your works." — leave Also enable for all works ticked and press Enable again.
- Open the plugin's settings form, paste the credentials from the table above, and press Save Settings. Enabled is not the same as configured: a connector with a missing required field reports itself not configured and its sweep is skipped quietly.
- Narrow the sweep while you are there.
teamIds,databaseIds,projectKeys,objectTypes,entityTypes,driveFolderIds,calendarIds,surfacesandeventChannelIdsall scope what gets pulled — empty usually means "everything", which is rarely what you want on a large workspace. - Decide on history before the first sweep.
backfillDayswidens the first pull only:0(the default) is off,1–90reaches that many days back, and anything else — negatives,NaN, garbage — is clamped to0. Setting it after the first sweep has already run does nothing. - Wait up to five minutes, then check
/works/:id/activityor/activity.
The Discord connector posts into a channel; it does not read one. It declares no event-source capability, so nothing from Discord reaches your Activity feed and backfillDays does not apply. Inbound routing is a documented follow-up — the publicKey setting sits on the manifest for it, unused today.
4. Notification channels and the digest
Channels are the opposite direction from connectors: they carry alerts, digests and agent pings out. They are a separate plugin family, so slack-channel (an incoming webhook) and slack-connector (a bot token) are two different things and can both be configured at once.
How to: add a channel and test it
-
Open Settings → Channels (
/settings/integrations/channels). -
Press Add channel — "Pick a provider and enter its delivery details. You can send a test after."
-
Pick a provider and fill its fields:
Provider Plugin id Fields Slack slack-channelIncoming Webhook URL ( https://hooks.slack.com/services/…)Discord discord-channelWebhook URL ( https://discord.com/api/webhooks/…)Telegram telegram-channelBot Token, Chat ID ( @channelor a numeric id)WhatsApp whatsapp-channelAccess Token, Phone Number ID, Recipient ( +15551234567)Novu novu-channelAPI Key, Workflow ID, Subscriber ID -
Save, then press Test on the new row. It sends "Ever Works notification channel test message ✓" and marks the row ✓ with the provider's message id, or ✗ with the provider's error.
-
Remove deletes a channel; the row disappears immediately.
The same operations on the API:
# add a channel
curl -X POST https://api.ever.works/api/notification-channels \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{ "pluginId": "slack-channel", "name": "Ops alerts",
"targetConfig": { "webhookUrl": "https://hooks.slack.com/services/…" } }'
# test it
curl -X POST https://api.ever.works/api/notification-channels/<id>/test \
-H "Authorization: Bearer <jwt-token>"
GET /api/notification-channels lists them, PATCH /api/notification-channels/:id renames or disables one, and DELETE /api/notification-channels/:id removes it. Limits worth knowing: the name is capped at 120 characters, targetConfig at 16 KB serialized, creates at 20 per minute and updates at 30 per minute.
Two things to know once channels exist:
- Which events reach which channel is configured at Settings → Notifications. The event × channel grid renders read-only in this version — use the preferences API to change it. Full behaviour, including quiet hours and category mutes, is in Notifications.
- Agents can ping a channel directly with the
notifyChanneltool, given a channel id and plain text. It requires the Agent'scanCallExternalToolspermission and at least one enabled channel. Use it for proactive status pings; leave routine event delivery to the subscription fan-out.
Turn the digest on
The digest is the daily or weekly briefing, and its Connected sources section counts the ingested events per source — which makes it the easiest ongoing check that your connectors are still pulling.
- Open Settings → Digest (
/settings/digest). Digests are off by default. - Pick Daily or Weekly for your personal digest. The dispatcher runs at 07:15 UTC every day; weekly briefings go out on Mondays in that same run.
- The organization digest is a separate setting on the same page — turning one on never changes the other.
Delivery reuses the notifications stack: the bell always gets a copy, and the briefing fans out under the digest_ready event to every channel subscribed to it. See Digests.
5. Where the events show up
| Surface | Route / call | What you see |
|---|---|---|
| Work → Activity | /works/:id/activity | The External activity panel below the platform feed — "Events ingested from the connectors for this Work — repos, trackers, docs, chat and meetings." Source chips start at All sources and are derived from what is actually there; each row deep-links out. |
| Global Activity | /activity | Ingested events fan out to the Activity log. github.push, github.commit and github.merge get dedicated git action types. |
| Memory | /memory | A best-effort memory observation per event, carrying its provenance. |
| Meetings | /memory#meetings | zoom.recording and google.meet-recording envelopes become Meeting rows. |
| API | GET /api/ingest/events?workId=&source= | Owner-scoped, newest first, limit defaults to 20 and caps at 100. |
| Agents and chat | list_recent_events tool | Agents list your recent events filtered by source or workId and cite each one by its sourceUrl. |
An event with no Work match is not an error — it stays account-scoped, still reaches Activity and Memory, and simply is not filed under a Work.
How to: route a source's events to a Work
A connector knows Slack channel ids and Jira project keys; it has never heard of your Work ids. Claim the container and the routing follows.
-
Open the Work and go to its Settings tab (
/works/:id/settings). -
Find Ingest routing claims — "Claim the external containers whose events belong to this Work."
-
Type the source-system id into the matching group and press Add:
Group What goes in it Example Chat channels Slack channel ids C0123456789Tracker teams Linear team keys, Jira project keys ENGDoc databases Notion database ids, Drive folder ids db_2f19c4a7Meetings Recurring meeting / conference ids meeting-8891 -
Press Save claims. Ids are trimmed and compared case-insensitively, capped at 200 characters each and 50 per kind.
Repositories are deliberately absent from that panel: repository events already route through the repositories the Work declares. HubSpot, Pipedrive, Bluesky and Mastodon emit no routing hint at all today, so their events always stay account-scoped.
6. Meetings from recorded calls
A recorded call becomes a first-class Meeting: title, start and end, participants, a link back to the recording and — the part that matters — the transcript.
- Enable Zoom Connector at
/pluginsand open its settings. - Create a Server-to-Server OAuth app in your Zoom account and paste its Account id, Client id and Client secret. Set
backfillDaysnow if you want existing recordings pulled in. Press Save Settings. - Wait for the next 5-minute sweep. Completed cloud recordings arrive as
zoom.recordingenvelopes and are turned into Meeting rows by a kind processor that runs before the Activity write, so a failure retries rather than duplicating feed rows. - Open Sidebar → Memory and scroll to the Meetings block (
/memory#meetings), or filter it —/memory?source=zoom#meetingsis a real, shareable URL.
Google Meet works the same way through Google Workspace Connector: keep meetTranscripts on (the default) and Meet transcript documents are exported into meeting envelopes alongside Drive and Calendar activity. Recordings often complete before their transcripts do, which is why a transcript-bearing delivery is treated as a distinct event rather than a duplicate of the transcript-less first one.
Attaching a transcript — by connector or by hand at /meetings/new — kicks off a best-effort fan-out: an AI summary, a memory observation, and an Activity entry carrying the recording link. Only the transcript write itself can fail the call, so a missing AI key costs you the summary, not the transcript. See Meetings.
Verify the whole chain
Work down this list; each row fails in a distinctive way.
| Check | Where | Healthy looks like |
|---|---|---|
| The plugin is enabled, not just configured | /plugins | The card reads Disable, meaning it is currently on. |
| It is enabled for your Works, not only the account | /works/:id/plugins | The plugin is on for the Work. Account-level on does not cascade. |
| Required credentials are filled in | The plugin's settings page | No unfilled required field; no warning dot on the category in the Settings sidebar. |
| Webhook deliveries are being accepted | Slack Event Subscriptions / GitHub Recent Deliveries | 200, not 401. |
| Events are landing | GET /api/ingest/events or /activity | Rows appear within one 5-minute tick. |
| They are filed under the right Work | /works/:id/activity | The External activity panel is populated, not empty. |
| Outbound delivery works | /settings/integrations/channels | Test marks the row ✓. |
Two failure modes are silent on purpose and worth remembering: a connector missing a required setting skips its sweep quietly rather than erroring every five minutes, and an inbound receiver with nothing configured rejects everything rather than guessing an owner. Neither writes anything you would notice without looking.
Related
- Integrations · Connectors — the event envelope, the receivers, and the full connector catalog
- Plugins — enabling, the account-vs-Work asymmetry, and where settings live
- Notifications · Digests — outbound delivery, subscriptions and briefings
- Activity Log & Schedules · Memory · Meetings — where ingested events end up
- Community PR Processing · Repositories · Work Import — the GitHub side in depth
- Inbound Triggers · MCP Connections — the other two ways outside systems reach your Agents
- Do Everything From Chat · Run Your Business 24/7 — what to do once the events are flowing
- API reference: Notifications · Activity Log · Integrations Module