@keystrokehq/skills 0.0.3 → 0.0.6

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +24 -15
  3. package/package.json +3 -10
  4. package/{AGENTS-blurb.md → src/_AGENTS.md} +1 -1
  5. package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/SKILL.md +1 -1
  6. package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/source-map.md +1 -1
  7. package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/testing.md +1 -1
  8. package/{keystroke-cli-workspace → src/keystroke-cli-workspace}/SKILL.md +1 -1
  9. package/{keystroke-cli-workspace → src/keystroke-cli-workspace}/references/command-map.md +3 -3
  10. package/{keystroke-cli-workspace → src/keystroke-cli-workspace}/references/project-lifecycle.md +1 -1
  11. package/{keystroke-credential-binding → src/keystroke-credential-binding}/SKILL.md +4 -3
  12. package/{keystroke-credential-binding → src/keystroke-credential-binding}/references/patterns.md +14 -15
  13. package/{keystroke-credential-binding → src/keystroke-credential-binding}/references/source-map.md +2 -1
  14. package/{keystroke-task-authoring → src/keystroke-task-authoring}/SKILL.md +4 -6
  15. package/{keystroke-task-authoring → src/keystroke-task-authoring}/references/patterns.md +7 -7
  16. package/{keystroke-task-authoring → src/keystroke-task-authoring}/references/source-map.md +4 -5
  17. package/{keystroke-trigger-authoring → src/keystroke-trigger-authoring}/SKILL.md +26 -20
  18. package/{keystroke-trigger-authoring → src/keystroke-trigger-authoring}/references/patterns.md +62 -66
  19. package/src/keystroke-trigger-authoring/references/source-map.md +166 -0
  20. package/{keystroke-trigger-authoring → src/keystroke-trigger-authoring}/references/testing.md +30 -27
  21. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/SKILL.md +4 -3
  22. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/references/patterns.md +6 -6
  23. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/references/prebuilt-integrations.md +1 -1
  24. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/references/runtime-helpers.md +2 -2
  25. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/references/source-map.md +1 -1
  26. package/{keystroke-workflow-authoring → src/keystroke-workflow-authoring}/references/testing.md +2 -2
  27. package/keystroke-agent-authoring/evals/evals.json +0 -29
  28. package/keystroke-cli-workspace/evals/evals.json +0 -23
  29. package/keystroke-credential-binding/evals/evals.json +0 -29
  30. package/keystroke-data-toolkit/evals/evals.json +0 -23
  31. package/keystroke-task-authoring/evals/evals.json +0 -23
  32. package/keystroke-trigger-authoring/evals/evals.json +0 -29
  33. package/keystroke-trigger-authoring/references/source-map.md +0 -128
  34. package/keystroke-workflow-as-tool-debugging/evals/evals.json +0 -23
  35. package/keystroke-workflow-authoring/evals/evals.json +0 -29
  36. /package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/messaging-gateways.md +0 -0
  37. /package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/patterns.md +0 -0
  38. /package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/prebuilt-integrations.md +0 -0
  39. /package/{keystroke-agent-authoring → src/keystroke-agent-authoring}/references/sandbox-and-mcp.md +0 -0
  40. /package/{keystroke-cli-workspace → src/keystroke-cli-workspace}/references/credentials-and-connect.md +0 -0
  41. /package/{keystroke-credential-binding → src/keystroke-credential-binding}/references/cli.md +0 -0
  42. /package/{keystroke-data-toolkit → src/keystroke-data-toolkit}/SKILL.md +0 -0
  43. /package/{keystroke-data-toolkit → src/keystroke-data-toolkit}/references/usage.md +0 -0
  44. /package/{keystroke-workflow-as-tool-debugging → src/keystroke-workflow-as-tool-debugging}/SKILL.md +0 -0
  45. /package/{keystroke-workflow-as-tool-debugging → src/keystroke-workflow-as-tool-debugging}/references/playbook.md +0 -0
@@ -13,11 +13,9 @@ File layout reminder:
13
13
  ## Shared advanced fields
14
14
 
15
15
  ```ts
16
- import type { ExecutionIdentityPolicy } from '@keystrokehq/core/types';
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
- name: 'Nightly Digest Trigger',
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
- name: 'Payment Webhook',
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: (payload) => payload.type === 'payment.completed',
96
- idempotencyKey: (payload) => payload.id,
80
+ filter: z.object({
81
+ type: z.literal('payment.completed'),
82
+ }),
83
+ idempotencyKey: {
84
+ from: 'payload',
85
+ path: 'id',
86
+ },
87
+ });
88
+ ```
89
+
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
+ }),
97
107
  });
