@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 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
- ├── AGENTS.md
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/AGENTS.md` is the Keystroke project context written or appended to project-root `AGENTS.md` by `keystroke init`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keystrokehq/skills",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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 that trigger's `TriggerAttachment` values
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/rules/zod-v4-requirements.md`.
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 surface from the agent, its tools, its MCP servers, and its messaging gateways
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
- - only the trigger `verify` callback receives credentials through its callback context (`filter`, `idempotencyKey`, and `transform` do not receive credentials)
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/rules/zod-v4-requirements.md`.
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 the public credential surface
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 surfaces as
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 { WebhookTrigger } from '@keystrokehq/core';
643
+ import { pollingTrigger } from '@keystrokehq/core';
644
644
  import { z } from 'zod';
645
645
 
646
- export const signedWebhook = new WebhookTrigger({
647
- name: 'Signed Webhook',
648
- description: 'Verifies a webhook with a credential-backed secret.',
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
- payload: z.object({
653
- id: z.string(),
650
+ schedule: '5m',
651
+ response: z.object({
652
+ records: z.array(z.object({ id: z.string() })),
654
653
  }),
655
- verify: async (_request, ctx) => {
656
- const secret = ctx.credentials.crmApi.apiKey;
657
- if (!secret) {
658
- throw new Error('Missing signing secret');
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 the trigger itself must verify or authenticate incoming events.
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
- - trigger `verify` callback and polling trigger `poll` callback (`filter`, `idempotencyKey`, and `transform` do not receive credentials)
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
- name: 'Welcome Webhook',
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 name: {{trigger.name}}.',
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.name}}`
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/rules/zod-v4-requirements.md`.
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
- name: 'Inbound Webhook',
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
- name: 'Daily Report Trigger',
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.name}} using {{trigger.payload}}.',
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
- name: 'Stale Ticket Polling',
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: (payload) => payload.status === 'stale',
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
- - `name`
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.name}}` — resolved to the trigger name
52
- - `{{trigger.type}}` — resolved to the trigger type (one of `cron`, `polling`, `provider`, `webhook`)
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 use `TriggerAttachment`
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, polling, and provider triggers with @keystrokehq/core. Use when the user wants to author, test, or explain trigger code, including webhook verification, polling schedules, filter and idempotency callbacks, and mapping trigger input into workflows or task-driven agent runs.
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
- name: 'Payment Webhook',
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: (payload) => payload.type === 'payment.completed',
40
- idempotencyKey: (payload) => payload.id,
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
- name: 'Daily Reminder Trigger',
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.name}}.',
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
- Default to cron, webhook, and polling first. Use provider triggers when the user specifically needs provider-event authoring.
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 (`.name`, `.toManifest()`, etc.) that is also callable as a function to create a bound trigger with optional `transform` and `filter`. Webhook and polling triggers also expose `.filter` and `.idempotencyKey` callbacks; cron triggers do not.
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 `verify` for authenticity or admission checks (webhook triggers only).
160
- - Use `filter` for event gating (webhook and polling triggers only cron triggers do not support `filter`).
161
- - Use `idempotencyKey` for deduplication or stable identity (webhook and polling triggers only cron triggers do not support `idempotencyKey`).
162
- - `filter` and `idempotencyKey` receive the typed parsed payload as the first argument. Only `verify` receives credentials.
163
- - For webhook providers, verify against the raw request data the runtime gives you.
164
- - Keep the webhook path rooted with `/` and provide only the suffix that follows the organization id in the URL e.g. `path: '/payments'` is served at `/api/v1/webhooks/{orgId}/payments`. Do **not** include the `/webhooks/` prefix; the router supplies it.
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 logic in callbacks. Always run the tests to verify that the new triggers run (see `references/testing.md`).
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 trigger-specific callbacks such as `verify`, `filter`, or `poll` directly on the trigger
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 provider triggers
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
- 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
+ },
97
87
  });
98
88
  ```
99
89
 
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))`.
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
- 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
@@ -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
- name: 'large-payments',
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, webhookTrigger } from '@keystrokehq/core';
229
+ import { CredentialSet, pollingTrigger } from '@keystrokehq/core';
235
230
  import { z } from 'zod';
236
231
 
237
- const signingCredentials = new CredentialSet({
238
- id: 'webhookSigning',
232
+ const crmCredentials = new CredentialSet({
233
+ id: 'crmApi',
239
234
  auth: z.object({
240
- secret: z.string(),
235
+ apiKey: z.string(),
241
236
  }),
242
237
  });
243
238
 
244
- const signedWebhook = webhookTrigger({
245
- name: 'Signed Webhook',
246
- description: 'Verifies a signed webhook.',
247
- source: {
248
- type: 'custom',
249
- method: 'POST',
250
- path: '/signed',
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
- ExecutionIdentityPolicy,
14
+ IdempotencyKeyConfig,
16
15
  TriggerBindOptions,
17
- TriggerEntry,
18
- TriggerInstance,
19
- } from '@keystrokehq/core/types';
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
- - `name`
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
- - `.name`
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
- - `path`
61
- - `method`
59
+ - `id`
60
+ - `description`
61
+ - `source`
62
62
  - `payload`
63
- - `verify`
64
63
  - `filter`
65
64
  - `idempotencyKey`
66
- - `response`
67
65
 
68
- ## `webhookTrigger` instance methods
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
- - `.verify?(request, ctx)` — raw request + credentials for authenticity checks
72
- - `.filter?(payload, request?)` — typed parsed body, optional raw request (no credentials)
73
- - `.idempotencyKey?(payload, request?)` — typed parsed body, optional raw request (no credentials)
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?(payload)` — typed parsed payload (no credentials)
88
- - `.idempotencyKey?(payload)` — typed parsed payload (no credentials)
98
+ - `.filter` — pure Zod schema metadata, not a callback
89
99
 
90
- ## `providerTrigger` fields
100
+ Polling triggers do not support idempotency config.
91
101
 
92
- - `provider`
93
- - `eventTypes`
94
- - `appRef`
95
- - `filter`
96
- - `idempotencyKey`
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({ name, filter })` and attach that — `.narrow()` is the only authoring path for per-target filtering. Binding-level `filter` and `idempotencyKey` are intentionally not part of the API.
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 a `TriggerEntry[]`:
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 callback and bound trigger tests
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 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.
@@ -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.
@@ -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