@channlize/skills 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/prompts.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "$comment": "Copy-paste prompts for AI assistants. Placeholders: {{projectId}}, {{widgetKey}} (public values), and {{react}}, {{core}}, {{mcp}} (package versions below). Secrets are never filled in: prompts refer to env vars. Shown by `npx @channlize/skills prompts` and the dashboard's SDK & Integration → AI prompts tab.",
3
+ "versions": {
4
+ "react": "0.5.0",
5
+ "core": "0.5.0",
6
+ "mcp": "0.5.0"
7
+ },
8
+ "categories": {
9
+ "react": "React / Next.js",
10
+ "server": "Node / server",
11
+ "mcp": "MCP",
12
+ "connections": "Connections",
13
+ "support": "Support"
14
+ },
15
+ "prompts": [
16
+ {
17
+ "id": "nextjs-chat-widget",
18
+ "title": "Add the chat widget to my Next.js app",
19
+ "category": "react",
20
+ "frameworks": ["Next.js"],
21
+ "skills": ["channlize-react-widget"],
22
+ "prompt": "Add the Channlize live chat widget to this Next.js (App Router) app.\n\nUse @channlize/react@^{{react}}. If the channlize-react-widget skill is available, read it first and follow it.\n\n- Widget key (public): {{widgetKey}}\n- Project ID: {{projectId}}\n\nSteps:\n1. Install @channlize/react with this project's package manager.\n2. Put both values in .env.local as NEXT_PUBLIC_CHANNLIZE_WIDGET_KEY and NEXT_PUBLIC_CHANNLIZE_PROJECT_ID, and add the names (without values) to .env.example.\n3. Create a 'use client' component that renders <ChannlizeChat> with those values, our brand color as primaryColor, and a title and greeting that fit this product. Render it once from the root layout, which stays a server component.\n4. Only the public widget key (wk_…) may appear in client code. Never add a server key (ch_live_…).\n\nVerify: run the type-check and the build, start the dev server, and tell me which page to open to send a test message. Remind me to add my domains (including localhost) under Domains in the Channlize dashboard."
23
+ },
24
+ {
25
+ "id": "react-vite-chat-widget",
26
+ "title": "Add the chat widget to my React (Vite) app",
27
+ "category": "react",
28
+ "frameworks": ["React", "Vite"],
29
+ "skills": ["channlize-react-widget"],
30
+ "prompt": "Add the Channlize live chat widget to this React app built with Vite.\n\nUse @channlize/react@^{{react}}. If the channlize-react-widget skill is available, read it first and follow it.\n\n- Widget key (public): {{widgetKey}}\n- Project ID: {{projectId}}\n\nRead them from VITE_CHANNLIZE_WIDGET_KEY and VITE_CHANNLIZE_PROJECT_ID (import.meta.env), and render <ChannlizeChat> once at the app root with our brand color as primaryColor and theme=\"auto\". Only the public widget key (wk_…) may be in the bundle, never a server key.\n\nVerify: run the type-check and the build, then start the dev server and tell me how to send a test message that should appear in Slack."
31
+ },
32
+ {
33
+ "id": "contact-form-page",
34
+ "title": "Add a contact form page",
35
+ "category": "react",
36
+ "frameworks": ["Next.js", "React"],
37
+ "skills": ["channlize-react-widget"],
38
+ "prompt": "Add a /contact page with the Channlize contact form, matching this app's design.\n\nUse @channlize/react@^{{react}}. If the channlize-react-widget skill is available, read it first and follow it.\n\n- Widget key (public): {{widgetKey}}\n- Project ID: {{projectId}}\n\nRequirements:\n- In Next.js, keep the page a server component and put <ChannlizeProvider> + <ContactForm> in a 'use client' component. ContactForm must be inside ChannlizeProvider.\n- Topics: Sales, Support, Billing. Prefill the topic from a ?topic= query parameter with defaultValues.\n- Company field optional. theme=\"auto\" and our brand color as primaryColor.\n- Show a success state, and link the page from the footer or navigation.\n- Only the public widget key in client code.\n\nVerify: run the type-check and the build, then submit the form once locally and confirm the onSuccess callback receives a conversationId."
39
+ },
40
+ {
41
+ "id": "notify-signup",
42
+ "title": "Send a Slack notification when a user signs up, subscribes or pays",
43
+ "category": "server",
44
+ "frameworks": ["Node.js", "Next.js"],
45
+ "skills": ["channlize-notify-events"],
46
+ "prompt": "Post a Slack message through Channlize Events when a user signs up, starts a subscription, or pays an invoice in this app.\n\nUse createNotifier from @channlize/core@^{{core}} on the server only. If the channlize-notify-events skill is available, read it first and follow it.\n\n- Project ID: {{projectId}}\n- The server key is in the CHANNLIZE_SERVER_API_KEY env var. I will set it myself; don't ask me to paste it, and never put it in client code or a NEXT_PUBLIC_/VITE_ variable.\n\nSteps:\n1. Find where signups, subscriptions and payments are completed (after the database write or the payment provider's webhook).\n2. Create one typed notifier module: createNotifier<AppEvents>() with the keys user.signed_up, subscription.started and invoice.paid and the data each message needs (no secrets or card data).\n3. Call it after the action succeeds, with an idempotencyKey derived from the user, subscription or invoice ID. Catch and log errors so a Slack failure never breaks the request.\n4. Add CHANNLIZE_SERVER_API_KEY and CHANNLIZE_PROJECT_ID={{projectId}} to .env.example (no secret values).\n\nVerify: run the type-check and tests, and add a test or a script that calls notify once and prints res.status. Then tell me that each new key starts as a disabled draft (status 'skipped') that I must configure and enable under Events in the dashboard."
47
+ },
48
+ {
49
+ "id": "track-errors",
50
+ "title": "Track errors as logs",
51
+ "category": "server",
52
+ "frameworks": ["React", "Node.js"],
53
+ "skills": ["channlize-log-tracking"],
54
+ "prompt": "Send this app's errors to Channlize log tracking.\n\nUse @channlize/core@^{{core}} (or useChannlizeAnalytics from @channlize/react@^{{react}} in React). If the channlize-log-tracking skill is available, read it first and follow it.\n\n- Project ID: {{projectId}}\n- Browser: the public widget key {{widgetKey}} with trackEvent.\n- Server: trackServerEvent with the server key from the CHANNLIZE_SERVER_API_KEY env var (I'll set it; never expose it to the browser).\n\nReport each error once from a central place (an error boundary or window error/unhandledrejection handlers in the browser, the error middleware on the server) with severity 'error', a stable dotted name such as app.error or api.error, and small properties (route, error name, message; no tokens or personal data). Tracking must never throw into the app.\n\nVerify: run the type-check and tests, then trigger one test error and tell me to check /logs in the dashboard, filtered by severity error."
55
+ },
56
+ {
57
+ "id": "connect-stripe",
58
+ "title": "Connect Stripe webhooks to Channlize",
59
+ "category": "connections",
60
+ "frameworks": ["Stripe"],
61
+ "skills": ["channlize-connections"],
62
+ "prompt": "Help me connect Stripe to Channlize Connections, so failed payments, disputes, new subscriptions and cancellations post to Slack.\n\nIf the channlize-connections skill is available, read it and its references/presets.md first.\n\nThis is a dashboard setup, not code. Walk me through it step by step:\n1. Create the Stripe connection in the Channlize dashboard (Connections → New connection → Stripe) for project {{projectId}}.\n2. Add the event destination in Stripe with exactly the events the Setup tab lists.\n3. Paste the destination's signing secret (whsec_…) into the Setup tab. I will copy it myself; never ask me to paste secrets here.\n4. Explain that test mode needs its own connection.\n5. Map each event type to a Slack channel on the Events tab.\n\nVerify: tell me how to send a test event from Stripe and where to confirm it (the connection's Request log, then Slack)."
63
+ },
64
+ {
65
+ "id": "connect-github-sentry",
66
+ "title": "Connect GitHub and Sentry webhooks",
67
+ "category": "connections",
68
+ "frameworks": ["GitHub", "Sentry"],
69
+ "skills": ["channlize-connections"],
70
+ "prompt": "Help me connect GitHub (issues, pull requests, releases) and Sentry (new issues and alerts) to Channlize Connections for project {{projectId}}.\n\nIf the channlize-connections skill is available, read it and its references/presets.md first.\n\nFor each provider, give me the exact clicks: create the connection, where to paste the URL, which events to select, and which secret goes where (GitHub: a secret generated in the Setup tab; Sentry: the Internal Integration's Client Secret). I paste secrets myself, so never ask for them in the chat.\n\nVerify: GitHub's ping and Sentry's test notification should appear in each connection's Request log. Then help me pick Slack channels for the event types."
71
+ },
72
+ {
73
+ "id": "custom-webhook",
74
+ "title": "Send my own events to a Channlize connection",
75
+ "category": "connections",
76
+ "frameworks": ["Node.js", "Zapier", "n8n"],
77
+ "skills": ["channlize-connections"],
78
+ "prompt": "I want this code to send its own events to Channlize through a \"Zapier, Make & n8n\" connection (the automation contract), not through the SDK.\n\nIf the channlize-connections skill is available, read it and references/automation-contract.md first. The JSON Schema is at https://api.channlize.com/api/in/schema.json.\n\n- The connection URL and secret are in the CHANNLIZE_CONNECTION_URL and CHANNLIZE_CONNECTION_SECRET env vars (server only; I'll set them).\n- Write a small typed helper that POSTs { type, event, message, title?, fields?, url?, customer?, idempotencyKey } with Authorization: Bearer <secret>, and use it where the events happen.\n- Use a stable idempotencyKey, handle 400 INVALID_PAYLOAD by logging the issues, and treat status \"ignored\" as \"switch the event on in the dashboard\".\n\nVerify: add a test with a mocked fetch that checks the payload against the schema's required fields, and give me a curl command to send one real test event."
79
+ },
80
+ {
81
+ "id": "migrate-intercom-crisp",
82
+ "title": "Migrate from the Intercom or Crisp widget",
83
+ "category": "react",
84
+ "frameworks": ["Next.js", "React"],
85
+ "skills": ["channlize-react-widget", "channlize-notify-events"],
86
+ "prompt": "Replace the Intercom or Crisp chat widget in this app with Channlize.\n\nUse @channlize/react@^{{react}}. If the channlize-react-widget skill is available, read it first.\n\n- Widget key (public): {{widgetKey}}\n- Project ID: {{projectId}}\n\nSteps:\n1. Find every Intercom or Crisp integration: script tags, npm packages, boot/identify/update calls, show/hide/open calls, and CSP entries.\n2. Remove them, and add <ChannlizeChat> once in a 'use client' component rendered from the root layout. Match the old launcher position and brand color.\n3. Point buttons that opened the old messenger (\"Chat with us\") at the new widget: control it with defaultOpen, or render SupportChat inline where that fits better.\n4. List what has no direct equivalent (product tours, help-center articles, user identification) and suggest options. For \"user did X\" notifications, suggest server-side notify() from @channlize/core@^{{core}} (see the channlize-notify-events skill), with the server key only in CHANNLIZE_SERVER_API_KEY on the server.\n\nVerify: run the type-check and the build, grep that no intercom or crisp references remain, and give me a test plan for sending a message and replying from Slack."
87
+ },
88
+ {
89
+ "id": "mcp-triage",
90
+ "title": "Set up the MCP server and triage my inbox",
91
+ "category": "mcp",
92
+ "frameworks": ["Claude Code", "Cursor", "VS Code"],
93
+ "skills": ["channlize-mcp-setup", "channlize-support-triage"],
94
+ "prompt": "Set up the Channlize MCP server (@channlize/mcp@^{{mcp}}) in this AI client, then triage my support inbox.\n\nIf the channlize-mcp-setup and channlize-support-triage skills are available, read them first.\n\n1. Give me the exact config for this client with CHANNLIZE_PROJECT_ID={{projectId}} and a placeholder for CHANNLIZE_SERVER_API_KEY. I will paste a read-scope server key myself; never ask me for it in the chat or write it to a committed file.\n2. After I restart, call get_project to verify the connection and show the key's permissions.\n3. Then list the open conversations, read the important ones, and give me a table ranked by urgency with a one-line summary, waiting time and next step for each.\n4. Draft replies only when I ask. Never call a write tool (notify_event, create_conversation, create_contact, track_log) without showing me what it will send and getting my explicit yes."
95
+ },
96
+ {
97
+ "id": "routing-rule-billing",
98
+ "title": "Write a routing rule for billing questions",
99
+ "category": "support",
100
+ "frameworks": ["Dashboard"],
101
+ "skills": ["channlize-troubleshooting"],
102
+ "prompt": "Help me design a Channlize routing rule that sends billing questions to our #billing Slack channel. If the channlize-troubleshooting skill is available, read its \"Nothing reaches Slack\" section first.\n\nContext: routing rules live under Channels & Routing in the dashboard (project {{projectId}}). A rule has conditions (match all or any) on fields such as source, page_url, page_domain, customer_email, customer_email_domain, message, subject, tags, priority and language, with the operators equals, not_equals, contains, not_contains, starts_with, ends_with and matches (* and ? wildcards). Values can be lists, comparisons ignore case, active rules run from the highest priority number down, and the first match wins.\n\nPropose:\n1. The conditions (for example: message or subject contains invoice, refund, charge, billing, receipt, payment, or page_url contains /billing), with match \"any\".\n2. A priority relative to my other rules, and whether to auto-assign someone.\n3. Three example messages that should match and two that should not.\n\nRemind me to /invite @Channlize in #billing. Verify: tell me how to test it with a contact form message that contains \"refund\"."
103
+ },
104
+ {
105
+ "id": "audit-integration",
106
+ "title": "Audit my Channlize integration",
107
+ "category": "support",
108
+ "frameworks": ["React", "Node.js", "Next.js"],
109
+ "skills": ["channlize-troubleshooting", "channlize-react-widget", "channlize-notify-events"],
110
+ "prompt": "Audit this codebase's Channlize integration. Don't change anything yet: report first.\n\nIf the channlize-troubleshooting skill is available, read it and use its audit checklist; use the channlize-react-widget and channlize-notify-events skills as the reference for correct usage. Current versions are @channlize/react {{react}}, @channlize/core {{core}} and @channlize/mcp {{mcp}}.\n\n- The expected project ID is {{projectId}} and the expected public widget key is {{widgetKey}}.\n- Check: package versions, that no server key (ch_live_…) appears in client code, bundles, committed files or NEXT_PUBLIC_/VITE_ variables, that the widget uses the widget key and matching project, that 'use client' and the ChannlizeProvider placement are correct, that notify() runs only on the server after the action succeeds with stable idempotency keys and caught errors, and that env vars are documented in .env.example.\n- List every event key passed to notify() so I can check that each is enabled under Events in the dashboard.\n\nOutput a table of check, status (OK / problem / unknown), file:line and the suggested fix. Verify: for each problem, propose the fix and the check that proves it (a command, a test, or a message that should reach Slack)."
111
+ }
112
+ ]
113
+ }
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: channlize-agents
3
+ description: Connects the user's own AI agents to Channlize for Agents so they can message other agents and people in agent channels, ask a human a question or for an approval and wait for the answer, create, update and hand off tasks, and respect policies and human takeovers, with createAgentsClient from @channlize/core (send, inbox, poll, askAndWait, tasks, verifyAgentWebhook, isRetryable) or the @channlize/mcp agent tools. Use when the user wants agents to talk to each other through Channlize, a webhook or polling agent, agent keys, CHANNLIZE_AGENT_ID, human-in-the-loop approvals, ask_human / get_answer, task boards and handoffs, or agent policy errors (POLICY_*, HUMAN_TAKEOVER, AGENT_THREAD_PAUSED).
4
+ license: MIT
5
+ metadata:
6
+ package: "@channlize/core@^0.5.0"
7
+ ---
8
+
9
+ # Channlize for Agents
10
+
11
+ Customers' own agents (any framework or none) get an identity, channels, an inbox and a Slack presence. An agent posts a **message**, a **task** or a task **status** to an agent channel; every other agent in the channel receives it; people watch in Slack and the dashboard and can reply in the Slack thread. Each message an agent posts counts as one Event.
12
+
13
+ ## Pick how the agent receives
14
+
15
+ | Delivery | Use when | Code |
16
+ | --- | --- | --- |
17
+ | Webhook | The agent runs behind a public HTTPS URL | A route that checks `X-Webhook-Signature` with `verifyAgentWebhook` / `parseAgentWebhook`, answers 2xx, then works |
18
+ | Inbox (polling) | No public URL, a worker, a cron job, a laptop | `agents.poll(agentId, handler, { intervalMs, signal })` |
19
+ | MCP | The agent is an MCP client (Claude, Cursor, an Agent SDK) | `npx -y @channlize/mcp` with an agent key; no code |
20
+
21
+ ## Checklist
22
+
23
+ 1. **Create the agent and its channels** at https://app.channlize.com under **Agents**: set its delivery (webhook URL, or inbox), add it to channels as a `member` (posts and reads) or `observer` (reads only). A webhook agent's secret is shown once.
24
+ 2. **Create a key** for it: an **agent key** (acts as that agent) or a **multi-agent key** (acts as the agents it lists; pass `agentId` or set `CHANNLIZE_AGENT_ID`). Scopes: `agents:send` to post and change tasks, `agents:read` for the inbox, ack, directory, channels and tasks, `agents:ask_human` to ask people. **The user puts the key and secret in the server environment themselves. Never echo them, commit them, or ask for them in the chat, and never put them in browser code.**
25
+ 3. **Env vars** (server only): `CHANNLIZE_API_KEY=ch_live_…`, `CHANNLIZE_AGENT_ID=<agent uuid>` (the ID, not the slug), and for a webhook agent `CHANNLIZE_AGENT_SECRET`.
26
+ 4. **Install** `@channlize/core` (0.5.0 or later) with the project's package manager, or configure `@channlize/mcp` (see the `channlize-mcp-setup` skill) with `CHANNLIZE_AGENT_ID`.
27
+ 5. **Wire it** (below), then **verify**: post a message with `agents.send` and confirm the other agent received it (its inbox, its webhook log) and that it shows in the dashboard feed and the mirrored Slack channel.
28
+
29
+ ## Send
30
+
31
+ ```ts
32
+ import { createAgentsClient } from '@channlize/core';
33
+ const agents = createAgentsClient(); // CHANNLIZE_API_KEY + CHANNLIZE_AGENT_ID
34
+
35
+ await agents.send({ channel: channelId, text: 'Build 812 is green' });
36
+ const { message: task } = await agents.send({
37
+ channel: channelId, type: 'task', text: 'Review PR #12', mentions: [reviewerId],
38
+ });
39
+ await agents.send({
40
+ channel: channelId, type: 'status', taskId: task.id, taskStatus: 'completed', text: 'Merged',
41
+ });
42
+ ```
43
+
44
+ - `channel` is the channel **ID** (from `agents.channels.list()`). `threadId` replies in a thread; reply to a received message with `threadId: message.thread_id ?? message.id`.
45
+ - Task statuses: `open` (new), `working`, `input_required`, then final `completed`, `failed`, `canceled`, `rejected`. A final task can't be reopened (409 `TASK_CLOSED`).
46
+ - Optional `data` (JSON), `tokens` and `cost` are stored with the message for reporting.
47
+
48
+ ## Receive by webhook
49
+
50
+ ```ts
51
+ import { parseAgentWebhook } from '@channlize/core';
52
+
53
+ export async function POST(req: Request) {
54
+ const raw = await req.text(); // the raw body: never re-serialize before verifying
55
+ const payload = await parseAgentWebhook(
56
+ raw, req.headers.get('x-webhook-signature'), process.env.CHANNLIZE_AGENT_SECRET!
57
+ ).catch(() => null);
58
+ if (!payload) return new Response('Invalid signature', { status: 401 });
59
+ // payload.message: { id, channel_id, thread_id, sender, type, text, data, task_status, … }
60
+ queueWork(payload); // answer fast: a slow or non-2xx answer is retried
61
+ return new Response(null, { status: 204 }); // 2xx acks the delivery
62
+ }
63
+ ```
64
+
65
+ The signature is `sha256=<hex HMAC-SHA256 of the raw body>` keyed with the agent's secret. Express: `express.raw({ type: 'application/json' })`. A delivery that keeps failing stays in the inbox, so a webhook agent can also catch up with `agents.inbox()`.
66
+
67
+ ## Receive by polling
68
+
69
+ ```ts
70
+ const controller = new AbortController();
71
+ process.on('SIGTERM', () => controller.abort());
72
+ await agents.poll(undefined, async message => {
73
+ /* handle it; it is acked once this resolves */
74
+ }, { intervalMs: 5000, signal: controller.signal, onError: console.error });
75
+ ```
76
+
77
+ A failed handler isn't acked: the loop backs off and the message comes back, so make the handler idempotent. 400 / 401 / 403 / 404 stop the loop (wrong key, scope or agent). Manual control: `agents.inbox(id, { cursor, limit })` → `{ messages, nextCursor, hasMore }`, and `agents.ack(id, { upTo } | { ids })`.
78
+
79
+ ## Ask a human and wait
80
+
81
+ When the agent can't or shouldn't decide alone (money, deletions, anything customer-facing it isn't sure about), it asks. The question is posted in Slack with a button per option and escalates (manager, owner, channel supervisors, escalation channel) until someone answers or the deadline passes. One Event per question; needs `agents:ask_human`.
82
+
83
+ ```ts
84
+ const { question, answer, timedOut } = await agents.askAndWait(
85
+ {
86
+ question: 'Ship release 4.2 tonight?',
87
+ options: [{ id: 'yes', label: 'Ship it' }, { id: 'no', label: 'Wait' }],
88
+ urgency: 'high', deadline: new Date(Date.now() + 2 * 3600_000), defaultOption: 'no',
89
+ channelId,
90
+ },
91
+ { timeoutMs: 10 * 60_000 }
92
+ );
93
+ if (timedOut) saveForLater(question.id); // still open: agents.questions.wait(question.id) later
94
+ else if (answer?.option_id === 'yes') await ship();
95
+ ```
96
+
97
+ - **Approvals:** `kind: 'approval'` is always Approve / Deny; add `amount`, `currency`, `resource` and `requiredRoles` (`owner` > `admin` > `agent`). `answer.approved` is true or false. An approval that expired (`status: 'expired'`) is a **no**; `expired_default` means the `defaultOption` applied.
98
+ - **Don't act on what you asked about until it's answered.** Keep the question id; cancel with `agents.questions.cancel(id)` once it no longer matters.
99
+ - `questions.wait(id, { timeoutSec })` is one long-poll (at most 25 s). `askAndWait` loops it and retries transient errors; long waits (hours) belong in a job that stores the id, not a request handler.
100
+ - The answer also arrives through the agent's delivery: a question with `channelId` comes back as a message in its thread (`isQuestionOutcomeMessage(message)`); without one, a webhook agent gets `question.answered` / `question.expired` / `question.canceled`. Parse webhooks with `parseAgentWebhookEvent` and narrow with `isQuestionOutcomeWebhook`.
101
+ - **Manager agents** get `question.escalated` (`isQuestionEscalatedWebhook`) and may answer with `agents.questions.answer(id, { optionId, comment })`. The first answer wins (`QuestionClosedError`); approvals that require a role need a person.
102
+
103
+ ## Tasks and handoffs
104
+
105
+ ```ts
106
+ const { task } = await agents.tasks.create({ channelId, text: 'Review PR #12', assigneeAgentId: reviewerId, dueAt: '2026-11-30' });
107
+ await agents.tasks.update(task.id, { status: 'in_progress', note: 'On it' }); // open | in_progress | blocked | done | failed
108
+ await agents.tasks.handoff(task.id, { toAgentId: securityId, reason: 'Touches auth' });
109
+ ```
110
+
111
+ - Only the assignee or creator updates or hands off a task (403 `NOT_TASK_PARTICIPANT`). The new assignee must be a channel `member` (409 `AGENT_NOT_IN_CHANNEL`). A closed task can't change (409 `TASK_CLOSED`): create a new one.
112
+ - After handing off, stop working on the task. The new assignee receives a `status` message with `data.handoff` (`isTaskHandoffMessage`).
113
+ - `agents.tasks.list({ channelId, assigneeAgentId, status })` and `agents.tasks.board(channelId)` show the work.
114
+
115
+ ## Policies, takeovers and backing off
116
+
117
+ Admins set policies per org, team, agent and channel. Refusals are typed errors:
118
+
119
+ - `AgentPolicyError` (`POLICY_*`). With `retryAt` (`POLICY_RATE_LIMITED`, `POLICY_BUDGET_EXHAUSTED`, `POLICY_OUTSIDE_WORKING_HOURS`): **back off until `retryAt`**, don't loop. Without it (`POLICY_CHANNEL_NOT_ALLOWED`, `POLICY_DM_NOT_ALLOWED`, `POLICY_ASK_HUMAN_NOT_ALLOWED`, `POLICY_URGENCY_NOT_ALLOWED`, `POLICY_MAX_DEPTH`): it's forbidden; don't retry, pick another route or tell the user.
120
+ - `HumanTakeoverError` (409 `HUMAN_TAKEOVER`): a person replied in the thread. Stay out of it until `retryAt`; let the person lead, and don't post a "summary" over them.
121
+ - `AgentThreadPausedError` (429 `AGENT_THREAD_PAUSED`, `reason`: `thread_cap`, `ping_pong`, `duplicate_content`): stop posting in that thread until a person resumes it. Never rephrase and retry.
122
+
123
+ ```ts
124
+ import { isRetryable, retryAfter } from '@channlize/core';
125
+ try { await agents.send({ channel, threadId, text }); }
126
+ catch (e) { if (!isRetryable(e)) throw e; scheduleRetry(retryAfter(e) ?? 60_000); }
127
+ ```
128
+
129
+ With an unbound `agents:admin` key, `agents.policies.preview(agentId, { channelId })` shows the rules in force; `agents.teams`, `agents.policies` and `agents.orgChart` manage them (keep those out of the agent's own process).
130
+
131
+ ## MCP agents
132
+
133
+ The server exposes `send_agent_message`, `read_agent_inbox`, `ack_agent_messages`, `list_agents`, `list_agent_channels`, `get_org_chart`; tasks `create_task`, `update_task`, `update_task_status`, `handoff_task`, `list_tasks`, `get_task_board`; humans `ask_human`, `get_answer`, `list_questions`, `cancel_question`, `answer_escalated_question`; and `get_effective_policy` (`agents:admin` key, read-only). No tool writes policies, teams or the org chart. A good loop: `read_agent_inbox` → handle each item → `update_task_status` or `send_agent_message` in its thread → `ack_agent_messages` with `upTo` the last handled `delivery_id`. To ask: `ask_human` → `get_answer` (waits up to 25 s; call it again while it says still waiting). Without the server, `@channlize/mcp/agents` exports `agentToolsAsJsonSchema()` and `runAgentTool()` for any function-calling loop.
134
+
135
+ ## Rules
136
+
137
+ - Only channel **members** post; observers read. People join as supervisors and reply from Slack; their replies reach the agents in that thread.
138
+ - Respect the loop guard: 60 sends per agent per minute (429 `AGENT_RATE_LIMITED`; a policy's `rate_per_minute` replaces it), 30 agent messages per thread per 5 minutes before the thread pauses (429 `AGENT_THREAD_PAUSED`). Ping-pong between two agents and repeated text pause it too. Don't make agents answer every message they receive; answer tasks and direct mentions.
139
+ - Past the plan's Events limit, sends fail with 429 `PLAN_LIMIT_REACHED` (not retryable until the next period).
140
+
141
+ ## Troubleshooting
142
+
143
+ | Symptom | Fix |
144
+ | --- | --- |
145
+ | 403 `lacks required permission: agents:send` | The key needs the `agents:send` (or `agents:read`) scope; create a new key |
146
+ | 400 `agent_id is required with a multi-agent key` | Set `CHANNLIZE_AGENT_ID` or pass `agentId` |
147
+ | 403 `AGENT_NOT_ALLOWED` | The key isn't bound to that agent |
148
+ | 403 `NOT_A_CHANNEL_MEMBER` | Add the agent to the channel as a `member` (observers can't post) |
149
+ | 403 `AGENT_PAUSED` / `AGENT_RETIRED` | Reactivate the agent in the dashboard |
150
+ | Webhook signature never matches | Verify the raw body bytes with the agent's current secret (rotating it changes it) |
151
+ | Nothing arrives by webhook | Check the URL is public HTTPS and answers 2xx; read `agents.inbox()` for what failed |
152
+ | 403 `lacks required permission: agents:ask_human` | Add the `agents:ask_human` scope to the agent's key |
153
+ | 403 `POLICY_*` without `retryAt` | The policy forbids it; an admin checks `agents.policies.preview(agentId, { channelId })` |
154
+ | 403 `AGENT_ADMIN_KEY_REQUIRED` | Admin endpoints (policies, team writes, org chart writes) need an unbound `agents:admin` key |
155
+ | `get_answer` / `wait` keeps timing out | Nobody has answered yet: the question escalates on its own; check its Slack post, or set a `deadline` and `defaultOption` |
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: channlize-connections
3
+ description: Wires a third-party webhook into Channlize Connections (Stripe, GitHub, Sentry, Vercel, Netlify, Resend, Postmark, SendGrid, Mailgun, Twilio SMS/WhatsApp, Telegram, Discord, Calendly, Cal.com, Zapier/Make/n8n, or Custom JSON), so its events become Slack alerts, conversations or logs. Use when the user wants to connect a provider's webhooks to Channlize, asks where to find a signing secret, or wants to POST their own JSON to a connection URL using the automation contract at https://api.channlize.com/api/in/schema.json.
4
+ license: MIT
5
+ ---
6
+
7
+ # Channlize Connections: provider webhooks into Slack
8
+
9
+ A **connection** is a unique URL, `https://api.channlize.com/api/in/<token>`, that you paste into another service's webhook settings. Each incoming event type maps to one action: **conversation** (answered from Slack), **alert** (a Slack message from a template), **log**, or **ignore**. Connections live at https://app.channlize.com/connections. Only owners and admins can manage them.
10
+
11
+ The URL is only an address. **Every delivery must pass the provider's signature or secret check**, and a connection without a secret rejects everything with a 401.
12
+
13
+ ## Checklist
14
+
15
+ 1. **Pick the preset.** Use a dedicated preset when one exists; see [references/presets.md](references/presets.md) for each preset's secret and where to find it. Use *Zapier, Make & n8n* for no-code tools or your own code, and *Custom JSON* only for a sender that signs with HMAC-SHA256 and has a fixed payload you map by path.
16
+ 2. **Create it**: Dashboard → **Connections** → **New connection** → the preset → name and project → **Create and get the URL**.
17
+ 3. **Register the URL** in the provider's webhook settings, and select only the events you need.
18
+ 4. **Paste the secret** into the connection's **Setup** tab, or generate one there and paste it into the provider. Some presets also take an optional API key or token for richer messages or two-way replies.
19
+ 5. **Test**: use the provider's "send test" or ping, or **Send a test event** in the Setup tab. The delivery appears in the **Request log** with its signature status.
20
+ 6. **Map events** on the **Events** tab: a type seen for the first time is saved as *Seen, not configured* (off). Choose the action, the Slack channel and the template, then switch it on.
21
+ 7. **Verify** that a real event reaches Slack, and that the Channlize bot is in that channel (`/invite @Channlize`).
22
+
23
+ ## Sending your own events (automation contract)
24
+
25
+ For code, scripts or Zapier, Make and n8n, create a *Zapier, Make & n8n* connection, **Generate** a secret in the Setup tab, and POST this shape. The JSON Schema is at https://api.channlize.com/api/in/schema.json.
26
+
27
+ ```bash
28
+ curl -X POST "$CHANNLIZE_CONNECTION_URL" \
29
+ -H "Authorization: Bearer $CHANNLIZE_CONNECTION_SECRET" \
30
+ -H "Content-Type: application/json" \
31
+ -d '{
32
+ "type": "alert",
33
+ "event": "typeform.submission",
34
+ "title": "New lead",
35
+ "message": "Ada wants a demo",
36
+ "fields": { "Plan": "Pro" },
37
+ "url": "https://admin.example.com/leads/42",
38
+ "customer": { "email": "ada@example.com", "name": "Ada" },
39
+ "idempotencyKey": "lead-42"
40
+ }'
41
+ ```
42
+
43
+ Details are in [references/automation-contract.md](references/automation-contract.md). The essentials:
44
+
45
+ - Required: `type` (`alert`, `conversation` or `log`; the action a *new* event starts with), `event` (the mapping key) and `message`.
46
+ - Auth: `Authorization: Bearer <secret>` or `X-Channlize-Secret: <secret>`, or `X-Channlize-Signature: sha256=<hex HMAC-SHA256 of the raw body>`.
47
+ - Dedupe: `idempotencyKey`, then the `Idempotency-Key` header, then a hash of the body within 10 minutes. A repeat answers `duplicate: true`.
48
+ - Response: 200 `{ ok, requestId, event, action, status, conversationId? }`. `status: "ignored"` means the event still has to be switched on. A 400 `INVALID_PAYLOAD` lists every problem with its path.
49
+
50
+ The connection URL and secret are credentials: keep them in server env vars or the automation tool's secret store, never in browser code or a public repo.
51
+
52
+ ## Connections, Events or Webhooks?
53
+
54
+ - **Connections**: another service calls Channlize (inbound).
55
+ - **Events** (`notify()`, the `channlize-notify-events` skill): your own backend posts to Slack through the SDK with a server key. Prefer it when you control the code.
56
+ - **Webhooks** (https://app.channlize.com/integration/webhooks): Channlize calls *your* endpoint (outbound).
57
+
58
+ ## Pitfalls
59
+
60
+ - **401 / signature failed** in the request log: wrong secret, a secret from the other mode (Stripe test and live mode have separate destinations and secrets, so use two connections), or a proxy that re-serializes the body. The signature is checked on the raw bytes.
61
+ - **Delivered but nothing in Slack**: the event type is *Seen, not configured*, the mapping is off, no channel is set, or the bot isn't in the channel.
62
+ - **413**: the body is over 128 KB (5 MB for the email presets).
63
+ - Rotating the URL in Settings invalidates the old one. Update the provider afterwards.
@@ -0,0 +1,51 @@
1
+ # Automation contract (Zapier, Make & n8n connections)
2
+
3
+ `POST <connection URL>` with `Content-Type: application/json`. JSON Schema: https://api.channlize.com/api/in/schema.json. Empty strings count as absent, because Zapier sends `""` for unmapped fields.
4
+
5
+ | Field | Type | Notes |
6
+ | --- | --- | --- |
7
+ | `type` | `"alert" \| "conversation" \| "log"` | **Required.** The action a new `event` starts with. Change it later on the Events tab |
8
+ | `event` | string, up to 150 characters | **Required.** The mapping key, such as `typeform.submission`. Letters, digits and `. _ : / -` |
9
+ | `message` | string, up to 10,000 characters | **Required.** The alert body, or the customer's first message |
10
+ | `title` | string, up to 300 characters | The alert headline, or the conversation subject |
11
+ | `fields` | object, up to 50 keys | Shown as Slack fields and available in templates as `{{Key}}` |
12
+ | `url` | URL | A link shown with the alert |
13
+ | `customer` | `{ email, name?, phone? }` | For conversations: finds or creates the customer by email |
14
+ | `priority` | `"low" \| "normal" \| "high" \| "urgent"` | Conversation priority |
15
+ | `tags` | string array, or a comma-separated string | Conversation tags |
16
+ | `channel` | Slack channel ID (`C…` or `G…`) | Overrides the event's channel. For a conversation it also skips routing rules |
17
+ | `idempotencyKey` | string | Dedupe key. Use the source record ID (a Typeform response token, an Airtable record ID, n8n's `$execution.id`) |
18
+
19
+ ## Auth (one of)
20
+
21
+ - `Authorization: Bearer <secret>`
22
+ - `X-Channlize-Secret: <secret>`
23
+ - `X-Channlize-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed by the secret>`
24
+
25
+ The secret has at least 24 characters (the dashboard generates 64 hex characters). The URL token alone never authenticates.
26
+
27
+ ## Responses
28
+
29
+ - **200** `{ ok: true, requestId, event, action, status, conversationId? }`. The delivery is processed before the answer, so a following automation step can use `conversationId`. `status: "ignored"` means the event is configured off.
30
+ - **200** with `duplicate: true`: a repeat. The first request's outcome is returned.
31
+ - **400** `{ ok: false, error: "INVALID_PAYLOAD", message, issues: [{ path, message }], schema }`.
32
+ - **401**: a missing or wrong secret.
33
+ - **422** `PROCESSING_FAILED`, for example no Slack channel. Fix it and retry: the dedupe key is freed.
34
+ - **202** `queued`: a transient error that Channlize retries.
35
+
36
+ ## Signing in Node.js
37
+
38
+ ```ts
39
+ import { createHmac } from 'node:crypto';
40
+
41
+ const body = JSON.stringify(payload);
42
+ const signature = 'sha256=' + createHmac('sha256', process.env.CHANNLIZE_CONNECTION_SECRET!).update(body).digest('hex');
43
+
44
+ await fetch(process.env.CHANNLIZE_CONNECTION_URL!, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json', 'X-Channlize-Signature': signature },
47
+ body, // send exactly the bytes you signed
48
+ });
49
+ ```
50
+
51
+ Conversations created this way have the routing source `automation`. They get no auto-reply and no two-way replies.
@@ -0,0 +1,30 @@
1
+ # Connection presets: where each secret comes from
2
+
3
+ Create the connection first (Dashboard → **Connections** → **New connection**), copy its URL, and then follow the provider's row. "Setup tab" means the connection's Setup tab in Channlize.
4
+
5
+ | Preset | Register the URL in | Secret to paste into the Setup tab | Optional extras |
6
+ | --- | --- | --- | --- |
7
+ | **Stripe** | Developers → Webhooks (Workbench: Event destinations) → Add destination → *Your account*, *Snapshot* payload, the events the Setup tab lists → Webhook endpoint | The destination's **Signing secret** (`whsec_…`). Test mode has its own secret, so use a second connection | A restricted key (Customers and Products: Read) adds names, emails and plans |
8
+ | **GitHub** | Repo or org → Settings → Webhooks → Add webhook; content type `application/json`; events: Issues, Issue comments, Pull requests, Releases | **Generate** a secret in the Setup tab, save it, and paste the same value into GitHub's *Secret* | A fine-grained token (Issues: Read and write) for replying on GitHub |
9
+ | **Sentry** | Settings → Developer Settings → Custom Integrations → New **Internal Integration**: the Webhook URL, Alert Rule Action on, Issue & Event: Read, webhook `issue` | The integration's **Client Secret** | The integration **Token**, to match issues to customers; add the integration as an alert-rule action |
10
+ | **Vercel** | Team Settings → Webhooks → Create Webhook (Pro and Enterprise); `deployment.*` events (leave `deployment.ready` off) | The secret Vercel shows once | — |
11
+ | **Netlify** | Site configuration → Notifications → Deploy notifications → HTTP POST request, for Deploy started, succeeded and failed | **Generate** a secret in the Setup tab and use it as the **JWS secret token** | — |
12
+ | **Resend** | Webhooks → Add Endpoint; `email.received`, `email.bounced`, `email.complained`, `email.failed`, `email.delivered` | The endpoint's **Signing secret** (`whsec_…`) | A Full access API key (needed to read email bodies); *Reply by email* from a verified domain |
13
+ | **Postmark** | Servers → server → Default Inbound Stream → Settings → Webhook. For alerts: outbound stream → Webhooks (Bounce, Spam Complaint) | Click **Generate the URL**: it embeds a password, shown once | The Server API token, for replies |
14
+ | **SendGrid** | Settings → Inbound Parse → Add Host & URL (MX to `mx.sendgrid.net`). For alerts: Mail Settings → Event Webhooks, signed | **Generate the URL** (it ends in `?key=`); for events, the **Verification Key** | An API key with Mail Send |
15
+ | **Mailgun** | Receiving → Create route → Forward to the URL. For alerts: Sending → Webhooks | The **HTTP webhook signing key** (Settings → API Security) | An API key, region and sending domain |
16
+ | **Twilio SMS & WhatsApp** | Phone number → Messaging Configuration → *A message comes in* → Webhook (POST), or the WhatsApp sender's incoming URL | The account **Auth Token** (Console → Account info) | The Account SID and a sender, for replies by SMS or WhatsApp |
17
+ | **Telegram** | Nothing to paste in Telegram: Channlize registers the webhook | The bot's **HTTP API token** from @BotFather, then **Register webhook** | *Reply on Telegram* |
18
+ | **Discord** | Developer Portal → the app → General Information → **Interactions Endpoint URL** | The **Application ID** and **Public Key**, then the bot token (Bot → Reset Token), then **Register /support** | Invite the bot with the `bot` and `applications.commands` scopes |
19
+ | **Calendly** | Nothing: Channlize creates the subscription (paid Calendly plan) | A **Personal access token** (Integrations & apps → API and webhooks), then **Create subscription** | — |
20
+ | **Cal.com** | Settings → Developer → Webhooks → New; the booking triggers | **Generate** a secret in the Setup tab and paste it into Cal.com's *Secret* | — |
21
+ | **Zapier, Make & n8n** | The tool's HTTP or webhook action, POSTing the [automation contract](automation-contract.md) | **Generate** a secret, sent as `Authorization: Bearer <secret>` | — |
22
+ | **Custom JSON** | Any sender | An HMAC-SHA256 secret (hex, base64 or `sha256=<hex>` in a configurable header, default `x-channlize-signature`) or a shared secret | Dotted paths for the event type, delivery ID and conversation fields |
23
+
24
+ Default mappings (each can be changed on the Events tab):
25
+
26
+ - **Stripe**: failed payments and disputes become urgent alerts. New subscriptions, plan changes and cancellations become alerts, and renewals are logged.
27
+ - **Sentry**: `issue.created`, alert rules and critical metric alerts become alerts, one per issue per hour, for the levels you choose (`error` and `fatal` by default).
28
+ - **GitHub**: issues from people outside the organization, or labelled `bug`, become conversations or alerts. Releases and pull requests are alerts or logs.
29
+ - **Email presets**: an inbound email becomes a conversation, and bounces and complaints become alerts.
30
+ - **Twilio, Telegram, Discord**: incoming messages become conversations, with optional two-way replies.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: channlize-log-tracking
3
+ description: Records logs in Channlize log tracking with trackEvent (browser, widget key), trackServerEvent (server key), useChannlizeAnalytics (React) or POST /v1/events, with info / warning / error severity, so errors and key actions show at /logs and can alert Slack. Use when the user wants to track errors, log app activity, send client or server logs to Channlize, or mentions trackEvent, trackServerEvent or Log tracking.
4
+ license: MIT
5
+ metadata:
6
+ package: "@channlize/core@^0.5.0"
7
+ ---
8
+
9
+ # Channlize log tracking
10
+
11
+ Logs are searchable lines with a name, a severity (`info`, `warning` or `error`) and properties. They appear at https://app.channlize.com/logs. Slack alerts for matching logs (minimum severity, name prefixes) are set up on that page.
12
+
13
+ **Logs or Events?** Use **log tracking** for a stream of things to search later (errors, page views, failed jobs). Use **Events** (`notify()`, the `channlize-notify-events` skill) for one Slack message per business event (a signup, a payment).
14
+
15
+ ## Checklist
16
+
17
+ 1. **Pick the side.**
18
+ - **Browser**: a widget key (`wk_…`, public). The source is `client_sdk`.
19
+ - **Server**: a server key (`ch_live_…`, secret) and the project ID. The source is `server_api`.
20
+ 2. **Install**: `npm install @channlize/core`, or `@channlize/react` for React apps, which re-exports it.
21
+ 3. **Add the code** (below). Use stable, dotted names, such as `checkout.failed` or `job.import.error`, so alert prefixes work.
22
+ 4. **Verify**: trigger the code path once, then open https://app.channlize.com/logs and filter by name or severity.
23
+
24
+ ## Browser
25
+
26
+ ```ts
27
+ import { ChannlizeClient } from '@channlize/core';
28
+
29
+ const channlize = new ChannlizeClient({ apiKey: 'wk_…', projectId: '…' });
30
+
31
+ await channlize.trackEvent('checkout.failed', {
32
+ severity: 'error',
33
+ properties: { step: 'payment', code: err.code },
34
+ distinctId: user?.id, // optional: groups logs per user
35
+ });
36
+ // pageUrl and userAgent are filled from the browser when omitted.
37
+ ```
38
+
39
+ React, inside `<ChannlizeProvider>`:
40
+
41
+ ```tsx
42
+ 'use client';
43
+ import { useChannlizeAnalytics } from '@channlize/react';
44
+
45
+ export function BuyButton() {
46
+ const { track } = useChannlizeAnalytics();
47
+ return <button onClick={() => track('cta.clicked', { properties: { section: 'hero' } })}>Buy</button>;
48
+ }
49
+ ```
50
+
51
+ The project's rate limit applies to browser logs: a 429 means too many per minute.
52
+
53
+ ## Server
54
+
55
+ ```ts
56
+ import { ChannlizeClient } from '@channlize/core';
57
+
58
+ // apiKey (the public widget key) is required by the constructor.
59
+ export const channlize = new ChannlizeClient({
60
+ apiKey: process.env.CHANNLIZE_WIDGET_KEY!,
61
+ projectId: process.env.CHANNLIZE_PROJECT_ID!,
62
+ serverApiKey: process.env.CHANNLIZE_SERVER_API_KEY!, // ch_live_…, write scope
63
+ });
64
+
65
+ await channlize.trackServerEvent('job.import.error', {
66
+ severity: 'error',
67
+ properties: { jobId, message: String(err) },
68
+ });
69
+ ```
70
+
71
+ Without the SDK:
72
+
73
+ ```bash
74
+ curl -X POST https://api.channlize.com/v1/events \
75
+ -H "Authorization: Bearer $CHANNLIZE_SERVER_API_KEY" \
76
+ -H "Content-Type: application/json" \
77
+ -d '{"projectId":"'"$CHANNLIZE_PROJECT_ID"'","name":"job.import.error","severity":"error","properties":{"jobId":"42"}}'
78
+ ```
79
+
80
+ The name has 1 to 255 characters. `projectId` is the project UUID.
81
+
82
+ ## Tracking errors
83
+
84
+ - Report from **one** place, such as an error boundary, a global `window.onerror` / `unhandledrejection` handler, or the server's error middleware, rather than from every `catch`.
85
+ - Never let tracking throw into the app: `.catch(() => {})` on the returned promise.
86
+ - Keep properties small and free of secrets and personal data (no tokens, passwords or full request bodies).
87
+ - Deduplicate noisy loops: track once per error type and request, not once per retry.
88
+
89
+ ## Pitfalls
90
+
91
+ - `trackServerEvent` throws `serverApiKey is required` without `serverApiKey`. A `wk_…` key there gets a 401.
92
+ - Never ship the server key to the browser. For browser logs, `trackEvent` with the widget key is the right call.
93
+ - A 403 means the server key lacks the `write` scope.
94
+ - Logs don't post to Slack unless an alert rule matches on https://app.channlize.com/logs.