@zibby/workflow-templates 0.3.0 → 0.4.2
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/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/browser-test-automation/package.json +1 -0
- package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/code-analysis/nodes/generate-code-node.js +12 -2
- package/index.js +235 -0
- package/notify-lark/README.md +88 -0
- package/notify-lark/graph.mjs +43 -0
- package/notify-lark/icon.png +0 -0
- package/notify-lark/nodes/notify-lark-node.js +303 -0
- package/notify-lark/package.json +18 -0
- package/notify-lark/state.js +85 -0
- package/notify-notion/README.md +71 -0
- package/notify-notion/graph.mjs +64 -0
- package/notify-notion/icon.png +0 -0
- package/notify-notion/nodes/notify-notion-node.js +342 -0
- package/notify-notion/package.json +19 -0
- package/notify-notion/state.js +110 -0
- package/notify-slack/README.md +94 -0
- package/notify-slack/graph.mjs +51 -0
- package/notify-slack/icon.png +0 -0
- package/notify-slack/nodes/notify-slack-node.js +268 -0
- package/notify-slack/package.json +18 -0
- package/notify-slack/state.js +112 -0
- package/package.json +17 -3
- package/sentry-triage/graph.mjs +81 -0
- package/sentry-triage/icon.png +0 -0
- package/sentry-triage/nodes/classify-node.js +38 -0
- package/sentry-triage/nodes/dispatch-alerts-node.js +191 -0
- package/sentry-triage/nodes/fetch-issues-node.js +52 -0
- package/sentry-triage/nodes/filter-noise-node.js +112 -0
- package/sentry-triage/package.json +18 -0
- package/sentry-triage/prompts/classify.md +76 -0
- package/sentry-triage/prompts/fetch-issues.md +66 -0
- package/sentry-triage/state.js +134 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify-lark node — deterministic, no LLM.
|
|
3
|
+
*
|
|
4
|
+
* Sends a Lark Interactive Card via /open-apis/im/v1/messages. Same
|
|
5
|
+
* design philosophy as notify-slack: parent makes all the decisions
|
|
6
|
+
* (severity, title, body, who to mention), this node just renders the
|
|
7
|
+
* card and POSTs it.
|
|
8
|
+
*
|
|
9
|
+
* Auth differs from Slack:
|
|
10
|
+
* - resolveIntegrationToken('lark') returns { appId, appSecret, host }
|
|
11
|
+
* not a ready-to-use bearer token.
|
|
12
|
+
* - We exchange app credentials for a `tenant_access_token` via
|
|
13
|
+
* /open-apis/auth/v3/tenant_access_token/internal, then use that as
|
|
14
|
+
* Bearer auth on the message send. Same flow @zibby/skills/lark.js
|
|
15
|
+
* uses; we reimplement here so the node stays decoupled from the
|
|
16
|
+
* MCP-tool path (which only the LLM nodes use).
|
|
17
|
+
*
|
|
18
|
+
* The token has ~2h TTL — we cache per-process so a parent dispatching
|
|
19
|
+
* many alerts only pays the auth round-trip once.
|
|
20
|
+
*
|
|
21
|
+
* Severity → Lark header template (named themes, not hex):
|
|
22
|
+
* low → grey
|
|
23
|
+
* medium → yellow
|
|
24
|
+
* high → orange
|
|
25
|
+
* critical → red
|
|
26
|
+
*
|
|
27
|
+
* The Lark API quirk: `content` on the message body must be a
|
|
28
|
+
* JSON-encoded STRING (not the object itself). Easy to miss.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
import { SKILLS } from '@zibby/core';
|
|
33
|
+
import { resolveIntegrationToken } from '@zibby/core/backend-client.js';
|
|
34
|
+
// Universal renderer — see notify-slack-node.js for the design note;
|
|
35
|
+
// when state.report is present the legacy buildLarkCard path is bypassed.
|
|
36
|
+
import { reportToLarkCard } from '@zibby/skills/report';
|
|
37
|
+
|
|
38
|
+
const SEVERITY_TEMPLATE = Object.freeze({
|
|
39
|
+
low: 'grey',
|
|
40
|
+
medium: 'yellow',
|
|
41
|
+
high: 'orange',
|
|
42
|
+
critical: 'red',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const SEVERITY_EMOJI = Object.freeze({
|
|
46
|
+
low: '⚪',
|
|
47
|
+
medium: '🟡',
|
|
48
|
+
high: '🟠',
|
|
49
|
+
critical: '🚨',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const NotifyLarkOutputSchema = z.object({
|
|
53
|
+
delivered: z.boolean(),
|
|
54
|
+
receiveId: z.string(),
|
|
55
|
+
receiveIdType: z.string(),
|
|
56
|
+
messageId: z.string().optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the Lark Interactive Card body. Returns the card OBJECT (not
|
|
61
|
+
* the JSON-stringified form the API expects — callers stringify it).
|
|
62
|
+
* Exposed for unit tests so we can pin the rendered shape.
|
|
63
|
+
*/
|
|
64
|
+
export function buildLarkCard(input) {
|
|
65
|
+
const {
|
|
66
|
+
severity = 'medium',
|
|
67
|
+
title,
|
|
68
|
+
body,
|
|
69
|
+
sentryLink,
|
|
70
|
+
affectedUsers,
|
|
71
|
+
events,
|
|
72
|
+
release,
|
|
73
|
+
firstSeen,
|
|
74
|
+
codeSnippet,
|
|
75
|
+
actionUrl,
|
|
76
|
+
actionLabel,
|
|
77
|
+
mentions,
|
|
78
|
+
} = input;
|
|
79
|
+
|
|
80
|
+
const elements = [];
|
|
81
|
+
|
|
82
|
+
// Metadata grid — 2-column layout via `is_short: true` on each field.
|
|
83
|
+
const fields = [];
|
|
84
|
+
if (typeof affectedUsers === 'number') {
|
|
85
|
+
fields.push({
|
|
86
|
+
is_short: true,
|
|
87
|
+
text: { tag: 'lark_md', content: `**Users affected**\n${affectedUsers}` },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (typeof events === 'number') {
|
|
91
|
+
fields.push({
|
|
92
|
+
is_short: true,
|
|
93
|
+
text: { tag: 'lark_md', content: `**Events**\n${events}` },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (release) {
|
|
97
|
+
fields.push({
|
|
98
|
+
is_short: true,
|
|
99
|
+
text: { tag: 'lark_md', content: `**Release**\n\`${release}\`` },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (firstSeen) {
|
|
103
|
+
fields.push({
|
|
104
|
+
is_short: true,
|
|
105
|
+
text: { tag: 'lark_md', content: `**First seen**\n${firstSeen}` },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (fields.length > 0) {
|
|
109
|
+
elements.push({ tag: 'div', fields });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Body — rendered as lark_md (Lark's markdown variant).
|
|
113
|
+
if (body && body.trim().length > 0) {
|
|
114
|
+
elements.push({
|
|
115
|
+
tag: 'div',
|
|
116
|
+
text: { tag: 'lark_md', content: body },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Code snippet — fenced triple-backtick block.
|
|
121
|
+
if (codeSnippet) {
|
|
122
|
+
elements.push({
|
|
123
|
+
tag: 'div',
|
|
124
|
+
text: { tag: 'lark_md', content: '```\n' + codeSnippet + '\n```' },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Action buttons — same logic as Slack: primary "Open in Sentry"
|
|
129
|
+
// when sentryLink set, plus secondary actionUrl/Label pair if both
|
|
130
|
+
// supplied.
|
|
131
|
+
const actions = [];
|
|
132
|
+
if (sentryLink) {
|
|
133
|
+
actions.push({
|
|
134
|
+
tag: 'button',
|
|
135
|
+
text: { tag: 'plain_text', content: 'Open in Sentry' },
|
|
136
|
+
type: 'primary',
|
|
137
|
+
url: sentryLink,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (actionUrl && actionLabel) {
|
|
141
|
+
actions.push({
|
|
142
|
+
tag: 'button',
|
|
143
|
+
text: { tag: 'plain_text', content: actionLabel.slice(0, 40) },
|
|
144
|
+
type: 'default',
|
|
145
|
+
url: actionUrl,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (actions.length > 0) {
|
|
149
|
+
elements.push({ tag: 'action', actions });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Mentions — small-text note at bottom. Caller pre-formatted as
|
|
153
|
+
// `<at user_id="ou_xxx">@name</at>` strings.
|
|
154
|
+
if (Array.isArray(mentions) && mentions.length > 0) {
|
|
155
|
+
elements.push({
|
|
156
|
+
tag: 'note',
|
|
157
|
+
elements: [{ tag: 'lark_md', content: mentions.join(' ') }],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const emoji = SEVERITY_EMOJI[severity] || SEVERITY_EMOJI.medium;
|
|
162
|
+
const template = SEVERITY_TEMPLATE[severity] || SEVERITY_TEMPLATE.medium;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
config: { wide_screen_mode: true },
|
|
166
|
+
header: {
|
|
167
|
+
template,
|
|
168
|
+
title: {
|
|
169
|
+
tag: 'plain_text',
|
|
170
|
+
content: `${emoji} ${severity.toUpperCase()} ${title}`.slice(0, 150),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
elements,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Infer the Lark receive_id_type query param from the id prefix.
|
|
179
|
+
* Mirrors @zibby/skills/lark.js's inferReceiveIdType — duplicated to
|
|
180
|
+
* keep the node self-contained.
|
|
181
|
+
*/
|
|
182
|
+
export function inferReceiveIdType(id) {
|
|
183
|
+
if (!id || typeof id !== 'string') return 'chat_id';
|
|
184
|
+
if (id.startsWith('oc_')) return 'chat_id';
|
|
185
|
+
if (id.startsWith('ou_')) return 'open_id';
|
|
186
|
+
if (id.startsWith('on_')) return 'union_id';
|
|
187
|
+
if (id.startsWith('cli_')) return 'app_id';
|
|
188
|
+
if (id.includes('@')) return 'email';
|
|
189
|
+
return 'chat_id';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Per-process cache. Tenant access tokens last ~2h; we cache 100m to
|
|
193
|
+
// avoid the boundary case where a 119-min-old token is rejected mid-call.
|
|
194
|
+
const TOKEN_TTL_MS = 100 * 60 * 1000;
|
|
195
|
+
let _tenantTokenCache = null;
|
|
196
|
+
|
|
197
|
+
async function getTenantAccessToken() {
|
|
198
|
+
const { appId, appSecret, host } = await resolveIntegrationToken('lark');
|
|
199
|
+
if (
|
|
200
|
+
_tenantTokenCache
|
|
201
|
+
&& _tenantTokenCache.appId === appId
|
|
202
|
+
&& _tenantTokenCache.expiresAt > Date.now()
|
|
203
|
+
) {
|
|
204
|
+
return { token: _tenantTokenCache.token, host };
|
|
205
|
+
}
|
|
206
|
+
const url = `${host}/open-apis/auth/v3/tenant_access_token/internal`;
|
|
207
|
+
const res = await fetch(url, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
211
|
+
});
|
|
212
|
+
const data = await res.json().catch(() => ({}));
|
|
213
|
+
if (data.code !== 0) {
|
|
214
|
+
const e = new Error(`Lark tenant_access_token failed: ${data.msg || data.code}`);
|
|
215
|
+
e.code = data.code;
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
_tenantTokenCache = {
|
|
219
|
+
appId,
|
|
220
|
+
token: data.tenant_access_token,
|
|
221
|
+
expiresAt: Date.now() + TOKEN_TTL_MS,
|
|
222
|
+
};
|
|
223
|
+
return { token: data.tenant_access_token, host };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Test hook — resets the per-process token cache. */
|
|
227
|
+
export function _resetTokenCache() { _tenantTokenCache = null; }
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Send the Lark card. Exposed for unit tests so we can stub fetch +
|
|
231
|
+
* assert payload shape without going through token resolution.
|
|
232
|
+
*/
|
|
233
|
+
export async function postToLark({ token, host, receiveId, receiveIdType, card }) {
|
|
234
|
+
const url = `${host}/open-apis/im/v1/messages?receive_id_type=${encodeURIComponent(receiveIdType)}`;
|
|
235
|
+
const res = await fetch(url, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${token}`,
|
|
239
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
240
|
+
},
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
receive_id: receiveId,
|
|
243
|
+
msg_type: 'interactive',
|
|
244
|
+
// Lark wants `content` as a JSON-encoded STRING, not an object.
|
|
245
|
+
content: JSON.stringify(card),
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
const data = await res.json().catch(() => ({}));
|
|
249
|
+
if (data.code !== 0) {
|
|
250
|
+
const e = new Error(`Lark send message failed: ${data.msg || `code ${data.code} / http ${res.status}`}`);
|
|
251
|
+
e.code = data.code || 'unknown';
|
|
252
|
+
e.status = res.status;
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
return { messageId: data.data?.message_id };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const notifyLarkNode = {
|
|
259
|
+
name: 'notify_lark',
|
|
260
|
+
// Declares Lark as a hard requirement for marketplace gating (same
|
|
261
|
+
// pattern as notify-slack — see that file's comment for rationale).
|
|
262
|
+
skills: [SKILLS.LARK],
|
|
263
|
+
outputSchema: NotifyLarkOutputSchema,
|
|
264
|
+
timeout: 15 * 1000,
|
|
265
|
+
execute: async (context) => {
|
|
266
|
+
const state = (context?.state && typeof context.state.getAll === 'function')
|
|
267
|
+
? context.state.getAll()
|
|
268
|
+
: context;
|
|
269
|
+
|
|
270
|
+
const receiveId = state.receiveId;
|
|
271
|
+
if (!receiveId) {
|
|
272
|
+
throw new Error('notify-lark: input.receiveId is required');
|
|
273
|
+
}
|
|
274
|
+
if (!state.report && !state.title) {
|
|
275
|
+
throw new Error('notify-lark: input.title is required when input.report is absent');
|
|
276
|
+
}
|
|
277
|
+
const receiveIdType = inferReceiveIdType(receiveId);
|
|
278
|
+
|
|
279
|
+
const { token, host } = await getTenantAccessToken();
|
|
280
|
+
// Two rendering paths (symmetric with notify-slack):
|
|
281
|
+
// 1. state.report present → reportToLarkCard renders the full
|
|
282
|
+
// structured digest. Legacy fields are ignored.
|
|
283
|
+
// 2. Legacy severity/title/body shape → buildLarkCard renders
|
|
284
|
+
// the simple alert (sentry-triage et al.).
|
|
285
|
+
const card = (state.report && typeof state.report === 'object')
|
|
286
|
+
? reportToLarkCard(state.report)
|
|
287
|
+
: buildLarkCard(state);
|
|
288
|
+
const { messageId } = await postToLark({
|
|
289
|
+
token,
|
|
290
|
+
host,
|
|
291
|
+
receiveId,
|
|
292
|
+
receiveIdType,
|
|
293
|
+
card,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
delivered: true,
|
|
298
|
+
receiveId,
|
|
299
|
+
receiveIdType,
|
|
300
|
+
messageId,
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "notify-lark",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Post a structured alert card to a Lark / Feishu chat — child workflow, dispatched by parent workflows via sub-graph.",
|
|
7
|
+
"main": "graph.mjs",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@zibby/core": "^0.5.1",
|
|
13
|
+
"zod": "^3.23.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"vitest": "^2.1.5"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify-lark — three-schema state model.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors notify-slack's input shape EXACTLY (provider-neutral fields)
|
|
5
|
+
* so a parent workflow that wants to fan-out alerts to both Slack and
|
|
6
|
+
* Lark can use the same `input: (state) => ({...})` block for both
|
|
7
|
+
* dispatchSubgraph calls. The notifier-specific bits (`receiveId` vs
|
|
8
|
+
* `channel`, mention syntax) are caller-supplied; we don't try to
|
|
9
|
+
* translate between providers.
|
|
10
|
+
*
|
|
11
|
+
* Lark differences vs Slack:
|
|
12
|
+
* - Target is `receiveId` (chat_id `oc_…`, open_id `ou_…`, or email)
|
|
13
|
+
* — Lark needs the right `receive_id_type` query param, inferred
|
|
14
|
+
* from the id prefix.
|
|
15
|
+
* - Color is a theme template name (`red`, `orange`, `yellow`, `grey`,
|
|
16
|
+
* `purple`, `green`), not a hex.
|
|
17
|
+
* - Mention syntax: `<at user_id="ou_xxx">name</at>` — caller passes
|
|
18
|
+
* these strings ready-formatted, same as Slack.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
|
|
23
|
+
export const SEVERITIES = /** @type {const} */ (['low', 'medium', 'high', 'critical']);
|
|
24
|
+
|
|
25
|
+
export const notifyLarkInputSchema = z.object({
|
|
26
|
+
severity: z.enum(SEVERITIES)
|
|
27
|
+
.default('medium')
|
|
28
|
+
.describe('Alert severity. Drives header color + mention strategy.'),
|
|
29
|
+
|
|
30
|
+
// Required in legacy-mode; rich-mode (when `report` is set) sources
|
|
31
|
+
// the title from `report.title` instead. See notify-slack/state.js
|
|
32
|
+
// for the symmetric design.
|
|
33
|
+
title: z.string().min(1).max(300).optional()
|
|
34
|
+
.describe('Card header text. Required when `report` is absent (rich-mode sources from report.title).'),
|
|
35
|
+
|
|
36
|
+
body: z.string().max(3000).optional()
|
|
37
|
+
.describe('Body text. Supports Lark lark_md (e.g. **bold**, `code`, [text](url)).'),
|
|
38
|
+
|
|
39
|
+
receiveId: z.string().min(1).max(120)
|
|
40
|
+
.describe(
|
|
41
|
+
'Lark target. Format determines receive_id_type automatically:\n' +
|
|
42
|
+
' oc_… → chat_id (group/DM)\n' +
|
|
43
|
+
' ou_… → open_id (user)\n' +
|
|
44
|
+
' on_… → union_id\n' +
|
|
45
|
+
' email → email',
|
|
46
|
+
),
|
|
47
|
+
|
|
48
|
+
// Sentry-flavored fields (same shape as notify-slack).
|
|
49
|
+
sentryLink: z.string().url().optional()
|
|
50
|
+
.describe('Sentry issue URL — adds an "Open in Sentry" primary button.'),
|
|
51
|
+
affectedUsers: z.number().int().min(0).optional(),
|
|
52
|
+
events: z.number().int().min(0).optional(),
|
|
53
|
+
release: z.string().max(120).optional(),
|
|
54
|
+
firstSeen: z.string().optional(),
|
|
55
|
+
codeSnippet: z.string().max(2000).optional(),
|
|
56
|
+
actionUrl: z.string().url().optional(),
|
|
57
|
+
actionLabel: z.string().max(40).optional(),
|
|
58
|
+
|
|
59
|
+
// Mentions — caller-supplied Lark <at> strings.
|
|
60
|
+
mentions: z.array(z.string().max(200))
|
|
61
|
+
.max(20)
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Lark @-mentions, e.g. [\'<at user_id="ou_alice">@Alice</at>\'].'),
|
|
64
|
+
|
|
65
|
+
idempotencyKey: z.string().max(128).optional(),
|
|
66
|
+
|
|
67
|
+
// ── Rich-report mode ──────────────────────────────────────────────
|
|
68
|
+
// When `report` is present, the node renders the report-object via
|
|
69
|
+
// reportToLarkCard() and IGNORES the legacy severity/title/body
|
|
70
|
+
// fields. See notify-slack/state.js for the symmetric design.
|
|
71
|
+
report: z.record(z.any()).optional()
|
|
72
|
+
.describe('Rich report-object (see @zibby/skills/report). When set, supersedes severity/title/body — the node renders a full Lark Card.'),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const notifyLarkContextSchema = z.object({
|
|
76
|
+
notify_lark: z.object({
|
|
77
|
+
delivered: z.boolean(),
|
|
78
|
+
receiveId: z.string(),
|
|
79
|
+
receiveIdType: z.string(),
|
|
80
|
+
messageId: z.string().optional(),
|
|
81
|
+
}).optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const notifyLarkStateSchema =
|
|
85
|
+
notifyLarkInputSchema.merge(notifyLarkContextSchema);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# notify-notion
|
|
2
|
+
|
|
3
|
+
A reusable **child workflow** that posts a structured page (or appends blocks) to a Notion workspace.
|
|
4
|
+
|
|
5
|
+
Designed to be dispatched via sub-graph from any parent workflow — most commonly digest workflows (`ai-spend-weekly-digest`) that want to archive a weekly report to a Notion database, or alert workflows (`sentry-triage`) that want a durable record of each incident.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Takes a provider-neutral payload (severity, title, body, target, optional Sentry-flavored fields)
|
|
10
|
+
- Builds a Notion page (or block-children array) — including rich-report mode that delegates to `@zibby/skills/report`'s `reportToNotionBlocks`
|
|
11
|
+
- Posts via `POST /v1/pages` (when `databaseId` is supplied) or `PATCH /v1/blocks/{pageId}/children` (when `pageId` is supplied)
|
|
12
|
+
- Returns the page id + URL so the parent can link to it from downstream notifications
|
|
13
|
+
|
|
14
|
+
No LLM call — single deterministic API request, typically ~1-2s depending on block count.
|
|
15
|
+
|
|
16
|
+
## Dispatch shape (parent workflow)
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { WorkflowGraph, SKILLS } from '@zibby/core';
|
|
20
|
+
|
|
21
|
+
const g = new WorkflowGraph();
|
|
22
|
+
g.addNode('archive_to_notion', {
|
|
23
|
+
workflow: 'notify-notion',
|
|
24
|
+
input: (state) => ({
|
|
25
|
+
// Either databaseId OR pageId — not both:
|
|
26
|
+
databaseId: process.env.NOTION_INCIDENT_DB,
|
|
27
|
+
severity: 'critical',
|
|
28
|
+
title: 'Checkout: TypeError on session.user.id',
|
|
29
|
+
body: '12 users affected in the last 1h.',
|
|
30
|
+
sentryLink: 'https://sentry.io/.../1234567890/',
|
|
31
|
+
affectedUsers: 12,
|
|
32
|
+
events: 47,
|
|
33
|
+
release: '1.42.0',
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For digest workflows, pass a `report` object instead of severity/title/body — the node renders the full structured page via `reportToNotionBlocks`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
g.addNode('archive_digest', {
|
|
42
|
+
workflow: 'notify-notion',
|
|
43
|
+
input: (state) => ({
|
|
44
|
+
databaseId: process.env.NOTION_REPORTS_DB,
|
|
45
|
+
report: state.analyze.report, // produced by an upstream digest node
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Output
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
{
|
|
54
|
+
delivered: boolean,
|
|
55
|
+
pageId: string,
|
|
56
|
+
pageUrl?: string, // only set on the create branch (Notion returns it)
|
|
57
|
+
blocksCount?: number // diagnostic — how many blocks were posted
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Auth
|
|
62
|
+
|
|
63
|
+
The bot's OAuth token is resolved via `resolveIntegrationToken('notion')`. Connect your workspace in **Settings → Integrations → Notion** in the Zibby dashboard before running.
|
|
64
|
+
|
|
65
|
+
## Failure modes
|
|
66
|
+
|
|
67
|
+
- **401** — Notion rejected the token (revoked, or wrong workspace).
|
|
68
|
+
- **404** — Page/database not found, OR the Notion integration isn't added to the target page (Notion's quirk — share the page with the integration explicitly).
|
|
69
|
+
- **429** — Rate-limited. The parent should backoff + retry.
|
|
70
|
+
|
|
71
|
+
All three surface as typed errors with `.status` set; the parent's `onComplete` sees a failed execution row and can re-dispatch.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify-notion — single-node child workflow.
|
|
3
|
+
*
|
|
4
|
+
* Companion to notify-slack / notify-lark; same provider-neutral input
|
|
5
|
+
* shape so a parent can dispatch the same payload to all three (just
|
|
6
|
+
* swap the destination field: `channel` → `receiveId` → `databaseId`
|
|
7
|
+
* or `pageId`).
|
|
8
|
+
*
|
|
9
|
+
* Two write modes:
|
|
10
|
+
* - Create a NEW page in a database (`input.databaseId` set)
|
|
11
|
+
* - APPEND blocks to an existing page (`input.pageId` set)
|
|
12
|
+
* Exactly one must be supplied — the node throws clearly if neither
|
|
13
|
+
* or both are present.
|
|
14
|
+
*
|
|
15
|
+
* Returns `{ delivered, pageId, pageUrl, blocksCount }`. The `pageUrl`
|
|
16
|
+
* is only populated on the create-page branch (Notion returns the URL
|
|
17
|
+
* in the response); appended pages reuse the caller-supplied pageId.
|
|
18
|
+
*
|
|
19
|
+
* Example dispatch shape (parent workflow):
|
|
20
|
+
*
|
|
21
|
+
* g.addNode('notify', { workflow: 'notify-notion',
|
|
22
|
+
* input: (state) => ({
|
|
23
|
+
* severity: 'critical',
|
|
24
|
+
* title: 'Checkout: TypeError',
|
|
25
|
+
* body: '12 users affected in last 1h',
|
|
26
|
+
* databaseId: 'abc123...', // create new page in this DB
|
|
27
|
+
* sentryLink: 'https://sentry.io/.../1234567890/',
|
|
28
|
+
* }),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* For digest / report workflows, drop the legacy severity/title/body
|
|
32
|
+
* fields and pass a `report` object instead — the node delegates to
|
|
33
|
+
* @zibby/skills's reportToNotionBlocks() for the full structured page.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
|
|
37
|
+
import { notifyNotionNode } from './nodes/notify-notion-node.js';
|
|
38
|
+
import {
|
|
39
|
+
notifyNotionInputSchema,
|
|
40
|
+
notifyNotionContextSchema,
|
|
41
|
+
} from './state.js';
|
|
42
|
+
|
|
43
|
+
export class NotifyNotionAgent extends WorkflowAgent {
|
|
44
|
+
buildGraph() {
|
|
45
|
+
const graph = new WorkflowGraph();
|
|
46
|
+
graph
|
|
47
|
+
.setInputSchema(notifyNotionInputSchema)
|
|
48
|
+
.setContextSchema(notifyNotionContextSchema);
|
|
49
|
+
|
|
50
|
+
graph.addNode('notify_notion', notifyNotionNode);
|
|
51
|
+
graph.setEntryPoint('notify_notion');
|
|
52
|
+
graph.addEdge('notify_notion', 'END');
|
|
53
|
+
|
|
54
|
+
return graph;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async onComplete(result) {
|
|
58
|
+
const delivered = !!result?.state?.notify_notion?.delivered;
|
|
59
|
+
const pageId = result?.state?.notify_notion?.pageId || '?';
|
|
60
|
+
console.log(`[notify-notion] ${delivered ? 'delivered' : 'failed'} (pageId=${pageId})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default NotifyNotionAgent;
|
|
Binary file
|