@keystrokehq/skills 0.0.4 → 0.0.7
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/CHANGELOG.md +18 -0
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/{AGENTS.md → _AGENTS.md} +3 -1
- package/src/keystroke-agent-authoring/SKILL.md +1 -1
- package/src/keystroke-agent-authoring/references/source-map.md +1 -1
- package/src/keystroke-credential-binding/SKILL.md +4 -3
- package/src/keystroke-credential-binding/references/patterns.md +14 -15
- package/src/keystroke-credential-binding/references/source-map.md +2 -1
- package/src/keystroke-runtime-scripts/SKILL.md +79 -0
- package/src/keystroke-task-authoring/SKILL.md +4 -6
- package/src/keystroke-task-authoring/references/patterns.md +7 -7
- package/src/keystroke-task-authoring/references/source-map.md +4 -5
- package/src/keystroke-trigger-authoring/SKILL.md +26 -20
- package/src/keystroke-trigger-authoring/references/patterns.md +55 -63
- package/src/keystroke-trigger-authoring/references/source-map.md +67 -28
- package/src/keystroke-trigger-authoring/references/testing.md +29 -26
- package/src/keystroke-workflow-authoring/SKILL.md +2 -1
- package/src/keystroke-workflow-authoring/references/patterns.md +6 -6
- package/src/keystroke-workflow-authoring/references/prebuilt-integrations.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @keystrokehq/skills
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7abf3aa: Add runtime script guidance and scaffold `@keystrokehq/runtime` into initialized projects.
|
|
8
|
+
|
|
9
|
+
## 0.0.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a1488f8: Update trigger authoring skills for declarative webhook and polling trigger APIs.
|
|
14
|
+
|
|
15
|
+
## 0.0.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- a94a8e3: Rename the packaged Keystroke project context artifact to `_AGENTS.md` while preserving `AGENTS.md` as the destination filename during CLI init and skills sync.
|
|
20
|
+
|
|
3
21
|
## 0.0.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The packaged skills are:
|
|
|
16
16
|
- `keystroke-trigger-authoring`
|
|
17
17
|
- `keystroke-task-authoring`
|
|
18
18
|
- `keystroke-cli-workspace`
|
|
19
|
+
- `keystroke-runtime-scripts`
|
|
19
20
|
|
|
20
21
|
## Editing
|
|
21
22
|
|
|
@@ -31,7 +32,7 @@ The publishable package content follows this structure:
|
|
|
31
32
|
|
|
32
33
|
```text
|
|
33
34
|
src/
|
|
34
|
-
├──
|
|
35
|
+
├── _AGENTS.md
|
|
35
36
|
├── <skill-name>/
|
|
36
37
|
│ ├── SKILL.md
|
|
37
38
|
│ └── references/
|
|
@@ -40,7 +41,8 @@ src/
|
|
|
40
41
|
└── ...
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
- `src/
|
|
44
|
+
- `src/_AGENTS.md` is the Keystroke project context written or appended to project-root `AGENTS.md` by `keystroke init` and `keystroke skills sync`.
|
|
45
|
+
- The leading underscore keeps the package source artifact from being loaded as repository-local editor context while preserving the destination filename.
|
|
44
46
|
- `SKILL.md` stays concise and procedural.
|
|
45
47
|
- `references/` holds longer examples, source maps, and gotchas.
|
|
46
48
|
- `evals/<skill-name>/evals.json` stores the initial evaluation prompts used with `.agents/skills/skill-creator/`.
|
package/package.json
CHANGED
|
@@ -29,6 +29,8 @@ Keystroke also has one shared unit-of-work primitive: `Operation`.
|
|
|
29
29
|
- Use the `Operation` name when describing shared infrastructure, integrations, or a reusable unit that can be used in both places.
|
|
30
30
|
- The runtime behavior comes from context, not from which alias name was used in the constructor.
|
|
31
31
|
|
|
32
|
+
For local TypeScript scripts, agents can import `@keystrokehq/runtime` to run `Operation` / `Step` / `Tool` instances with automatic credential loading from overrides, env vars, or saved Keystroke auth.
|
|
33
|
+
|
|
32
34
|
Runtime boundary:
|
|
33
35
|
|
|
34
36
|
- workflows and steps are authored as TypeScript control-flow and unit-of-work code
|
|
@@ -103,7 +105,7 @@ Required structure:
|
|
|
103
105
|
|
|
104
106
|
- exported primitives should be top-level and statically visible
|
|
105
107
|
- helper files such as `schemas.ts`, `utils.ts`, or `prompts.ts` should not export primitives
|
|
106
|
-
- a `*.trigger.ts` file may also export
|
|
108
|
+
- a `*.trigger.ts` file may also export narrowed trigger variants or bound trigger helpers
|
|
107
109
|
- tests are exempt, but authored project code should follow the typed-file convention everywhere
|
|
108
110
|
|
|
109
111
|
Example layout:
|
|
@@ -172,7 +172,7 @@ Teach these rules explicitly:
|
|
|
172
172
|
- Do not use `process.env` in authored agent or tool code.
|
|
173
173
|
- Expect the default agent sandbox to be persistent.
|
|
174
174
|
- Create a custom `Sandbox` only when the default sandbox needs customization.
|
|
175
|
-
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/
|
|
175
|
+
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/skills/zod-4/SKILL.md` for deep schema guidance.
|
|
176
176
|
|
|
177
177
|
## Agent Guidelines for Custom Tools & Operations
|
|
178
178
|
|
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
### What they are used for
|
|
89
89
|
|
|
90
90
|
- `credentialSets`: credentials attached directly to the agent
|
|
91
|
-
- `allCredentialSets`: combined credential
|
|
91
|
+
- `allCredentialSets`: combined credential requirements from the agent, its tools, its MCP servers, and its messaging gateways
|
|
92
92
|
- `messaging`: `MessagingGateway[]` conversational entry configuration
|
|
93
93
|
- `runtimeKind`: whether the agent is declarative-only or has `run` / `stream` implementations
|
|
94
94
|
- `workflowSafeReference`: stable reference shape used by other primitives such as `Task`
|
|
@@ -87,7 +87,8 @@ Do not assume one credential location covers every layer. An agent can need prov
|
|
|
87
87
|
|
|
88
88
|
Teach these rules:
|
|
89
89
|
- steps and tools read credentials from `ctx.credentials`
|
|
90
|
-
-
|
|
90
|
+
- polling triggers read credentials from `poll(ctx).credentials` when they attach credential sets
|
|
91
|
+
- webhook filters, webhook idempotency, and workflow trigger transforms are declarative or mapping-only paths and do not receive credentials
|
|
91
92
|
- MCP servers map credentials through `credentialMapper`
|
|
92
93
|
- authored code should not read secrets from `process.env`
|
|
93
94
|
|
|
@@ -117,7 +118,7 @@ This matters for integration authors and when explaining `createOperationFactory
|
|
|
117
118
|
- If `stored` is present, `resolve` must also be present.
|
|
118
119
|
- If `resolve` is present, `stored` must also be present.
|
|
119
120
|
- Use `CredentialSet` instead of env-based secret handling inside authored primitives.
|
|
120
|
-
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/
|
|
121
|
+
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/skills/zod-4/SKILL.md` for deep schema guidance.
|
|
121
122
|
- The drawer-label rule is enforced at three layers today: compile-time on a single primitive (`AssertUniqueCredentialSetIds`), build-time across the project (`assertUniqueCredentialDefinitionIds`), and deploy-time schema-drift detection (`detectSchemaDrifts`). Cluster 07 of the reconciled credential plan adds two more layers: construction-time identity registry and vault-row schema fingerprint. You do not need to do anything special in authored code beyond following the rule above.
|
|
122
123
|
|
|
123
124
|
### Vault-row schema mismatch
|
|
@@ -439,6 +440,6 @@ For wider command usage, cross-link to `../keystroke-cli-workspace/SKILL.md`.
|
|
|
439
440
|
## References
|
|
440
441
|
|
|
441
442
|
Read these files as needed:
|
|
442
|
-
- `references/source-map.md` for
|
|
443
|
+
- `references/source-map.md` for public credential APIs
|
|
443
444
|
- `references/cli.md` for the current credential command cookbook
|
|
444
445
|
- `references/patterns.md` for field-by-field code examples
|
|
@@ -410,7 +410,7 @@ drop it from `stored` and have `rotate` return `'needs-reinput'` instead.
|
|
|
410
410
|
- Example: HubSpot's auth-modality normalization — `stored` accepts
|
|
411
411
|
either `ACCESS_TOKEN` (OAuth) or `API_KEY` (private app); `resolve`
|
|
412
412
|
picks whichever is populated and returns `{ HUBSPOT_ACCESS_TOKEN }`.
|
|
413
|
-
Throws when neither is present so a missing credential
|
|
413
|
+
Throws when neither is present so a missing credential becomes
|
|
414
414
|
an actionable resolver error rather than a provider 401. One
|
|
415
415
|
credential set covers both authorization paths; splitting into two
|
|
416
416
|
sets would force every consumer to branch on modality.
|
|
@@ -640,28 +640,27 @@ This attaches credentials directly to the agent and also allows tools on the age
|
|
|
640
640
|
## Trigger usage
|
|
641
641
|
|
|
642
642
|
```ts
|
|
643
|
-
import {
|
|
643
|
+
import { pollingTrigger } from '@keystrokehq/core';
|
|
644
644
|
import { z } from 'zod';
|
|
645
645
|
|
|
646
|
-
export const
|
|
647
|
-
|
|
648
|
-
description: '
|
|
649
|
-
path: '/signed',
|
|
650
|
-
method: 'POST',
|
|
646
|
+
export const crmPolling = pollingTrigger({
|
|
647
|
+
id: 'crm-polling',
|
|
648
|
+
description: 'Polls CRM records with credential-backed API access.',
|
|
651
649
|
credentialSets: [crmCredentials],
|
|
652
|
-
|
|
653
|
-
|
|
650
|
+
schedule: '5m',
|
|
651
|
+
response: z.object({
|
|
652
|
+
records: z.array(z.object({ id: z.string() })),
|
|
654
653
|
}),
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
654
|
+
poll: async (ctx) => {
|
|
655
|
+
const apiKey = ctx.credentials.crmApi.apiKey;
|
|
656
|
+
return {
|
|
657
|
+
records: [{ id: `record-for-${apiKey}` }],
|
|
658
|
+
};
|
|
660
659
|
},
|
|
661
660
|
});
|
|
662
661
|
```
|
|
663
662
|
|
|
664
|
-
This pattern is useful when
|
|
663
|
+
This pattern is useful when polling logic needs credentials. Webhook triggers do not attach per-trigger credential sets in the current custom/app source model.
|
|
665
664
|
|
|
666
665
|
## MCP server usage
|
|
667
666
|
|
|
@@ -52,7 +52,8 @@ import { CredentialSet } from '@keystrokehq/core';
|
|
|
52
52
|
- `Operation.run(..., ctx).credentials`
|
|
53
53
|
- `Step.run(..., ctx).credentials`
|
|
54
54
|
- `Tool.run(..., ctx).credentials`
|
|
55
|
-
-
|
|
55
|
+
- polling trigger `poll(ctx).credentials`
|
|
56
|
+
- not webhook `filter`, webhook `idempotencyKey`, or workflow trigger `transform`
|
|
56
57
|
- `McpServer.credentialMapper(credentials)`
|
|
57
58
|
|
|
58
59
|
## Important rules
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keystroke-runtime-scripts
|
|
3
|
+
description: Use @keystrokehq/runtime in local TypeScript scripts to run Keystroke Operation, Step, or Tool instances with credential loading and runtime context. Use when writing one-off scripts, local integration checks, or credential-backed operation runs outside Vitest.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Keystroke Runtime Scripts
|
|
7
|
+
|
|
8
|
+
Use this skill when an agent needs to run Keystroke operations from a local TypeScript or JavaScript script.
|
|
9
|
+
|
|
10
|
+
Default split:
|
|
11
|
+
- use `@keystrokehq/runtime` for local scripts and manual integration checks
|
|
12
|
+
- use `@keystrokehq/testing` for Vitest tests, mocks, hooks, and deterministic assertions
|
|
13
|
+
- use the CLI skill for workflow builds, deploys, credential upload, and deployed workflow runs
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Install the runtime side effect once, then call `.run(...)` on an `Operation`, `Step`, or `Tool`.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import '@keystrokehq/runtime';
|
|
21
|
+
|
|
22
|
+
import { listObjects } from '@keystrokehq/attio/objects';
|
|
23
|
+
|
|
24
|
+
const objects = await listObjects.run({});
|
|
25
|
+
console.info(JSON.stringify(objects, null, 2));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or use the explicit helper:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { run } from '@keystrokehq/runtime';
|
|
32
|
+
import { listObjects } from '@keystrokehq/attio/objects';
|
|
33
|
+
|
|
34
|
+
const objects = await run(listObjects, {});
|
|
35
|
+
console.info(JSON.stringify(objects, null, 2));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configure runtime
|
|
39
|
+
|
|
40
|
+
Use `configureRuntime(...)` for process-wide script settings:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { configureRuntime } from '@keystrokehq/runtime';
|
|
44
|
+
|
|
45
|
+
configureRuntime({
|
|
46
|
+
credentials: {
|
|
47
|
+
mode: 'auto',
|
|
48
|
+
overrides: {
|
|
49
|
+
attio: {
|
|
50
|
+
ACCESS_TOKEN: process.env.ATTIO_ACCESS_TOKEN,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
workflowGlobals: {
|
|
55
|
+
tenantId: 'tenant_local',
|
|
56
|
+
},
|
|
57
|
+
stepContext: {
|
|
58
|
+
stepId: 'local-script',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Credential modes:
|
|
64
|
+
- `auto`: explicit overrides, env vars, then saved `keystroke auth` credentials
|
|
65
|
+
- `env-only`: overrides and env vars only
|
|
66
|
+
- `off`: no runtime credential resolver
|
|
67
|
+
|
|
68
|
+
Use `withRuntime(options, callback)` when configuration must be scoped to one block. Use `createRuntime(options)` when you need an explicit handle and will call `runtime.dispose()`.
|
|
69
|
+
|
|
70
|
+
## Rules
|
|
71
|
+
|
|
72
|
+
- Keep scripts small and disposable.
|
|
73
|
+
- Prefer operation `.run(input)` after the side-effect import for simple scripts.
|
|
74
|
+
- Prefer `run(operation, input)` when the call site should make the runtime dependency obvious.
|
|
75
|
+
- Put secret values in `credentials.overrides` or env vars named from the credential set, not in source.
|
|
76
|
+
- Pass `workflowGlobals` when the operation expects typed workflow globals.
|
|
77
|
+
- Pass `stepContext` only for local metadata such as `stepId`, attempts, tracing, or prefilled credentials.
|
|
78
|
+
- Do not use runtime scripts as tests; use `@keystrokehq/testing` in `.test.ts` files.
|
|
79
|
+
- Do not use `execution: 'cloud'`; local scripts currently use `execution: 'local'`.
|
|
@@ -22,12 +22,10 @@ import { z } from 'zod';
|
|
|
22
22
|
import { welcomeAgent } from './welcome.agent';
|
|
23
23
|
|
|
24
24
|
const welcomeWebhookTrigger = webhookTrigger({
|
|
25
|
-
|
|
25
|
+
id: 'welcome-webhook',
|
|
26
26
|
description: 'Receives welcome payloads.',
|
|
27
27
|
source: {
|
|
28
28
|
type: 'custom',
|
|
29
|
-
method: 'POST',
|
|
30
|
-
path: '/welcome',
|
|
31
29
|
},
|
|
32
30
|
payload: z.object({
|
|
33
31
|
name: z.string(),
|
|
@@ -40,7 +38,7 @@ export const welcomeWebhookTask = new Task({
|
|
|
40
38
|
description: 'Uses a webhook payload to drive one agent run.',
|
|
41
39
|
agent: welcomeAgent,
|
|
42
40
|
prompt:
|
|
43
|
-
'Write a greeting for this webhook payload: {{trigger.payload}}. Trigger
|
|
41
|
+
'Write a greeting for this webhook payload: {{trigger.payload}}. Trigger id: {{trigger.id}}.',
|
|
44
42
|
triggers: [welcomeWebhookTrigger],
|
|
45
43
|
lifecycle: {
|
|
46
44
|
maxExecutions: 1,
|
|
@@ -80,7 +78,7 @@ Teach these rules:
|
|
|
80
78
|
Teach the supported task prompt variables:
|
|
81
79
|
|
|
82
80
|
- `{{trigger.payload}}`
|
|
83
|
-
- `{{trigger.
|
|
81
|
+
- `{{trigger.id}}`
|
|
84
82
|
- `{{trigger.type}}`
|
|
85
83
|
|
|
86
84
|
Use prompt templating when the agent prompt should include structured trigger context without inventing a separate payload mapping layer.
|
|
@@ -113,7 +111,7 @@ Choose a workflow when:
|
|
|
113
111
|
- Keep each exported task in its own `*.task.ts` file.
|
|
114
112
|
- Keep the referenced agent in its own `*.agent.ts` file.
|
|
115
113
|
- Keep triggers in their own `*.trigger.ts` files.
|
|
116
|
-
- Use Zod v4 syntax in all examples and authored code. See `../../../.agents/
|
|
114
|
+
- Use Zod v4 syntax in all examples and authored code. See `../../../.agents/skills/zod-4/SKILL.md` for deep schema guidance.
|
|
117
115
|
- Keep task prompts explicit about what the agent should do with trigger data.
|
|
118
116
|
|
|
119
117
|
## References
|
|
@@ -10,12 +10,10 @@ import { z } from 'zod';
|
|
|
10
10
|
import { triageAgent } from './triage.agent';
|
|
11
11
|
|
|
12
12
|
const inboundWebhook = webhookTrigger({
|
|
13
|
-
|
|
13
|
+
id: 'inbound-webhook',
|
|
14
14
|
description: 'Receives inbound payloads.',
|
|
15
15
|
source: {
|
|
16
16
|
type: 'custom',
|
|
17
|
-
method: 'POST',
|
|
18
|
-
path: '/inbound',
|
|
19
17
|
},
|
|
20
18
|
payload: z.object({
|
|
21
19
|
message: z.string(),
|
|
@@ -41,7 +39,7 @@ import { z } from 'zod';
|
|
|
41
39
|
import { reportAgent } from './report.agent';
|
|
42
40
|
|
|
43
41
|
const dailyReportTrigger = cronTrigger({
|
|
44
|
-
|
|
42
|
+
id: 'daily-report-trigger',
|
|
45
43
|
description: 'Runs each morning.',
|
|
46
44
|
input: z.object({
|
|
47
45
|
mode: z.literal('daily'),
|
|
@@ -56,7 +54,7 @@ export const dailyReportTask = new Task({
|
|
|
56
54
|
id: 'daily-report-task',
|
|
57
55
|
name: 'Daily Report Task',
|
|
58
56
|
agent: reportAgent,
|
|
59
|
-
prompt: 'Generate the daily report for {{trigger.
|
|
57
|
+
prompt: 'Generate the daily report for {{trigger.id}} using {{trigger.payload}}.',
|
|
60
58
|
triggers: [dailyReportTrigger],
|
|
61
59
|
});
|
|
62
60
|
```
|
|
@@ -69,7 +67,7 @@ import { z } from 'zod';
|
|
|
69
67
|
import { followUpAgent } from './follow-up.agent';
|
|
70
68
|
|
|
71
69
|
const staleTicketPolling = pollingTrigger({
|
|
72
|
-
|
|
70
|
+
id: 'stale-ticket-polling',
|
|
73
71
|
description: 'Polls for stale tickets.',
|
|
74
72
|
schedule: '*/15 * * * *',
|
|
75
73
|
response: z.object({
|
|
@@ -80,7 +78,9 @@ const staleTicketPolling = pollingTrigger({
|
|
|
80
78
|
ticketId: 'ticket_123',
|
|
81
79
|
status: 'stale',
|
|
82
80
|
}),
|
|
83
|
-
filter: (
|
|
81
|
+
filter: z.object({
|
|
82
|
+
status: z.literal('stale'),
|
|
83
|
+
}),
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
export const staleTicketTask = new Task({
|
|
@@ -31,13 +31,12 @@ import { Task, type TaskConfig, type TaskLifecycle } from '@keystrokehq/core';
|
|
|
31
31
|
## `TriggerContext`
|
|
32
32
|
|
|
33
33
|
- `payload`
|
|
34
|
-
- `
|
|
34
|
+
- `id`
|
|
35
35
|
- `type`
|
|
36
36
|
|
|
37
37
|
The current public trigger context `type` values are:
|
|
38
38
|
- `cron`
|
|
39
39
|
- `polling`
|
|
40
|
-
- `provider`
|
|
41
40
|
- `webhook`
|
|
42
41
|
|
|
43
42
|
## Prompt template tokens
|
|
@@ -48,8 +47,8 @@ directly into the `prompt` string — there is no function to call.
|
|
|
48
47
|
|
|
49
48
|
Supported tokens:
|
|
50
49
|
- `{{trigger.payload}}` — resolved to `JSON.stringify` of the trigger payload
|
|
51
|
-
- `{{trigger.
|
|
52
|
-
- `{{trigger.type}}` — resolved to the trigger type (one of `cron`, `polling`, `
|
|
50
|
+
- `{{trigger.id}}` — resolved to the trigger id
|
|
51
|
+
- `{{trigger.type}}` — resolved to the trigger type (one of `cron`, `polling`, `webhook`)
|
|
53
52
|
|
|
54
53
|
Unknown `{{...}}` tokens are left as-is in the resolved prompt.
|
|
55
54
|
|
|
@@ -57,5 +56,5 @@ Unknown `{{...}}` tokens are left as-is in the resolved prompt.
|
|
|
57
56
|
|
|
58
57
|
- tasks reference an agent through `agent`
|
|
59
58
|
- tasks list triggers directly in `triggers`
|
|
60
|
-
- tasks do not
|
|
59
|
+
- tasks do not bind triggers with workflow-style `{ transform }`
|
|
61
60
|
- tasks are deployed through `keystroke deploy` or focused with `keystroke deploy --target <task.task.ts>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: keystroke-trigger-authoring
|
|
3
|
-
description: Build Keystroke cron, webhook,
|
|
3
|
+
description: Build Keystroke cron, webhook, and polling triggers with @keystrokehq/core. Use when the user wants to author, test, or explain trigger code, including webhook app/custom sources, declarative filter schemas, webhook idempotency config, polling schedules, trigger narrowing, and mapping trigger input into workflows or task-driven agent runs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Keystroke Trigger Authoring
|
|
@@ -22,12 +22,10 @@ import { webhookTrigger, Workflow } from '@keystrokehq/core';
|
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
|
|
24
24
|
export const paymentWebhook = webhookTrigger({
|
|
25
|
-
|
|
25
|
+
id: 'payment-webhook',
|
|
26
26
|
description: 'Receives payment events from an external provider.',
|
|
27
27
|
source: {
|
|
28
28
|
type: 'custom',
|
|
29
|
-
method: 'POST',
|
|
30
|
-
path: '/payments',
|
|
31
29
|
},
|
|
32
30
|
payload: z.object({
|
|
33
31
|
id: z.string(),
|
|
@@ -36,8 +34,13 @@ export const paymentWebhook = webhookTrigger({
|
|
|
36
34
|
amount: z.number(),
|
|
37
35
|
}),
|
|
38
36
|
}),
|
|
39
|
-
filter: (
|
|
40
|
-
|
|
37
|
+
filter: z.object({
|
|
38
|
+
type: z.literal('payment.completed'),
|
|
39
|
+
}),
|
|
40
|
+
idempotencyKey: {
|
|
41
|
+
from: 'payload',
|
|
42
|
+
path: 'id',
|
|
43
|
+
},
|
|
41
44
|
});
|
|
42
45
|
```
|
|
43
46
|
|
|
@@ -69,7 +72,7 @@ import { z } from 'zod';
|
|
|
69
72
|
import { reminderAgent } from './reminder.agent';
|
|
70
73
|
|
|
71
74
|
const dailyReminderTrigger = cronTrigger({
|
|
72
|
-
|
|
75
|
+
id: 'daily-reminder-trigger',
|
|
73
76
|
description: 'Runs every morning.',
|
|
74
77
|
input: z.object({
|
|
75
78
|
mode: z.literal('daily'),
|
|
@@ -84,7 +87,7 @@ export const dailyReminderTask = new Task({
|
|
|
84
87
|
id: 'daily-reminder-task',
|
|
85
88
|
name: 'Daily Reminder Task',
|
|
86
89
|
agent: reminderAgent,
|
|
87
|
-
prompt: 'Send the daily reminder for trigger {{trigger.
|
|
90
|
+
prompt: 'Send the daily reminder for trigger {{trigger.id}}.',
|
|
88
91
|
triggers: [dailyReminderTrigger],
|
|
89
92
|
});
|
|
90
93
|
```
|
|
@@ -97,6 +100,8 @@ Teach this mental model clearly:
|
|
|
97
100
|
- workflow triggers are listed in `Workflow({ triggers: [...] })`
|
|
98
101
|
- a bare trigger in the array means the trigger payload passes through directly as workflow input
|
|
99
102
|
- calling the trigger as a function with `{ transform }` creates a bound trigger for payload-to-input mapping
|
|
103
|
+
- filtering lives on the trigger as a pure Zod schema, or on a narrowed child trigger with `.narrow({ id, filter })`
|
|
104
|
+
- webhook idempotency is declarative config, not a callback
|
|
100
105
|
- task triggers are listed inline in `Task.triggers`
|
|
101
106
|
- a `MessagingGateway` is not a trigger
|
|
102
107
|
|
|
@@ -106,11 +111,10 @@ Teach these public trigger factory functions from `@keystrokehq/core`:
|
|
|
106
111
|
- `cronTrigger`
|
|
107
112
|
- `webhookTrigger`
|
|
108
113
|
- `pollingTrigger`
|
|
109
|
-
- `providerTrigger`
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
Use webhook triggers with `source: { type: 'app', appRef }` when events are fanned out by a Keystroke-managed provider app. Use `source: { type: 'custom' }` when the platform should create a Keystroke-owned custom webhook surface.
|
|
112
116
|
|
|
113
|
-
The factories return a `CallableTrigger` — an object with trigger properties (`.
|
|
117
|
+
The factories return a `CallableTrigger` — an object with trigger properties (`.id`, `.toManifest()`, etc.) that is also callable as a function to create a bound trigger with optional `transform`. Bindings carry only `transform`; they do not carry filter or idempotency callbacks.
|
|
114
118
|
|
|
115
119
|
## Manual API Execution
|
|
116
120
|
|
|
@@ -140,6 +144,7 @@ Workflow trigger rules:
|
|
|
140
144
|
- list it in `Workflow({ triggers: [...] })`
|
|
141
145
|
- for a bare trigger (no transform), place it directly: `triggers: [myTrigger]`
|
|
142
146
|
- for a bound trigger with transform, call it: `triggers: [myTrigger({ transform: (payload) => ({...}) })]`
|
|
147
|
+
- for per-workflow filtering, attach a narrowed trigger: `triggers: [myTrigger.narrow({ id: '...', filter: z.object({...}) })({ transform })]`
|
|
143
148
|
- test the mapping by creating a bound trigger and calling `bound.transform?.(payload)`
|
|
144
149
|
|
|
145
150
|
## Task triggers
|
|
@@ -156,12 +161,13 @@ Task trigger rules:
|
|
|
156
161
|
## Trigger rules
|
|
157
162
|
|
|
158
163
|
- Keep each exported trigger in its own `*.trigger.ts` file.
|
|
159
|
-
- Use `
|
|
160
|
-
- Use `filter` for event gating
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
164
|
+
- Use `id`, not `name`, as the stable authored trigger identity.
|
|
165
|
+
- Use `filter` for event gating on webhook and polling triggers. It must be a pure Zod schema that can be converted to JSON Schema; do not use `.refine()`, `.superRefine()`, `.transform()`, `.preprocess()`, `.pipe()`, or `z.custom()`.
|
|
166
|
+
- Put conditional logic that cannot be expressed as a pure Zod schema at the start of the workflow body, or behind an integration `mapPayload` helper that throws when the declarative filter was too broad.
|
|
167
|
+
- Use `idempotencyKey` only on webhook triggers. It is declarative config such as `{ from: 'payload', path: 'id' }`, `{ from: 'payload', strategy: 'hash' }`, `{ from: 'header', name: 'x-event-id' }`, or `{ from: 'header', strategy: 'hash' }`.
|
|
168
|
+
- Polling triggers do not support idempotency config.
|
|
169
|
+
- `source: { type: 'custom' }` means the platform owns the HTTP surface and authenticates with a Keystroke-issued secret. Do not add `method`, `path`, `verify`, or `response`.
|
|
170
|
+
- `source: { type: 'app', appRef }` means a Keystroke-managed provider app fans out events centrally.
|
|
165
171
|
|
|
166
172
|
## Agent Guidelines for Custom Triggers
|
|
167
173
|
|
|
@@ -169,13 +175,13 @@ When an agent needs to write custom triggers, it must follow these rules:
|
|
|
169
175
|
1. **Always use prebuilt triggers** if they exist before writing custom ones.
|
|
170
176
|
2. **Collect context first**: Do you have all the information you need from the user to build the trigger? If not, ask the user to clarify what they are looking for. **Do Not Guess**.
|
|
171
177
|
3. **Understand API payloads**: If the trigger handles webhooks or fetches from an API endpoint, search the provider's docs to understand the payloads. If possible, hit available endpoints to inspect the actual payloads.
|
|
172
|
-
4. **Always write and run tests**: You must always write tests for custom triggers that handle non-trivial
|
|
178
|
+
4. **Always write and run tests**: You must always write tests for custom triggers that handle polling logic, payload mapping, or non-trivial filter schemas. Always run the tests to verify that the new triggers run (see `references/testing.md`).
|
|
173
179
|
5. **Handle missing credentials**: If you cannot run tests because of missing credentials, ask the user to configure them following the `../keystroke-credential-binding/SKILL.md` skill. The user will need to upload credentials before deploying anyway.
|
|
174
180
|
|
|
175
181
|
## Testing path
|
|
176
182
|
|
|
177
183
|
Default trigger testing guidance:
|
|
178
|
-
- test
|
|
184
|
+
- test declarative filter and idempotency metadata through `trigger.toManifest().runtime`
|
|
179
185
|
- test webhook parsing with `trigger.payload.parse(JSON.parse(request.rawBody))`
|
|
180
186
|
- test polling validation with `trigger.parseResponse(response)`
|
|
181
187
|
- test workflow input mapping by creating a bound trigger and calling `bound.transform?.(payload)`
|
|
@@ -185,5 +191,5 @@ Default trigger testing guidance:
|
|
|
185
191
|
|
|
186
192
|
Read these files as needed:
|
|
187
193
|
- `references/source-map.md` for the public trigger surface
|
|
188
|
-
- `references/patterns.md` for field-by-field examples, including
|
|
194
|
+
- `references/patterns.md` for field-by-field examples, including webhook app sources
|
|
189
195
|
- `references/testing.md` for trigger and bound trigger tests
|
|
@@ -13,11 +13,9 @@ File layout reminder:
|
|
|
13
13
|
## Shared advanced fields
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const executionIdentityPolicy: ExecutionIdentityPolicy = {
|
|
16
|
+
const executionIdentityPolicy = {
|
|
19
17
|
subjectMode: 'requiredWhenUserProvidedCredential',
|
|
20
|
-
};
|
|
18
|
+
} as const;
|
|
21
19
|
```
|
|
22
20
|
|
|
23
21
|
Use `executionIdentityPolicy` when the trigger should require a subject only in specific credential cases. Use `modeDefault` when the trigger should default to a specific trigger mode such as `'subscribable'`.
|
|
@@ -29,7 +27,7 @@ import { cronTrigger } from '@keystrokehq/core';
|
|
|
29
27
|
import { z } from 'zod';
|
|
30
28
|
|
|
31
29
|
export const nightlyDigestTrigger = cronTrigger({
|
|
32
|
-
|
|
30
|
+
id: 'nightly-digest-trigger',
|
|
33
31
|
description: 'Runs every night.',
|
|
34
32
|
enabled: true,
|
|
35
33
|
modeDefault: 'subscribable',
|
|
@@ -67,37 +65,49 @@ import { webhookTrigger } from '@keystrokehq/core';
|
|
|
67
65
|
import { z } from 'zod';
|
|
68
66
|
|
|
69
67
|
export const paymentWebhook = webhookTrigger({
|
|
70
|
-
|
|
68
|
+
id: 'payment-webhook',
|
|
71
69
|
description: 'Handles payment events.',
|
|
72
70
|
enabled: true,
|
|
73
71
|
modeDefault: 'subscribable',
|
|
74
72
|
source: {
|
|
75
73
|
type: 'custom',
|
|
76
|
-
method: 'POST',
|
|
77
|
-
path: '/payments',
|
|
78
|
-
verify: async (request) => {
|
|
79
|
-
if (!request.headers['x-signature']) {
|
|
80
|
-
throw new Error('Missing signature header');
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
response: {
|
|
84
|
-
successStatus: 202,
|
|
85
|
-
successBody: {
|
|
86
|
-
accepted: true,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
74
|
},
|
|
90
75
|
payload: z.object({
|
|
91
76
|
id: z.string(),
|
|
92
77
|
type: z.string(),
|
|
93
78
|
amount: z.number(),
|
|
94
79
|
}),
|
|
95
|
-
filter: (
|
|
96
|
-
|
|
80
|
+
filter: z.object({
|
|
81
|
+
type: z.literal('payment.completed'),
|
|
82
|
+
}),
|
|
83
|
+
idempotencyKey: {
|
|
84
|
+
from: 'payload',
|
|
85
|
+
path: 'id',
|
|
86
|
+
},
|
|
97
87
|
});
|
|
98
88
|
```
|
|
99
89
|
|
|
100
|
-
Use `
|
|
90
|
+
Use `source: { type: 'custom' }` when the platform should create a Keystroke-owned webhook surface. The platform owns HTTP authentication and response handling for custom webhooks; do not add `method`, `path`, `verify`, or `response`.
|
|
91
|
+
|
|
92
|
+
Use `source: { type: 'app', appRef }` when a Keystroke-managed provider app fans events out centrally:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export const slackMessageWebhook = webhookTrigger({
|
|
96
|
+
id: 'slack-message',
|
|
97
|
+
description: 'Receives Slack message events from the platform Slack app.',
|
|
98
|
+
source: {
|
|
99
|
+
type: 'app',
|
|
100
|
+
appRef: 'slack',
|
|
101
|
+
},
|
|
102
|
+
payload: z.object({
|
|
103
|
+
type: z.literal('message'),
|
|
104
|
+
channel: z.string(),
|
|
105
|
+
text: z.string(),
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To parse a webhook body in tests or local checks, use `trigger.payload.parse(JSON.parse(request.rawBody))`.
|
|
101
111
|
|
|
102
112
|
## Binding a webhook trigger to a workflow with `transform`
|
|
103
113
|
|
|
@@ -128,7 +138,7 @@ import { pollingTrigger } from '@keystrokehq/core';
|
|
|
128
138
|
import { z } from 'zod';
|
|
129
139
|
|
|
130
140
|
export const orderPolling = pollingTrigger({
|
|
131
|
-
|
|
141
|
+
id: 'order-polling',
|
|
132
142
|
description: 'Polls for the newest order.',
|
|
133
143
|
enabled: true,
|
|
134
144
|
schedule: '*/15 * * * *',
|
|
@@ -144,12 +154,14 @@ export const orderPolling = pollingTrigger({
|
|
|
144
154
|
status: 'created',
|
|
145
155
|
};
|
|
146
156
|
},
|
|
147
|
-
filter: (
|
|
148
|
-
|
|
157
|
+
filter: z.object({
|
|
158
|
+
status: z.literal('created'),
|
|
159
|
+
}),
|
|
149
160
|
});
|
|
150
161
|
```
|
|
151
162
|
|
|
152
163
|
Use this when the workflow should be entered from periodic remote-state checks.
|
|
164
|
+
Polling filters are pure Zod schemas. Polling triggers do not support idempotency config.
|
|
153
165
|
|
|
154
166
|
## `pollingTrigger.parseResponse(...)`
|
|
155
167
|
|
|
@@ -162,23 +174,6 @@ const parsedResponse = orderPolling.parseResponse({
|
|
|
162
174
|
|
|
163
175
|
Use `parseResponse(...)` when you want the trigger's response schema validation without running polling logic.
|
|
164
176
|
|
|
165
|
-
## `providerTrigger`
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
import { providerTrigger } from '@keystrokehq/core';
|
|
169
|
-
|
|
170
|
-
export const githubIssueTrigger = providerTrigger({
|
|
171
|
-
name: 'GitHub Issue Trigger',
|
|
172
|
-
description: 'Receives GitHub provider events for issue activity.',
|
|
173
|
-
provider: 'github',
|
|
174
|
-
eventTypes: ['issues.opened', 'issues.edited'],
|
|
175
|
-
filter: async (event) => event.type === 'issues.opened',
|
|
176
|
-
idempotencyKey: async (event) => event.id,
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Use a provider trigger when the user is authoring around normalized provider events instead of a raw webhook or polling loop.
|
|
181
|
-
|
|
182
177
|
## Bare trigger in workflow (no transform)
|
|
183
178
|
|
|
184
179
|
```ts
|
|
@@ -206,7 +201,7 @@ Call the trigger as a function with `{ transform }` when the trigger payload and
|
|
|
206
201
|
|
|
207
202
|
```ts
|
|
208
203
|
const largePayments = paymentWebhook.narrow({
|
|
209
|
-
|
|
204
|
+
id: 'large-payments',
|
|
210
205
|
filter: z.object({ amount: z.number().gt(100) }),
|
|
211
206
|
});
|
|
212
207
|
|
|
@@ -231,36 +226,33 @@ Use the task skill when the user needs prompt templating, task lifecycle, or foc
|
|
|
231
226
|
## Trigger credentials
|
|
232
227
|
|
|
233
228
|
```ts
|
|
234
|
-
import { CredentialSet,
|
|
229
|
+
import { CredentialSet, pollingTrigger } from '@keystrokehq/core';
|
|
235
230
|
import { z } from 'zod';
|
|
236
231
|
|
|
237
|
-
const
|
|
238
|
-
id: '
|
|
232
|
+
const crmCredentials = new CredentialSet({
|
|
233
|
+
id: 'crmApi',
|
|
239
234
|
auth: z.object({
|
|
240
|
-
|
|
235
|
+
apiKey: z.string(),
|
|
241
236
|
}),
|
|
242
237
|
});
|
|
243
238
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
description: '
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
credentialSets: [signingCredentials],
|
|
252
|
-
verify: async (_request, ctx) => {
|
|
253
|
-
if (!ctx.credentials.webhookSigning.secret) {
|
|
254
|
-
throw new Error('Missing signing secret');
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
payload: z.object({
|
|
259
|
-
id: z.string(),
|
|
239
|
+
const recentCustomers = pollingTrigger({
|
|
240
|
+
id: 'recent-customers',
|
|
241
|
+
description: 'Polls for recently updated customers.',
|
|
242
|
+
credentialSets: [crmCredentials],
|
|
243
|
+
schedule: '5m',
|
|
244
|
+
response: z.object({
|
|
245
|
+
customers: z.array(z.object({ id: z.string() })),
|
|
260
246
|
}),
|
|
247
|
+
poll: async (ctx) => {
|
|
248
|
+
const apiKey = ctx.credentials.crmApi.apiKey;
|
|
249
|
+
return { customers: [{ id: `customer-for-${apiKey}` }] };
|
|
250
|
+
},
|
|
261
251
|
});
|
|
262
252
|
```
|
|
263
253
|
|
|
254
|
+
Polling triggers can attach credential sets and read credentials in `poll(ctx)`. Webhook triggers do not attach per-trigger credential sets in the current custom/app source model.
|
|
255
|
+
|
|
264
256
|
## `describe()` and `toManifest()`
|
|
265
257
|
|
|
266
258
|
```ts
|
|
@@ -6,17 +6,16 @@ Use only the public imports a user repo can rely on:
|
|
|
6
6
|
import {
|
|
7
7
|
cronTrigger,
|
|
8
8
|
pollingTrigger,
|
|
9
|
-
providerTrigger,
|
|
10
9
|
webhookTrigger,
|
|
11
10
|
} from '@keystrokehq/core';
|
|
12
11
|
import type {
|
|
13
12
|
BoundTrigger,
|
|
14
13
|
CallableTrigger,
|
|
15
|
-
|
|
14
|
+
IdempotencyKeyConfig,
|
|
16
15
|
TriggerBindOptions,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '@keystrokehq/core/
|
|
16
|
+
TriggerManifest,
|
|
17
|
+
WebhookRequest,
|
|
18
|
+
} from '@keystrokehq/core/trigger';
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
When a trigger explanation also needs to talk about workflow steps or agent tools, use the terminology from the other Keystroke skills:
|
|
@@ -27,7 +26,7 @@ When a trigger explanation also needs to talk about workflow steps or agent tool
|
|
|
27
26
|
|
|
28
27
|
## Common trigger fields
|
|
29
28
|
|
|
30
|
-
- `
|
|
29
|
+
- `id`
|
|
31
30
|
- `description`
|
|
32
31
|
- `enabled`
|
|
33
32
|
- `credentialSets`
|
|
@@ -48,7 +47,7 @@ When a trigger explanation also needs to talk about workflow steps or agent tool
|
|
|
48
47
|
|
|
49
48
|
## `cronTrigger` instance properties
|
|
50
49
|
|
|
51
|
-
- `.
|
|
50
|
+
- `.id`
|
|
52
51
|
- `.payload`
|
|
53
52
|
- `.schedule`
|
|
54
53
|
- `.timezone`
|
|
@@ -57,43 +56,83 @@ When a trigger explanation also needs to talk about workflow steps or agent tool
|
|
|
57
56
|
|
|
58
57
|
## `webhookTrigger` fields
|
|
59
58
|
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
59
|
+
- `id`
|
|
60
|
+
- `description`
|
|
61
|
+
- `source`
|
|
62
62
|
- `payload`
|
|
63
|
-
- `verify`
|
|
64
63
|
- `filter`
|
|
65
64
|
- `idempotencyKey`
|
|
66
|
-
- `response`
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
### `webhookTrigger.source`
|
|
67
|
+
|
|
68
|
+
- `{ type: 'custom' }` — Keystroke owns the HTTP surface and authenticates with a Keystroke-issued secret
|
|
69
|
+
- `{ type: 'app', appRef: string }` — a Keystroke-managed provider app fans out events centrally
|
|
69
70
|
|
|
71
|
+
Do not teach `method`, `path`, `verify`, or `response` on webhook triggers. Those are not part of the current public config.
|
|
72
|
+
|
|
73
|
+
## `webhookTrigger` instance properties and methods
|
|
74
|
+
|
|
75
|
+
- `.id`
|
|
76
|
+
- `.source`
|
|
77
|
+
- `.payload`
|
|
70
78
|
- `.payload.parse(data)` — validate parsed body against the payload schema
|
|
71
|
-
- `.
|
|
72
|
-
- `.
|
|
73
|
-
- `.
|
|
79
|
+
- `.filter` — pure Zod schema metadata, not a callback
|
|
80
|
+
- `.idempotencyKey` — declarative `IdempotencyKeyConfig`, not a callback
|
|
81
|
+
- `.toManifest()`
|
|
82
|
+
- `.describe()`
|
|
74
83
|
|
|
75
84
|
## `pollingTrigger` fields
|
|
76
85
|
|
|
86
|
+
- `id`
|
|
87
|
+
- `description`
|
|
77
88
|
- `schedule`
|
|
78
89
|
- `response`
|
|
79
90
|
- `poll`
|
|
80
91
|
- `filter`
|
|
81
|
-
- `idempotencyKey`
|
|
82
92
|
|
|
83
|
-
## `pollingTrigger` instance methods
|
|
93
|
+
## `pollingTrigger` instance properties and methods
|
|
84
94
|
|
|
95
|
+
- `.id`
|
|
85
96
|
- `.poll(ctx)`
|
|
86
97
|
- `.parseResponse(response)`
|
|
87
|
-
- `.filter
|
|
88
|
-
- `.idempotencyKey?(payload)` — typed parsed payload (no credentials)
|
|
98
|
+
- `.filter` — pure Zod schema metadata, not a callback
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
Polling triggers do not support idempotency config.
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
## Filter schemas
|
|
103
|
+
|
|
104
|
+
Webhook and polling triggers accept `filter` as a pure Zod schema. The runtime converts it to JSON Schema for server-side evaluation before a VM hop.
|
|
105
|
+
|
|
106
|
+
Allowed filter schemas are structural schemas such as:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
z.object({
|
|
110
|
+
type: z.literal('payment.completed'),
|
|
111
|
+
amount: z.number().min(100),
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Do not use effectful Zod features in filters:
|
|
116
|
+
|
|
117
|
+
- `.refine()`
|
|
118
|
+
- `.superRefine()`
|
|
119
|
+
- `.transform()`
|
|
120
|
+
- `.preprocess()`
|
|
121
|
+
- `.pipe()`
|
|
122
|
+
- `z.custom()`
|
|
123
|
+
|
|
124
|
+
For conditional logic that cannot be expressed as a pure schema, do the check at the start of the workflow body or use an integration `mapPayload` helper.
|
|
125
|
+
|
|
126
|
+
## Webhook idempotency config
|
|
127
|
+
|
|
128
|
+
Webhook triggers accept declarative `IdempotencyKeyConfig`:
|
|
129
|
+
|
|
130
|
+
- `{ from: 'payload', path: 'eventId' }`
|
|
131
|
+
- `{ from: 'payload', strategy: 'hash' }`
|
|
132
|
+
- `{ from: 'header', name: 'x-event-id' }`
|
|
133
|
+
- `{ from: 'header', strategy: 'hash' }`
|
|
134
|
+
|
|
135
|
+
There is no function-style idempotency callback.
|
|
97
136
|
|
|
98
137
|
## Calling a trigger (creating a bound trigger)
|
|
99
138
|
|
|
@@ -102,7 +141,7 @@ All factory-created triggers are callable. Calling one returns a `BoundTrigger`:
|
|
|
102
141
|
- `trigger()` — bare binding (no transform)
|
|
103
142
|
- `trigger({ transform })` — bound with payload-to-input mapping
|
|
104
143
|
|
|
105
|
-
Bindings carry only `transform`. To filter events for a specific workflow, derive a child with `trigger.narrow({
|
|
144
|
+
Bindings carry only `transform`. To filter events for a specific workflow, derive a child with `trigger.narrow({ id, filter })` and attach that. Binding-level `filter` and `idempotencyKey` are intentionally not part of the API.
|
|
106
145
|
|
|
107
146
|
## `BoundTrigger`
|
|
108
147
|
|
|
@@ -112,7 +151,7 @@ Bindings carry only `transform`. To filter events for a specific workflow, deriv
|
|
|
112
151
|
|
|
113
152
|
## Workflow `triggers` array
|
|
114
153
|
|
|
115
|
-
`Workflow({ triggers: [...] })` accepts
|
|
154
|
+
`Workflow({ triggers: [...] })` accepts trigger entries:
|
|
116
155
|
- a bare `CallableTrigger` (trigger payload passes through as workflow input)
|
|
117
156
|
- a `BoundTrigger` (returned by calling the trigger with options)
|
|
118
157
|
|
|
@@ -124,4 +163,4 @@ Bindings carry only `transform`. To filter events for a specific workflow, deriv
|
|
|
124
163
|
## Where to read next
|
|
125
164
|
|
|
126
165
|
- `patterns.md` for trigger field examples
|
|
127
|
-
- `testing.md` for trigger
|
|
166
|
+
- `testing.md` for trigger metadata, polling, and bound trigger tests
|
|
@@ -17,7 +17,7 @@ export default defineConfig({
|
|
|
17
17
|
});
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
## Test webhook
|
|
20
|
+
## Test webhook parsing and declarative metadata
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
23
|
const request = {
|
|
@@ -34,28 +34,36 @@ const request = {
|
|
|
34
34
|
path: '/payments',
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const payload = paymentWebhook.payload.parse(JSON.parse(request.rawBody));
|
|
38
|
+
|
|
39
|
+
expect(payload).toEqual({
|
|
40
|
+
id: 'evt_123',
|
|
41
|
+
type: 'payment.completed',
|
|
42
|
+
amount: 5000,
|
|
41
43
|
});
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
+
const runtime = paymentWebhook.toManifest().runtime;
|
|
46
|
+
expect(runtime.filterSchema).toBeDefined();
|
|
47
|
+
expect(runtime.idempotencyConfig).toEqual({
|
|
48
|
+
from: 'payload',
|
|
49
|
+
path: 'id',
|
|
50
|
+
});
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
This isolates the webhook-only concerns:
|
|
48
54
|
|
|
49
|
-
- authenticity checks in `verify`
|
|
50
55
|
- parsing via `trigger.payload.parse(JSON.parse(request.rawBody))`
|
|
51
|
-
- event gating in `
|
|
56
|
+
- declarative event gating metadata in `runtime.filterSchema`
|
|
57
|
+
- declarative dedupe metadata in `runtime.idempotencyConfig`
|
|
58
|
+
|
|
59
|
+
Custom webhook HTTP authentication and responses are platform-owned in the current surface. Do not test `verify`, `method`, `path`, or `response` callbacks on authored webhook triggers.
|
|
52
60
|
|
|
53
61
|
## Test polling behavior
|
|
54
62
|
|
|
55
63
|
```ts
|
|
56
64
|
const response = await orderPolling.poll({
|
|
57
65
|
credentials: {},
|
|
58
|
-
|
|
66
|
+
triggerId: orderPolling.id,
|
|
59
67
|
triggerType: 'polling',
|
|
60
68
|
lastPolledAt: new Date().toISOString(),
|
|
61
69
|
lastResponse: {
|
|
@@ -65,6 +73,7 @@ const response = await orderPolling.poll({
|
|
|
65
73
|
});
|
|
66
74
|
|
|
67
75
|
const payload = orderPolling.parseResponse(response);
|
|
76
|
+
expect(orderPolling.toManifest().runtime.filterSchema).toBeDefined();
|
|
68
77
|
```
|
|
69
78
|
|
|
70
79
|
This isolates the polling-only concerns:
|
|
@@ -72,6 +81,7 @@ This isolates the polling-only concerns:
|
|
|
72
81
|
- what `poll(...)` returns
|
|
73
82
|
- how prior state affects the next poll
|
|
74
83
|
- whether the response matches the trigger schema
|
|
84
|
+
- declarative filter metadata when the polling trigger has a filter schema
|
|
75
85
|
|
|
76
86
|
## Test bound trigger transform
|
|
77
87
|
|
|
@@ -110,26 +120,19 @@ const workflowInput = bound.transform?.(
|
|
|
110
120
|
);
|
|
111
121
|
```
|
|
112
122
|
|
|
113
|
-
Pass the optional second argument when the transform depends on webhook request data
|
|
123
|
+
Pass the optional second argument when the transform depends on webhook request data such as headers or query params. `transform` does not receive credentials. Filters and idempotency are declarative metadata, not callbacks.
|
|
114
124
|
|
|
115
125
|
## Full webhook-to-workflow test
|
|
116
126
|
|
|
117
127
|
```ts
|
|
118
128
|
it('maps a valid webhook event into workflow input', async () => {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await paymentWebhook.verify?.(request, {
|
|
125
|
-
credentials: {},
|
|
126
|
-
triggerName: paymentWebhook.name,
|
|
127
|
-
triggerType: 'webhook',
|
|
129
|
+
const payload = paymentWebhook.payload.parse({
|
|
130
|
+
id: 'evt_123',
|
|
131
|
+
type: 'payment.completed',
|
|
132
|
+
amount: 5000,
|
|
128
133
|
});
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
const passed = paymentWebhook.filter?.(payload, request);
|
|
132
|
-
expect(passed).toBe(true);
|
|
135
|
+
expect(paymentWebhook.toManifest().runtime.filterSchema).toBeDefined();
|
|
133
136
|
|
|
134
137
|
const bound = paymentWebhook({
|
|
135
138
|
transform: (p) => ({ eventId: p.id, amount: p.amount }),
|
|
@@ -141,8 +144,8 @@ it('maps a valid webhook event into workflow input', async () => {
|
|
|
141
144
|
|
|
142
145
|
## What to validate
|
|
143
146
|
|
|
144
|
-
- webhook
|
|
145
|
-
- filter
|
|
146
|
-
- idempotency
|
|
147
|
+
- webhook payload parsing
|
|
148
|
+
- declarative filter metadata
|
|
149
|
+
- webhook idempotency config when defined
|
|
147
150
|
- polling response shape
|
|
148
151
|
- transform correctness from trigger payload to workflow input
|
|
@@ -93,6 +93,7 @@ Teach this mental model clearly:
|
|
|
93
93
|
- `Workflow.run(...)` coordinates steps, child workflows, waits, hooks, and agents
|
|
94
94
|
- `Step.run(...)` does low-level operational work and returns typed output
|
|
95
95
|
- triggers are listed in `Workflow({ triggers: [...] })`; call a trigger with `{ transform }` to bind payload mapping
|
|
96
|
+
- trigger filters are declarative Zod schemas on the trigger or on `.narrow({ id, filter })`, not workflow attachment callbacks
|
|
96
97
|
- tasks are different: use `Task` when the job is “trigger -> prompt -> agent run”
|
|
97
98
|
|
|
98
99
|
`Step`, `Tool`, and `Operation` are the same class. In this skill, default to `Step` because that matches workflow author language.
|
|
@@ -136,7 +137,7 @@ Teach these rules:
|
|
|
136
137
|
- Use `workflowGlobals` for typed workflow-wide runtime values.
|
|
137
138
|
- Use `CredentialSet` for secrets and integration auth.
|
|
138
139
|
- Do not use `process.env` in authored workflow or step code.
|
|
139
|
-
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/
|
|
140
|
+
- Follow Zod v4 syntax in examples and authored code. See `../../../.agents/skills/zod-4/SKILL.md` for deep schema guidance.
|
|
140
141
|
- Workflows can be registered as agent tools. Sync workflows return inline results; suspending workflows yield and resume later.
|
|
141
142
|
- Add `largeResultMode: 'ref'` when a workflow tool may return large data; agents inspect refs with `describe_ref`, `read_ref`, and `slice_ref`.
|
|
142
143
|
- Add `midSessionSnapshot: true` only for workflow tools that have measured benefit from preserving mid-tool-call reasoning state. The default turn-boundary yield path is simpler and should remain the default.
|
|
@@ -48,21 +48,21 @@ import { z } from 'zod';
|
|
|
48
48
|
const PayloadSchema = z.object({ accountId: z.string() });
|
|
49
49
|
|
|
50
50
|
const accountCron = cronTrigger({
|
|
51
|
-
|
|
51
|
+
id: 'nightly-account-sync',
|
|
52
|
+
description: 'Runs the account sync every night.',
|
|
52
53
|
schedule: '0 0 * * *',
|
|
53
54
|
input: PayloadSchema,
|
|
54
55
|
payload: { accountId: 'default' },
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
const accountWebhook = webhookTrigger({
|
|
58
|
-
|
|
59
|
+
id: 'account-webhook',
|
|
60
|
+
description: 'Receives account sync webhook events.',
|
|
59
61
|
source: {
|
|
60
62
|
type: 'custom',
|
|
61
|
-
method: 'POST',
|
|
62
|
-
path: '/accounts',
|
|
63
63
|
},
|
|
64
64
|
payload: z.object({ id: z.string(), action: z.string() }),
|
|
65
|
-
filter: (
|
|
65
|
+
filter: z.object({ action: z.literal('sync') }),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
export const accountSyncWorkflow = new Workflow({
|
|
@@ -80,7 +80,7 @@ export const accountSyncWorkflow = new Workflow({
|
|
|
80
80
|
});
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Bare triggers (like `accountCron` above) pass their payload directly as workflow input. Call the trigger with `{ transform }` when the payload shape differs from the workflow input.
|
|
83
|
+
Bare triggers (like `accountCron` above) pass their payload directly as workflow input. Call the trigger with `{ transform }` when the payload shape differs from the workflow input. Put filtering on the trigger as a pure Zod schema, or use `.narrow({ id, filter })` for workflow-specific filtering.
|
|
84
84
|
|
|
85
85
|
## Workflow metadata fields
|
|
86
86
|
|
|
@@ -249,7 +249,7 @@ Common operation groups:
|
|
|
249
249
|
- search: `searchCode`, `searchIssues`, `searchRepositories`
|
|
250
250
|
- users and orgs: `getAuthenticatedUser`, `getUser`, `listOrganizations`, `listOrgMembers`
|
|
251
251
|
|
|
252
|
-
Messaging-specific credential
|
|
252
|
+
Messaging-specific credential helpers also exist in this package, but workflow trigger and conversation entry guidance belongs in the trigger and agent skills.
|
|
253
253
|
|
|
254
254
|
## `@keystrokehq/google`
|
|
255
255
|
|