This val summarizes recent events on Attio lists on Slack.
This project receives Attio webhooks, verifies and processes them, and stores events, messages, and list entry state data in SQLite. A scheduled cron job uses then periodically sends summaries with these stored messages to Slack.
- Remix this val
- Get a Slack webhook
& set it as
SLACK_WEBHOOK_URL
in this val's Environment variables in the left sidebar - Get an Attio Access Token
(with all read & write permissions) & set it as
ATTIO_API_KEY
in this val's Environment variables in the left sidebar - Configure your lists in
webhook.ts
:- Update the
listIds
array with your Attio list IDs - To find your list's id, navigate to the list and copy the path segment
after
collection
:https://app.attio.com/<workspaceName>/collection/<listId>
- Update the
- (Optional) Customize attribute extractors in
webhook.ts
:- Modify the
customExtractors
object to customize how attribute values are formatted in Slack messages - See the "Customizing Attribute Extractors" section below for details
- Modify the
- Go to
setup.ts
and click run to set up the events database and Attio webhook - Go to
alert.ts
and click run to set up the cron job - Go trigger some Attio events and see the message in Slack!
If you want to get notifications about more than one list from your workspace,
simply add the list ID to the listIds
array in webhook.ts
.
Then, go to bootstrap.ts
and run the script. This will
initialize the new list in SQLite.
This template provides a flexible system for customizing how Attio attribute values are formatted in Slack messages. You can override any of the built-in extractors or add custom formatting.
The easiest way to customize extractors is directly in the webhook.ts
file:
// webhook.ts
export const customExtractors: CustomExtractors = {
// Add emojis to text values
text: (value) => `⨠${value.value} āØ`,
// Format status with custom styling
status: (value) => `šø ${value.status.title}`,
// Format currency with custom symbols
currency: (value) =>
`š° ${Intl.NumberFormat("en-US", {
style: "currency",
currency: value.currency_code,
}).format(Number(value.currency_value))}`,
// Format dates with custom format
date: (value) =>
new Date(value.value).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}),
};
You can customize any of these Attio attribute types:
Common Types:
text
- Simple text valuesstatus
- Status values with titlesselect
- Dropdown/select values with option titlesdate
- Date valuestimestamp
- Timestamp valuesrating
- Numeric rating valuescheckbox
- Boolean checkbox valuescurrency
- Currency values with proper formattingrecord-reference
- References to other records (fetches record name)actor-reference
- References to users/actors (fetches actor name)
Uncommon Types:
domain
- Domain valuesemail-address
- Email address valuesinteraction
- Interaction values with type and timestamplocation
- Location values with address formattingnumber
- Numeric valuespersonal-name
- Personal name valuesphone-number
- Phone number values
For more complex customizations, you can:
-
Override extractors programmatically:
import { overrideExtractors } from "./extractors/index.ts"; overrideExtractors({ text: (value) => `⨠${value.value} āØ`, }); -
Pass custom extractors to functions:
import { handleWebhookEvent } from "./services/webhook-handler.ts"; const customExtractors = { text: (value) => `Custom: ${value.value}`, }; await handleWebhookEvent(event, listIds, customExtractors); -
Use the bootstrap function with custom extractors:
import { initialize } from "./scripts/bootstrap.ts"; await initialize(customExtractors);
Here are some common customization patterns you can use as templates:
export const customExtractors: CustomExtractors = {
text: (value) => `š ${value.value}`,
status: (value) => `šø ${value.status.title}`,
select: (value) => `š ${value.option.title}`,
};
export const customExtractors: CustomExtractors = {
currency: (value) => `š° $${value.currency_value.toLocaleString()}`,
number: (value) => `š¢ ${value.value.toLocaleString()}`,
rating: (value) => `ā ${value.value}/5`,
};
export const customExtractors: CustomExtractors = {
date: (value) => `š
${new Date(value.value).toLocaleDateString()}`,
timestamp: (value) => `ā° ${new Date(value.value).toLocaleString()}`,
};
export const customExtractors: CustomExtractors = {
checkbox: (value) => (value.value ? "ā
Yes" : "ā No"),
};
export const customExtractors: CustomExtractors = {
"email-address": (value) => `š§ ${value.email_address}`,
"phone-number": (value) => `š ${value.original_phone_number}`,
"personal-name": (value) => `š¤ ${value.full_name}`,
};
export const customExtractors: CustomExtractors = {
location: (value) => {
const parts = [
value.line_1,
value.locality,
value.region,
value.country_code,
].filter(Boolean);
return `š ${parts.join(", ")}`;
},
domain: (value) => `š ${value.domain}`,
};
export const customExtractors: CustomExtractors = {
// Custom currency formatting with different symbols
currency: (value) => {
const symbol =
value.currency_code === "USD"
? "$"
: value.currency_code === "EUR"
? "ā¬"
: value.currency_code === "GBP"
? "Ā£"
: value.currency_code;
return `${symbol}${Number(value.currency_value).toLocaleString()}`;
},
// Custom date formatting with relative time
date: (value) => {
const date = new Date(value.value);
const now = new Date();
const diffInDays = Math.floor(
(now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)
);
if (diffInDays === 0) return `š
Today`;
if (diffInDays === 1) return `š
Yesterday`;
if (diffInDays < 7) return `š
${diffInDays} days ago`;
return `š
${date.toLocaleDateString()}`;
},
// Custom status formatting with colors
status: (value) => {
const status = value.status.title.toLowerCase();
if (status.includes("active") || status.includes("open"))
return `š¢ ${value.status.title}`;
if (status.includes("pending") || status.includes("waiting"))
return `š” ${value.status.title}`;
if (status.includes("closed") || status.includes("completed"))
return `š“ ${value.status.title}`;
return `āŖ ${value.status.title}`;
},
};
- Copy any of the examples above
- Paste them into your
webhook.ts
file, replacing the emptycustomExtractors
object - Uncomment and modify the extractors you want to customize
- Save the file and your customizations will be applied automatically
To format custom Attio attribute types in your Slack messages, you can add them to the customExtractors
object in webhook.ts
:
// webhook.ts
export const customExtractors: CustomExtractors = {
// Add custom formatters for any attribute type
"custom-type": (value) => value.customField,
};
The extractor system will automatically handle your custom formatters.
The Slack message formatting is handled in shared/slack.ts
. You can customize:
- Message headers and structure
- Link formatting
- Text truncation
- Message grouping logic
To handle additional Attio webhook event types:
- Add the event type to
shared/types.ts
- Update the webhook handler logic in
services/webhook-handler.ts
- Add any new processing logic as needed
If you need to modify the database schema:
- Update the table definitions in
services/sqlite.ts
- Run the migration by executing
createTables()
in a script - Update the corresponding TypeScript types in
shared/types.ts
The project uses these environment variables:
ATTIO_API_KEY
- Your Attio API access tokenSLACK_WEBHOOK_URL
- Your Slack webhook URL
The project includes comprehensive error logging and graceful error handling throughout the webhook processing pipeline.
This project is designed to run on Val Town. To deploy:
- Fork this val on Val Town
- Set environment variables in the Val Town interface:
ATTIO_API_KEY
- Your Attio API access tokenSLACK_WEBHOOK_URL
- Your Slack webhook URL
- Run setup scripts in order:
scripts/setup.ts
- Creates database and webhookscripts/bootstrap.ts
- Seeds initial data
- Set up the cron job by running
alert.ts
and scheduling it - Test the webhook by triggering some Attio events
Required environment variables:
ATTIO_API_KEY
- Attio API access token with read/write permissionsSLACK_WEBHOOK_URL
- Slack incoming webhook URL
The application provides health check endpoints:
GET /
- Basic health statusGET /health
- Detailed health information
Monitor these endpoints to ensure the service is running correctly.
Webhook not receiving events:
- Verify the webhook URL is correct in Attio
- Check that the webhook is enabled for the right event types
- Ensure the webhook secret is properly configured
Slack messages not appearing:
- Verify
SLACK_WEBHOOK_URL
is set correctly - Check Slack webhook permissions
- Review logs for error messages
Database issues:
- Run
scripts/setup.ts
to recreate tables - Check Val Town SQLite storage limits
- Verify database indexes are created properly
attio-slack-summaries/
āāā webhook.ts # Main HTTP endpoint - receives Attio webhooks
āāā alert.ts # Cron job - processes recent events & sends Slack summaries
āāā services/
ā āāā alert-processor.ts # Alert processing logic
ā āāā api-client.ts # Attio API client utilities
ā āāā notifications.ts # Notification handling
ā āāā sqlite.ts # Database operations - CRUD for webhook events & messages
ā āāā webhook-auth.ts # Attio webhook management - creates/stores webhook configs
ā āāā webhook-handler.ts # Webhook event processing logic
āāā shared/
ā āāā slack.ts # Slack integration - message formatting & webhook sending
ā āāā enrich-state.ts # Data enrichment for CRM entries
ā āāā types.ts # Core application types and type definitions
āāā extractors/
ā āāā index.ts # Attribute value extractors for all Attio types
ā āāā README.md # Documentation for the extractor system
āāā scripts/
ā āāā setup.ts # One-time setup - creates DB tables & Attio webhook
ā āāā bootstrap.ts # Bootstrap script for initial setup
āāā dev-helpers/
āāā scratch.ts # Development utilities and testing helpers
Attio webhooks return thin events, which contain a bunch of IDs, but no human-readable data, such as:
{ "event_type": "record.merged", "id": { "workspace_id": "b6c564a6-2cf7-49ab-9320-dea013196bd7", "object_id": "a87cf74e-5ca1-4a8d-b8d3-fcca5413d4c3", "record_id": "d64ff9f2-d1f1-424c-8be6-e41129e35697" }, "duplicate_object_id": "a87cf74e-5ca1-4a8d-b8d3-fcca5413d4c3", "duplicate_record_id": "112b5c78-1ffe-457a-8366-181b482888b4", "actor": { "type": "system", "id": null } }
The trickiest part of this val was transformning these events into data that could be used to track the state of entries in lists. I've accomplished this here by storing the state of each entry in a SQLite database and then comparing subsequent states to the previous state of the entry to determine the changes.
These state objects are then used to generate message objects that are also stored in the SQLite database. When the cron job runs, it takes these message objects formats them into a nice payload and sends them to Slack.