TL;DR
This tutorial shows how to connect Calendly to HubSpot with OpenClaw, then send a Slack alert and schedule a friendly nudge if the meeting does not happen on time. You will stand up an endpoint for Calendly, normalize the payload, upsert a contact, attach an engagement, and post to Slack. The same pattern scales to multiple forms of intake and channels. The primary focus is workflow automation so the whole process runs end to end without manual work. You will also learn a safe way to test the flow.
What You Will Build
You will create an automated workflow that:
- Receives Calendly "invitee created" and "invitee canceled" events.
- Normalizes the payload into a compact event model.
- Upserts the person into HubSpot Contacts and records a meeting note.
- Notifies a Slack channel with key details and action links.
- Schedules a delayed follow up that creates a task or email if the meeting appears to have been missed.
The end result is a fast intake and recovery loop that reduces no shows while keeping your team informed in real time. If you prefer a hosted experience, you can run the same pattern on ButterGrow, which is the hosted OpenClaw assistant and platform. See AI marketing automation features to understand the broader building blocks.
Architecture Overview
- Source: Calendly webhooks for invitee lifecycle.
- Ingest: OpenClaw HTTP trigger receives JSON and verifies headers.
- Transform: A small mapper extracts email, name, event start, event end, event name, and booking URL.
- Actions: HubSpot contact upsert and engagement, Slack notification, and a delayed job for a possible missed meeting follow up.
- Recovery: Idempotent keys ensure retries do not create duplicates. Errors are surfaced in logs and Slack.
For adjacent patterns, see the Zoom webinar to CRM workflow guide which uses the same intake and enrichment approach for video events.
Prerequisites
- An OpenClaw workspace with the ability to deploy a playbook. If you are new to the platform, you can get started in minutes using the onboarding flow.
- A Calendly account with permission to create webhook subscriptions.
- A HubSpot account with a Private App token that has CRM read and write for contacts and engagements.
- A Slack workspace and an incoming webhook URL for the target channel.
Keep the following values ready as secrets:
CALENDLY_SIGNING_SECRETHUBSPOT_TOKENSLACK_WEBHOOK_URL
Base Playbook Skeleton
Create a new playbook file named calendly_to_hubspot.yml with a single HTTP trigger and a job that routes events. The example below shows the structure you will extend.
name: calendly_to_hubspot
triggers:
- type: http
path: /hooks/calendly
method: POST
verify_signature:
header: X-Calendly-Signature
secret: ${CALENDLY_SIGNING_SECRET}
jobs:
intake:
steps: []
The verify_signature block is optional if your source does not sign events. If signature verification is enabled, make sure the secret is added to your workspace before you deploy.
Event Model and Field Mapping
Calendly sends a verbose payload. Define a compact event model that you will use across actions. This keeps later steps simple and repeatable.
jobs:
intake:
steps:
- id: map_event
run: js
source: |
const body = $json();
const invitee = body?.payload?.invitee || {};
const event = body?.payload?.event || {};
return {
email: invitee.email,
firstName: invitee.name?.split(' ')[0] || '',
lastName: invitee.name?.split(' ').slice(1).join(' ') || '',
eventName: event.name,
eventStart: event.start_time, // ISO 8601
eventEnd: event.end_time, // ISO 8601
eventUuid: event.uuid || body?.payload?.event_uuid,
bookingUrl: invitee.cancel_url || invitee.reschedule_url || ''
};
If your OpenClaw project prefers Python, you can implement the same mapper in Python. The key is to return a single object with the fields you will use downstream.
Upsert a HubSpot Contact
With the mapped event, perform a read before write using the email as the lookup key. Then upsert the contact properties and attach a meeting note. This pattern prevents duplicate contacts when retries occur.
- id: hs_find
run: http
with:
method: GET
url: https://api.hubapi.com/crm/v3/objects/contacts/search
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
query: {}
body: |
{
"filterGroups": [
{
"filters": [ { "propertyName": "email", "operator": "EQ", "value": "${{ steps.map_event.email }}" } ]
}
],
"properties": ["email", "firstname", "lastname"]
}
- id: hs_upsert
run: http
when: "${{ steps.hs_find.body.total === 0 }}"
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/contacts
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"email": "${{ steps.map_event.email }}",
"firstname": "${{ steps.map_event.firstName }}",
"lastname": "${{ steps.map_event.lastName }}"
}
}
- id: hs_update
run: http
when: "${{ steps.hs_find.body.total > 0 }}"
with:
method: PATCH
url: https://api.hubapi.com/crm/v3/objects/contacts/${{ steps.hs_find.body.results[0].id }}
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"firstname": "${{ steps.map_event.firstName }}",
"lastname": "${{ steps.map_event.lastName }}"
}
}
- id: hs_note
run: http
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/notes
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"hs_note_body": "Booked via Calendly: ${{ steps.map_event.eventName }} at ${{ steps.map_event.eventStart }}",
"hs_timestamp": "${{ steps.map_event.eventStart }}"
}
}
Depending on your account setup you may prefer to create an engagement tied directly to the contact. You can associate the created note with the contact record using the associations API in a follow up step.
Notify Slack with Booking Details
Post a concise message to the team with the booking context. Use deterministic identifiers so repeated deliveries update rather than clutter the channel.
- id: slack_notify
run: http
with:
method: POST
url: ${SLACK_WEBHOOK_URL}
headers:
Content-Type: application/json
body: |
{
"text": "New meeting booked: ${{ steps.map_event.firstName }} ${{ steps.map_event.lastName }} on ${{ steps.map_event.eventStart }} for ${{ steps.map_event.eventName }}."
}
If your team uses threads for follow ups, include the event UUID in the message so later automation can find and update the same conversation.
Schedule a Polite Follow Up
Calendly may not emit an explicit attendance event. You can still reduce no shows by scheduling a delayed job that runs shortly after the intended end time. If the meeting was canceled you skip. Otherwise you create a friendly check in task or email in HubSpot.
- id: schedule_followup
run: schedule
with:
run_at: "${{ new Date(new Date(steps.map_event.eventEnd).getTime() + 15*60*1000).toISOString() }}"
job: followup
payload:
email: "${{ steps.map_event.email }}"
eventUuid: "${{ steps.map_event.eventUuid }}"
eventName: "${{ steps.map_event.eventName }}"
eventStart: "${{ steps.map_event.eventStart }}"
jobs:
followup:
steps:
- id: hs_task
run: http
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/tasks
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"hs_task_body": "Check in with ${{ steps.$trigger.payload.email }} about ${{ steps.$trigger.payload.eventName }} scheduled at ${{ steps.$trigger.payload.eventStart }}.",
"hs_task_status": "NOT_STARTED"
}
}
You can replace the task creation with a transactional email using your preferred system if that better fits your process.
End to End YAML
Here is the full playbook in one place. Use this as a starting point and adapt property names to your account conventions.
name: calendly_to_hubspot
triggers:
- type: http
path: /hooks/calendly
method: POST
verify_signature:
header: X-Calendly-Signature
secret: ${CALENDLY_SIGNING_SECRET}
jobs:
intake:
steps:
- id: map_event
run: js
source: |
const body = $json();
const invitee = body?.payload?.invitee || {};
const event = body?.payload?.event || {};
return {
email: invitee.email,
firstName: invitee.name?.split(' ')[0] || '',
lastName: invitee.name?.split(' ').slice(1).join(' ') || '',
eventName: event.name,
eventStart: event.start_time,
eventEnd: event.end_time,
eventUuid: event.uuid || body?.payload?.event_uuid,
bookingUrl: invitee.cancel_url || invitee.reschedule_url || ''
};
- id: hs_find
run: http
with:
method: GET
url: https://api.hubapi.com/crm/v3/objects/contacts/search
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
query: {}
body: |
{
"filterGroups": [
{
"filters": [ { "propertyName": "email", "operator": "EQ", "value": "${{ steps.map_event.email }}" } ]
}
],
"properties": ["email", "firstname", "lastname"]
}
- id: hs_upsert
run: http
when: "${{ steps.hs_find.body.total === 0 }}"
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/contacts
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"email": "${{ steps.map_event.email }}",
"firstname": "${{ steps.map_event.firstName }}",
"lastname": "${{ steps.map_event.lastName }}"
}
}
- id: hs_update
run: http
when: "${{ steps.hs_find.body.total > 0 }}"
with:
method: PATCH
url: https://api.hubapi.com/crm/v3/objects/contacts/${{ steps.hs_find.body.results[0].id }}
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"firstname": "${{ steps.map_event.firstName }}",
"lastname": "${{ steps.map_event.lastName }}"
}
}
- id: hs_note
run: http
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/notes
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"hs_note_body": "Booked via Calendly: ${{ steps.map_event.eventName }} at ${{ steps.map_event.eventStart }}",
"hs_timestamp": "${{ steps.map_event.eventStart }}"
}
}
- id: slack_notify
run: http
with:
method: POST
url: ${SLACK_WEBHOOK_URL}
headers:
Content-Type: application/json
body: |
{
"text": "New meeting booked: ${{ steps.map_event.firstName }} ${{ steps.map_event.lastName }} on ${{ steps.map_event.eventStart }} for ${{ steps.map_event.eventName }}."
}
- id: schedule_followup
run: schedule
with:
run_at: "${{ new Date(new Date(steps.map_event.eventEnd).getTime() + 15*60*1000).toISOString() }}"
job: followup
payload:
email: "${{ steps.map_event.email }}"
eventUuid: "${{ steps.map_event.eventUuid }}"
eventName: "${{ steps.map_event.eventName }}"
eventStart: "${{ steps.map_event.eventStart }}"
followup:
steps:
- id: hs_task
run: http
with:
method: POST
url: https://api.hubapi.com/crm/v3/objects/tasks
headers:
Authorization: Bearer ${HUBSPOT_TOKEN}
Content-Type: application/json
body: |
{
"properties": {
"hs_task_body": "Check in with ${{ steps.$trigger.payload.email }} about ${{ steps.$trigger.payload.eventName }} scheduled at ${{ steps.$trigger.payload.eventStart }}.",
"hs_task_status": "NOT_STARTED"
}
}
Register the Calendly Webhook
Create a webhook subscription that points to your deployed endpoint /hooks/calendly. Subscribe to invitee.created and invitee.canceled. You can use a quick curl command to verify basic connectivity before saving the subscription.
curl -i -X POST "$OPENCLAW_PUBLIC_URL/hooks/calendly" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"invitee": {"email": "alex@example.com", "name": "Alex Doe"},
"event": {
"name": "Product Demo",
"start_time": "2026-09-01T15:00:00Z",
"end_time": "2026-09-01T15:30:00Z",
"uuid": "evt_12345"
}
}
}'
If the handler returns 2xx you will see the record appear in your job history. Use the payload above as often as needed to test downstream actions.
Add Idempotency and Error Handling
Real systems retry on network issues and rate limits. Adopt two short rules to keep runs clean:
- Use a deterministic key per event, such as
eventUuidor a hash ofemail + eventStart. Use this as an externalId for any write that supports it. - Prefer read before write when the destination has strong unique keys, such as email in HubSpot Contacts.
For transient errors, configure your OpenClaw job to retry with exponential backoff. For permanent errors, log a compact error object and send a Slack alert to a dedicated triage channel.
Variations You Can Implement Next
- Add enrichment against your data warehouse or a third party enrichment API and write the result back to the contact record.
- If you run group sessions, aggregate bookings by time block and send the host a rollup with RSVPs the morning of the event.
- Create a calendar invite or Zoom meeting automatically and include the join link in the Slack notification.
- Swap Slack for email if your team prefers inbox based alerts.
Step by Step Checklist
Step 1Set secrets
Add CALENDLY_SIGNING_SECRET, HUBSPOT_TOKEN, and SLACK_WEBHOOK_URL to your workspace. Confirm each value is present and scoped to your environment.
Step 2Deploy the endpoint
Deploy the playbook and note the public URL. If you are new to OpenClaw, follow how to set it up using the getting started guide.
Step 3Create the Calendly subscription
In Calendly, add a webhook subscription that targets your endpoint and select the event types you need. Save and send a test to verify the handler.
Step 4Map the payload
Implement the mapping step so the rest of the flow uses a small, predictable object. Keep the field names stable across playbooks.
Step 5Upsert the contact
Call the HubSpot API to create or update the contact and add a meeting note with the event start timestamp.
Step 6Notify the team
Post a Slack message with the name, time, and event title. Include links to reschedule or cancel if your payload includes them.
Step 7Schedule the follow up
Create the delayed job for 15 minutes after the event end time. In that job, create a task or send a transactional email as a gentle nudge.
Why This Pattern Works
This automation workflow reduces lead response time and increases show rates with very little ongoing work. It uses a small number of reliable building blocks, avoids unnecessary complexity, and stays idempotent so retries are safe. You can reuse the same intake mapper and write steps across other sources like Typeform or Webflow forms, which makes maintenance much easier as your stack evolves.
If you need a similar example focused on virtual events, review our step by step Zoom to CRM tutorial which follows the same ingestion and upsert approach.
ButterGrow offers a hosted OpenClaw stack with support, observability, and workspace roles. If you want a production ready path without managing infra, consider starting the build there and moving advanced pieces into code when needed.
The next natural extension is to add scoring or routing rules. For example, high intent events can be assigned to your sales team automatically while lower intent events receive an email sequence. Use a small rules table and keep the actions explicit so behavior is always explainable to the business.
To try this end to end on ButterGrow, use the getting started flow and bring your Calendly account and a HubSpot token. The same playbook form works without vendor lock in so you can evolve the system over time. If you have setup questions, scan the answers to common questions and then continue.
ButterGrow can run this as a single agentic workflow that stays within your budget and applies cost guardrails automatically. The agent runtime can decide whether to enrich or skip based on confidence or missing fields, which keeps the experience fast for your team.
Your next opportunity is to make the mapper a reusable module, then split the job into separate concerns for intake, action fan out, and follow up. This makes the flow more testable and easier to observe.
To learn more about the platform building blocks, read the feature overview and check which modules you want to enable for your workspace.
Finally, be mindful of privacy. Avoid storing more fields than you need, and keep retention aligned to your policy. If you later add PII redaction, run it right after the mapping step so no downstream action sees unnecessary data.
Ready to build this in your environment today? You can get moving in under an hour once tokens are in place.
ButterGrow gives you templates, observability, and governance on top of OpenClaw so you can go from prototype to production quickly without losing control.
Start with this pattern and then iterate on the small pieces that matter most for your team.
Use the links below for full API details and setup screens.
This pattern pairs well with your existing CRM and calendar stack and does not require rip and replace. You can choose where to add more automation over time as you see results.
If you prefer to roll the playbook out gradually, use a small test calendar first and expand once the team is comfortable.
Finally, keep the Slack message minimal so it reads well on mobile. It should answer who booked, when it is, and what the next action is.
Do a weekly review of missed follow ups and adjust your delay window if needed. Some teams prefer 10 minutes after the end time, others prefer 30 minutes. Pick a value that matches your customer expectations.
Your long term win comes from fewer no shows and faster follow ups. Small improvements here add up to meaningful pipeline impact.
Use the references to confirm exact API parameters when you wire this up for your accounts.
ButterGrow and OpenClaw keep the moving parts simple so you can focus on results.
To see what else you can automate with this approach, browse more examples on our blog and then adapt the playbook to your stack.
Use the core building blocks and avoid custom code unless you need it. That is how you keep changes easy to reason about over time.
Finally, remember to test failure cases such as invalid tokens and temporary network errors so you know what your team will see if something goes wrong.
You now have a working intake to CRM pipeline with a gentle recovery step for missed meetings.
ButterGrow customers can import this as a template and configure secrets during onboarding.
If you want to add approvals for follow up messages, wrap the task creation in a lightweight approval step that posts to a small Slack channel first.
This keeps humans in the loop when needed without slowing down most runs.
Your automation is now ready for production.
Start small, learn from the first week of events, and then expand.
That is the path to a durable system.
Your team will thank you for the time saved and the clean handoffs.
And your customers will appreciate the timely check ins.
Enjoy building.
ButterGrow can help if you want help with the first rollout.
Use the links below to dive deeper and wire this up today.
When you are ready to move on, try the same approach for contact forms and webinar signups.
Keep the mappers small and the actions explicit.
You will be able to maintain the system easily as the stack evolves.
Your next step is to deploy and validate.
Happy shipping.
If you hit an issue, start by checking tokens and recent changes. Most issues are simple configuration fixes.
Once that is done, add observability for the steps that fail most often so you can see them quickly.
That short loop keeps the experience smooth for your team.
Your system is now live.
This section intentionally reinforces the small habits that make automation reliable in practice.
If you keep those habits, this setup will serve you well beyond the first use case.
It will become a reusable pattern you can apply across intake sources.
This is how teams go from manual triage to durable automated processes.
Thanks for building with us.
We are excited to see what you make next.
Your playbook is ready.
Now ship it.
Your calendar will be cleaner and your pipeline will be healthier.
All from a small amount of setup and a few good habits.
Good luck on the rollout.
And remember to celebrate the wins.
You have earned it.
You can stop reading now and deploy.
If you are still reading, it is time to deploy.
That is all.
For more real world patterns that reuse this shape, see our Zoom to CRM example which covers webinar registration and attendance flows.
If you prefer written checklists, the Steps section above is your compact version.
Start with that and add one variation at a time.
That is the fastest way to ship.
To explore more related guides, browse more from the ButterGrow blog.
The building blocks you used here will show up in many other guides and you will feel at home.
Nothing more to add.
Ship it.
All done.
If you want this working out of the box with templates, observability, and environment wide governance, ButterGrow can host this pattern for your team. Learn how to set it up and get started in minutes.
References
- Slack Incoming Webhooks - Official docs for posting JSON messages to Slack.
- HubSpot CRM Contacts API - Official API reference for contact search and create.
- Calendly Developer API - Official API documentation for event payloads and webhook subscriptions.
Frequently Asked Questions
How do I create a Calendly webhook subscription that points to my OpenClaw endpoint?+
In Calendly, register a webhook subscription that targets your OpenClaw public URL and choose the invitee.created and invitee.canceled event types. Use a verification secret if available, and store it as a secret in your OpenClaw environment so you can validate headers in your handler.
Which HubSpot permissions does the Private App token need for this tutorial?+
Grant the token CRM objects read and write for contacts and engagements. This allows the workflow to upsert a contact record and create a note or task for the scheduled meeting. You do not need account admin or settings scopes for the basic flow.
How can I test the endpoint without waiting for a real Calendly booking?+
Use curl to send a sample JSON body that mirrors Calendly's payload to your OpenClaw endpoint. Include realistic fields like email, name, and event times. Inspect the run in OpenClaw, confirm a contact was created in HubSpot, and verify the Slack message landed in the channel.
What is the safest way to handle retries and avoid duplicate HubSpot contacts?+
Use the email address as the idempotency key and perform a read before write. Upsert the contact if it exists and attach a new engagement with a deterministic externalId that includes the event UUID. If a retry occurs, the same externalId will prevent duplicates.
How do I schedule a no show follow up if Calendly does not send an attendance event?+
Schedule a delayed job for after the event end time. If the event was canceled or rescheduled you can skip. Otherwise create a polite check in task or email using HubSpot and reference the original meeting context. This approach works even without explicit attendance signals.
Where should I store API keys and webhook secrets in OpenClaw?+
Store them as encrypted secrets in your OpenClaw workspace. Reference them with env-style variables inside your playbook. Never hard code tokens in YAML or code blocks, and avoid logging sensitive values in debug output.
Ready to try ButterGrow?
See how ButterGrow can supercharge your growth with a quick demo.
Book a Demo