Hourly CRM inbound orchestrator for three inboxes using Notion-synced SOP, strict business-lead filt
Hourly CRM inbound orchestrator for three inboxes using Notion-synced SOP, strict business-lead filtering, Supabase persistence, and actionable-only Slack reporting.
Use this skill for hourly polling CRM workflows across:
info@yourdomain.comsales@yourdomain.comsupport@yourdomain.comThe source-of-truth SOP is synced from Notion page CRM_SOP_PAGE_ID every run.
Grab the skill package ZIP file using the button above.
Extract and move the folder into your AI agent's skills directory.
Your agent now knows the skill. Just ask it to perform the task!
This is the raw instruction document consumed by your AI agent.
Use this skill for hourly polling CRM workflows across:
info@yourdomain.comsales@yourdomain.comsupport@yourdomain.comThe source-of-truth SOP is synced from Notion page CRM_SOP_PAGE_ID every run.
Required:
NOTION_API_KEYCRM_SOP_PAGE_ID (default: 31288fb313488013924ade7bf704ab6f)CRM_MONITORED_EMAILS (comma-separated)CRM_POLL_QUERY (default: in:inbox is:unread -in:spam -in:trash -category:promotions -category:social -category:updates -category:forums)CRM_POLL_OVERLAP_MINUTES (default: 120)SUPABASE_URLSUPABASE_SECRET_KEYOptional:
CRM_POLL_MAX_RESULTS (default: 200)CRM_POLL_MAX_AGE_HOURS (default: 36)CRM_SOP_CACHE_FILE (default: /tmp/crm-inbound-sop-cache.json)CRM_POLL_STATE_TABLE (default: crm_poll_state)CRM_CONTACTS_TABLE (default: crm_contacts)CRM_ACTIVITIES_TABLE (default: crm_activities)CRM_DRAFTS_TABLE (default: crm_drafts)CRM_ACCOUNTING_TABLE (default: accounting_entries)CRM_JOB_RUNS_TABLE (default: crm_job_runs)GOG_ACCOUNT (fallback sender account for approvals)CRM_OUTSTANDING_LOOKBACK_DAYS (default: 7)CRM_OUTSTANDING_STALE_HOURS (default: 24)CRM_OUTSTANDING_NOTIFY_EMPTY (default: false)CRM_CLASSIFIER_MODEL (default: gpt-5-nano)CRM_REPLY_MODEL (default: gpt-5.2)CRM_USE_MODEL_CLASSIFIER (default: true)CRM_USE_MODEL_REPLY_WRITER (default: true)OPENAI_API_KEY (required to use model classifier/reply writer)CRM_GMAIL_LABEL_APPLY (default: true)CRM_GMAIL_LABEL_LEAD (default: CRM/Lead)tsx {baseDir}/scripts/fetch-sop.ts fetch_sop
Optional flags:
--page-id <id>--cache-file <path>--output <path>tsx {baseDir}/scripts/poll-inboxes.ts poll_inboxes
Optional flags:
--accounts <csv>--query <gmail-query>--overlap-minutes <n>--max-age-hours <n>--output <path>tsx {baseDir}/scripts/process-inbound.ts process_inbound \
--poll-file /tmp/crm-poll.json
Optional flags:
--sop-file <path>--output <path>tsx {baseDir}/scripts/approval-action.ts approval_action \
--action approve \
--draft-id <draft_id> \
--approved-by "U052337J8QH"
Also supported:
--action revise --notes "<feedback>"--action reject --reason "<reason>"tsx {baseDir}/scripts/check-outstanding.ts check_outstanding
Optional flags:
--lookback-days <n> (default: 7)--stale-hours <n> (default: 24)--output <path>For each actionable lead, post a simple Slack card containing only:
Approval/revision happens in the Slack thread, not via command strings in the main message.
account_email:message_id.gpt-5-nano into receipt|sales|support|ignore (fallback to heuristics only if model call fails).classification, lead, inbound, routing, qualification) and inject it into the classifier prompt.sales when business ask is explicit.CRM/Lead (or CRM_GMAIL_LABEL_LEAD) to sales threads.view in browser, unsubscribe, manage preferences, roundup-style blasts, Gmail promotional categories) are forced to ignore unless explicit lead criteria are met.sales guesses are downgraded to ignore.gpt-5.2Use clear headings in your Notion page so policy extraction stays deterministic:
Business Context
Lead Classification Rules
sales): person/company reaching out for consulting, sponsorship, partnerships, affiliate opportunities, expert-network interviews, or any paid advisory callLead Qualification Checklist
Response Playbook
Out-of-Scope
Reference template:
cat {baseDir}/references/notion-inbound-sop-template.md
Tables:
crm_contactscrm_activitiescrm_draftsaccounting_entriescrm_job_runscrm_poll_stateReference DDL:
cat {baseDir}/references/supabase-schema.sql
openclaw cron add \
--name "CRM hourly polling" \
--cron "0 * * * *" \
--tz "America/New_York" \
--session isolated \
--message "Run crm-inbound-orchestrator hourly polling cycle. Use skill crm-inbound-orchestrator. Run fetch_sop, poll_inboxes, process_inbound. Only report actionable items."
openclaw cron add \
--name "CRM morning outstanding check" \
--cron "20 9 * * *" \
--tz "America/Toronto" \
--session isolated \
--message "Run crm-inbound-orchestrator outstanding review. Use skill crm-inbound-orchestrator. Run check_outstanding for last 7 days and post a concise summary to Slack."
degraded.Browse additional components, config blocks, and reference sheets included in the ZIP.
crm-inbound-orchestrator/SKILL.md
crm-inbound-orchestrator/references/notion-inbound-sop-template.md
crm-inbound-orchestrator/references/supabase-schema.sql
crm-inbound-orchestrator/scripts/approval-action.ts
crm-inbound-orchestrator/scripts/check-outstanding.ts
crm-inbound-orchestrator/scripts/fetch-sop.ts
crm-inbound-orchestrator/scripts/poll-inboxes.ts
crm-inbound-orchestrator/scripts/process-inbound.ts
skills/crm-inbound-orchestrator/references/supabase-schema.sql
-- CRM Inbound orchestrator schema (idempotent, replay-safe)
-- Apply in Supabase SQL editor before first run.
create extension if not exists pgcrypto;
create table if not exists crm_contacts (
id uuid primary key default gen_random_uuid(),
email text not null unique,
display_name text,
source_account_email text,
last_seen_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists crm_activities (
id uuid primary key default gen_random_uuid(),
source_key text not null unique,
account_email text not null,
message_id text not null,
thread_id text,
from_raw text,
from_email text,
from_name text,
subject text,
snippet text,
received_at timestamptz,
classification text not null,
classification_confidence numeric(5,4),
classification_reasons jsonb not null default '[]'::jsonb,
contact_id uuid references crm_contacts(id),
contact_email text,
sop_hash text,
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (account_email, message_id)
);
create table if not exists crm_drafts (
id uuid primary key default gen_random_uuid(),
activity_id uuid not null unique references crm_activities(id) on delete cascade,
account_email text not null,
to_email text not null,
subject text not null,
body text not null,
status text not null default 'draft',
approval_commands text,
slack_summary text,
reply_to_message_id text,
sop_hash text,
revision_notes text,
rejected_reason text,
approved_by text,
approved_at timestamptz,
rejected_at timestamptz,
sent_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists accounting_entries (
id uuid primary key default gen_random_uuid(),
source_key text not null unique,
activity_id uuid references crm_activities(id) on delete set null,
account_email text not null,
vendor text,
amount numeric(14,2),
currency text,
receipt_date timestamptz,
subject text,
snippet text,
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists crm_job_runs (
id uuid primary key,
started_at timestamptz not null,
finished_at timestamptz,
status text not null,
degraded boolean not null default false,
poll_partial_failure boolean not null default false,
metrics jsonb not null default '{}'::jsonb,
accounts jsonb not null default '[]'::jsonb,
warnings jsonb not null default '[]'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists crm_poll_state (
account_email text primary key,
last_polled_at timestamptz,
last_message_ts timestamptz,
updated_at timestamptz not null default now()
);
create index if not exists idx_crm_activities_account_received
on crm_activities (account_email, received_at desc);
create index if not exists idx_crm_activities_classification
on crm_activities (classification);
create index if not exists idx_crm_drafts_status
on crm_drafts (status, updated_at desc);
create index if not exists idx_accounting_entries_receipt_date
on accounting_entries (receipt_date desc);