98
108
  ```
99
109
 
100
- Use `request.rawBody` in `verify` when a provider expects raw request verification. To parse a webhook body, use `trigger.payload.parse(JSON.parse(request.rawBody))`.
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
- name: 'Order Polling',
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: (payload) => payload.status === 'created',
148
- idempotencyKey: (payload) => payload.orderId,
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
@@ -202,12 +197,16 @@ triggers: [
202
197
 
203
198
  Call the trigger as a function with `{ transform }` when the trigger payload and workflow input are different shapes. This returns a `BoundTrigger`.
204
199
 
205
- ## Bound trigger with additional `filter`
200
+ ## Narrow a trigger for per-workflow filtering
206
201
 
207
202
  ```ts
203
+ const largePayments = paymentWebhook.narrow({
204
+ id: 'large-payments',
205
+ filter: z.object({ amount: z.number().gt(100) }),
206
+ });
207
+
208
208
  triggers: [
209
- paymentWebhook({
210
- filter: (payload) => payload.amount > 100,
209
+ largePayments({
211
210
  transform: (payload) => ({
212
211
  eventId: payload.id,
213
212
  amount: payload.amount,
@@ -216,7 +215,7 @@ triggers: [
216
215
  ]
217
216
  ```
218
217
 
219
- An additional filter on the binding composes with the trigger's own filter.
218
+ Filtering and idempotency live on the trigger or its `.narrow()` derivatives — never on the attachment. Bindings accept only `transform`. Use `.narrow()` to produce a filtered child trigger and attach that to the workflow.
220
219
 
221
220
  ## Task trigger note
222
221
 
@@ -227,36 +226,33 @@ Use the task skill when the user needs prompt templating, task lifecycle, or foc
227
226
  ## Trigger credentials
228
227
 
229
228
  ```ts
230
- import { CredentialSet, webhookTrigger } from '@keystrokehq/core';
229
+ import { CredentialSet, pollingTrigger } from '@keystrokehq/core';
231
230
  import { z } from 'zod';
232
231
 
233
- const signingCredentials = new CredentialSet({
234
- id: 'webhookSigning',
232
+ const crmCredentials = new CredentialSet({
233
+ id: 'crmApi',
235
234
  auth: z.object({
236
- secret: z.string(),
235
+ apiKey: z.string(),
237
236
  }),
238
237
  });
239
238
 
240
- const signedWebhook = webhookTrigger({
241
- name: 'Signed Webhook',
242
- description: 'Verifies a signed webhook.',
243
- source: {
244
- type: 'custom',
245
- method: 'POST',
246
- path: '/signed',
247
- credentialSets: [signingCredentials],
248
- verify: async (_request, ctx) => {
249
- if (!ctx.credentials.webhookSigning.secret) {
250
- throw new Error('Missing signing secret');
251
- }
252
- },
253
- },
254
- payload: z.object({
255
- 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() })),
256
246
  }),
247
+ poll: async (ctx) => {
248
+ const apiKey = ctx.credentials.crmApi.apiKey;
249
+ return { customers: [{ id: `customer-for-${apiKey}` }] };
250
+ },
257
251
  });
258
252
  ```
259
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
+
260
256
  ## `describe()` and `toManifest()`
261
257
 
262
258
  ```ts
@@ -0,0 +1,166 @@
1
+ # Trigger Feature Map
2
+
3
+ Use only the public imports a user repo can rely on:
4
+
5
+ ```ts
6
+ import {
7
+ cronTrigger,
8
+ pollingTrigger,
9
+ webhookTrigger,
10
+ } from '@keystrokehq/core';
11
+ import type {
12
+ BoundTrigger,
13
+ CallableTrigger,
14
+ IdempotencyKeyConfig,
15
+ TriggerBindOptions,
16
+ TriggerManifest,
17
+ WebhookRequest,
18
+ } from '@keystrokehq/core/trigger';
19
+ ```
20
+
21
+ When a trigger explanation also needs to talk about workflow steps or agent tools, use the terminology from the other Keystroke skills:
22
+
23
+ - `Step`, `Tool`, and `Operation` are aliases for the same `Operation` class
24
+ - use the workflow skill for workflow-side operation guidance
25
+ - use the agent skill for agent-side tool guidance
26
+
27
+ ## Common trigger fields
28
+
29
+ - `id`
30
+ - `description`
31
+ - `enabled`
32
+ - `credentialSets`
33
+ - `executionIdentityPolicy`
34
+ - `modeDefault`
35
+
36
+ ## Common trigger methods
37
+
38
+ - `describe()`
39
+ - `toManifest()`
40
+
41
+ ## `cronTrigger` fields
42
+
43
+ - `input`
44
+ - `payload`
45
+ - `schedule`
46
+ - `timezone`
47
+
48
+ ## `cronTrigger` instance properties
49
+
50
+ - `.id`
51
+ - `.payload`
52
+ - `.schedule`
53
+ - `.timezone`
54
+ - `.toManifest()`
55
+ - `.describe()`
56
+
57
+ ## `webhookTrigger` fields
58
+
59
+ - `id`
60
+ - `description`
61
+ - `source`
62
+ - `payload`
63
+ - `filter`
64
+ - `idempotencyKey`
65
+
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
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`
78
+ - `.payload.parse(data)` — validate parsed body against the payload schema
79
+ - `.filter` — pure Zod schema metadata, not a callback
80
+ - `.idempotencyKey` — declarative `IdempotencyKeyConfig`, not a callback
81
+ - `.toManifest()`
82
+ - `.describe()`
83
+
84
+ ## `pollingTrigger` fields
85
+
86
+ - `id`
87
+ - `description`
88
+ - `schedule`
89
+ - `response`
90
+ - `poll`
91
+ - `filter`
92
+
93
+ ## `pollingTrigger` instance properties and methods
94
+
95
+ - `.id`
96
+ - `.poll(ctx)`
97
+ - `.parseResponse(response)`
98
+ - `.filter` — pure Zod schema metadata, not a callback
99
+
100
+ Polling triggers do not support idempotency config.
101
+
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.
136
+
137
+ ## Calling a trigger (creating a bound trigger)
138
+
139
+ All factory-created triggers are callable. Calling one returns a `BoundTrigger`:
140
+
141
+ - `trigger()` — bare binding (no transform)
142
+ - `trigger({ transform })` — bound with payload-to-input mapping
143
+
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.
145
+
146
+ ## `BoundTrigger`
147
+
148
+ - `.isBoundTrigger` — always `true`
149
+ - `.trigger` — reference to the underlying trigger instance
150
+ - `.transform?` — payload-to-workflow-input mapping function
151
+
152
+ ## Workflow `triggers` array
153
+
154
+ `Workflow({ triggers: [...] })` accepts trigger entries:
155
+ - a bare `CallableTrigger` (trigger payload passes through as workflow input)
156
+ - a `BoundTrigger` (returned by calling the trigger with options)
157
+
158
+ ## Task note
159
+
160
+ - task triggers are declared inline in `Task.triggers`
161
+ - messaging gateways belong to agent authoring, not trigger authoring
162
+
163
+ ## Where to read next
164
+
165
+ - `patterns.md` for trigger field examples
166
+ - `testing.md` for trigger metadata, polling, and bound trigger tests
@@ -10,14 +10,14 @@ Assume `paymentWebhook`, `orderPolling`, and `paymentWorkflow` are the public tr
10
10
 
11
11
  ```ts
12
12
  import { defineConfig } from 'vitest/config';
13
- import { keystrokeTestPlugin } from '@keystrokehq/testing/vitest';
13
+ import { keystrokeTestPlugin } from '@keystrokehq/testing';
14
14
 
15
15
  export default defineConfig({
16
16
  plugins: [keystrokeTestPlugin()],
17
17
  });
18
18
  ```
19
19
 
20
- ## Test webhook verification and filtering
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
- await paymentWebhook.verify?.(request, {
38
- credentials: {},
39
- triggerName: paymentWebhook.name,
40
- triggerType: 'webhook',
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 payload = paymentWebhook.payload.parse(JSON.parse(request.rawBody));
44
- const shouldRun = paymentWebhook.filter?.(payload, request);
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 `filter`
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
- triggerName: orderPolling.name,
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 (headers, query params, etc.). Note that only `verify` receives credentials `transform`, `filter`, and `idempotencyKey` do not.
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 request = mockRequest({
120
- headers: { 'x-signature': 'signed' },
121
- rawBody: JSON.stringify({ id: 'evt_123', type: 'payment.completed', amount: 5000 }),
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
- const payload = paymentWebhook.payload.parse(JSON.parse(request.rawBody));
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 verification success and failure
145
- - filter behavior
146
- - idempotency key behavior when defined
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.
@@ -123,7 +124,7 @@ Teach these rules:
123
124
  - waits and hooks
124
125
  5. Put operational work in steps, shared operations, or agents.
125
126
  6. Keep each exported primitive in its own typed file.
126
- 7. Finish with tests using `@keystrokehq/testing/vitest`.
127
+ 7. Finish with tests using `@keystrokehq/testing`.
127
128
 
128
129
  ## Workflow rules
129
130
 
@@ -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/rules/zod-v4-requirements.md`.
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.
@@ -202,7 +203,7 @@ keystroke workflows run <authoredWorkflowId> --input '{}' --wait
202
203
  keystroke workflows run <authoredWorkflowId> --input '{}' --follow
203
204
  ```
204
205
 
205
- `workflows run` executes the current deployed snapshot. It is different from `workflows try-deploy`, which builds local code and runs a temporary artifact.
206
+ `workflows run` executes the current deployed snapshot. It is different from `workflows test`, which builds local code and runs a temporary artifact.
206
207
 
207
208
  To explicitly start a workflow via API:
208
209
  - **Endpoint**: `POST /api/v1/workflows/execute`
@@ -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
- name: 'Nightly Account Sync',
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
- name: 'Account Webhook',
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: (p) => p.action === 'sync',
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 surfaces also exist in this package, but workflow trigger and conversation entry guidance belongs in the trigger and agent skills.
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
 
@@ -239,7 +239,7 @@ Use these when operation behavior depends on retry state or when you want the st
239
239
 
240
240
  ## Public testing helpers
241
241
 
242
- Import these from `@keystrokehq/testing/vitest`:
242
+ Import these from `@keystrokehq/testing`:
243
243
 
244
244
  - `keystrokeTestPlugin`
245
245
  - `createTestRuntime`
@@ -256,7 +256,7 @@ Import these from `@keystrokehq/testing/vitest`:
256
256
  Example:
257
257
 
258
258
  ```ts
259
- import { createMockHook } from '@keystrokehq/testing/vitest';
259
+ import { createMockHook } from '@keystrokehq/testing';
260
260
 
261
261
  const hook = createMockHook();
262
262
  await hook;
@@ -9,7 +9,7 @@ import {
9
9
  createTestRuntime,
10
10
  createTestStepContext,
11
11
  keystrokeTestPlugin,
12
- } from '@keystrokehq/testing/vitest';
12
+ } from '@keystrokehq/testing';
13
13
  ```
14
14
 
15
15
  `Operation`, `Step`, and `Tool` are aliases for the same class. In this workflow skill, prefer `Step` for examples and explanations, but `Operation` is equally valid for shared or integration-oriented code.
@@ -10,7 +10,7 @@ Assume `helloWorkflow`, `accountSyncWorkflow`, `loadCustomer`, `paymentWebhook`,
10
10
 
11
11
  ```ts
12
12
  import { defineConfig } from 'vitest/config';
13
- import { keystrokeTestPlugin } from '@keystrokehq/testing/vitest';
13
+ import { keystrokeTestPlugin } from '@keystrokehq/testing';
14
14
 
15
15
  export default defineConfig({
16
16
  plugins: [keystrokeTestPlugin()],
@@ -40,7 +40,7 @@ Use it when a workflow test needs more than plain input data.
40
40
 
41
41
  ```ts
42
42
  import { createTestRuntime } from '@keystrokehq/testing';
43
- import { createMockHook } from '@keystrokehq/testing/vitest';
43
+ import { createMockHook } from '@keystrokehq/testing';
44
44
 
45
45
  const runtime = createTestRuntime({
46
46
  workflowGlobals: {
@@ -1,29 +0,0 @@
1
- {
2
- "skill_name": "keystroke-agent-authoring",
3
- "evals": [
4
- {
5
- "id": 1,
6
- "prompt": "I need a Keystroke agent that can look up Slack users and DM one of them. What fields should I set up, and where do tools and credentials go?",
7
- "expected_output": "Explains the canonical Keystroke agent path, the important agent fields, tool configuration, and the credential relationship between the agent and its tools.",
8
- "files": []
9
- },
10
- {
11
- "id": 2,
12
- "prompt": "Should this be a Step or an agent? The task is to inspect some repo files, decide what changed, and then call a couple of tools depending on what it finds.",
13
- "expected_output": "Explains when an agent is appropriate, when a normal step would be better, and how workflow orchestration relates to agent scope.",
14
- "files": []
15
- },
16
- {
17
- "id": 3,
18
- "prompt": "Show me how to build a sandboxed Keystroke coding agent that can work in a checked-out repo and maybe use MCP later if needed.",
19
- "expected_output": "Uses the public sandbox and MCP patterns, points to MCP references when needed, and keeps workflow orchestration separate.",
20
- "files": []
21
- },
22
- {
23
- "id": 4,
24
- "prompt": "I want my Keystroke agent to answer messages from GitHub conversations. Should I use a trigger or something else?",
25
- "expected_output": "Explains that conversational entry belongs on Agent.messaging with a MessagingGateway, not on workflow triggers, and keeps the distinction between tasks, triggers, and conversations clear.",
26
- "files": []
27
- }
28
- ]
29
- }