@keystrokehq/cli 0.1.24 → 0.1.26
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/dist/index.mjs +216 -153
- package/dist/index.mjs.map +1 -1
- package/dist/{maybe-auto-update-q5MthdI8.mjs → maybe-auto-update-BDvSKDZp.mjs} +2 -2
- package/dist/{maybe-auto-update-q5MthdI8.mjs.map → maybe-auto-update-BDvSKDZp.mjs.map} +1 -1
- package/dist/skills-bundle/_AGENTS.md +1 -1
- package/dist/templates/hello-world/README.md +3 -2
- package/dist/{version-DcR3O1UD.mjs → version-pY9N8XlL.mjs} +1 -2
- package/dist/version-pY9N8XlL.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/skills-bundle/skills/keystroke-actions/SKILL.md +0 -160
- package/dist/skills-bundle/skills/keystroke-actions/references/catalog-and-imports.md +0 -71
- package/dist/skills-bundle/skills/keystroke-agents/SKILL.md +0 -115
- package/dist/skills-bundle/skills/keystroke-agents/references/models.md +0 -23
- package/dist/skills-bundle/skills/keystroke-agents/references/tools-mcp-codemode.md +0 -85
- package/dist/skills-bundle/skills/keystroke-agents/references/workflows-and-testing.md +0 -26
- package/dist/skills-bundle/skills/keystroke-apps/SKILL.md +0 -151
- package/dist/skills-bundle/skills/keystroke-apps/references/cli-and-catalog.md +0 -104
- package/dist/skills-bundle/skills/keystroke-channels/SKILL.md +0 -66
- package/dist/skills-bundle/skills/keystroke-channels/references/slack-setup.md +0 -41
- package/dist/skills-bundle/skills/keystroke-cli/SKILL.md +0 -93
- package/dist/skills-bundle/skills/keystroke-deploy/SKILL.md +0 -93
- package/dist/skills-bundle/skills/keystroke-deploy/references/build-and-full-deploy.md +0 -30
- package/dist/skills-bundle/skills/keystroke-deploy/references/filtered-deploy.md +0 -50
- package/dist/skills-bundle/skills/keystroke-deploy/references/wip-ignore.md +0 -35
- package/dist/skills-bundle/skills/keystroke-files/SKILL.md +0 -43
- package/dist/skills-bundle/skills/keystroke-skills/SKILL.md +0 -42
- package/dist/skills-bundle/skills/keystroke-triggers/SKILL.md +0 -143
- package/dist/skills-bundle/skills/keystroke-workflows/SKILL.md +0 -78
- package/dist/skills-bundle/skills/keystroke-workflows/references/authoring.md +0 -168
- package/dist/skills-bundle/skills/keystroke-workflows/references/testing.md +0 -138
- package/dist/version-DcR3O1UD.mjs.map +0 -1
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Build and full deploy
|
|
2
|
-
|
|
3
|
-
## What deploy does
|
|
4
|
-
|
|
5
|
-
1. `walkProject` with `phase: "deploy"` — applies `@keystroke ignore:deploy`, collects source snapshot for the dashboard.
|
|
6
|
-
2. `buildApp` — tsdown with `clean: true`; writes `dist/` and `dist/.keystroke/route-manifest.json`.
|
|
7
|
-
3. `packProjectArtifact` — gzip tarball of `dist/`.
|
|
8
|
-
4. Platform `artifacts.create` → PUT tarball → `complete` → blue/green promote.
|
|
9
|
-
|
|
10
|
-
Source files upload in parallel (content-addressed); failure there warns but does not block deploy. The dashboard source snapshot is capped (per-file 256KB, total 8MB, 2000 files) and skips binaries, `.env`, and `.log` files — so `.env` is never uploaded.
|
|
11
|
-
|
|
12
|
-
After upload, the CLI polls the project status (every ~2s, up to a 120s timeout) until it reports `active` or `failed`; on failure it surfaces the platform's `lastError`. A hang usually means the platform promotion is still in progress.
|
|
13
|
-
|
|
14
|
-
## Entry keys
|
|
15
|
-
|
|
16
|
-
Build entry keys mirror `dist/` paths without `.mjs`:
|
|
17
|
-
|
|
18
|
-
| Source file | Entry key |
|
|
19
|
-
| ------------------------------ | ---------------------- |
|
|
20
|
-
| `src/agents/support.ts` | `agents/support` |
|
|
21
|
-
| `src/workflows/morning-check.ts` | `workflows/morning-check` |
|
|
22
|
-
| `src/triggers/incoming-message.ts` | `triggers/incoming-message` |
|
|
23
|
-
|
|
24
|
-
Nested agent dirs use the same `agents/<id>` pattern from discovery.
|
|
25
|
-
|
|
26
|
-
## Freshness
|
|
27
|
-
|
|
28
|
-
Full deploy always runs `buildApp` before pack. Default `clean: true` removes prior `dist/` output before rebuild. You do not need a separate `keystroke build` before deploy.
|
|
29
|
-
|
|
30
|
-
Orphan files from removed modules are unlikely to affect routing — `route-manifest.json` is regenerated and drives HTTP routes.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Filtered deploy (`--filter`)
|
|
2
|
-
|
|
3
|
-
Ship changes to one or a few modules without a full rebuild of every agent, workflow, and trigger.
|
|
4
|
-
|
|
5
|
-
## Command
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
keystroke deploy --project <slug> --filter workflows/morning-check
|
|
9
|
-
keystroke deploy --project <slug> --filter agents/support --filter workflows/signup-pipeline
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
(`--project` can go before or after `deploy`, and can be omitted after the first deploy.)
|
|
13
|
-
|
|
14
|
-
- Exact entry keys only — no globs, no negation, no slug aliases.
|
|
15
|
-
- First deploy for a project must be **full** (no `--filter`).
|
|
16
|
-
- Without an active artifact, deploy errors: run a full deploy first.
|
|
17
|
-
|
|
18
|
-
## Pipeline
|
|
19
|
-
|
|
20
|
-
```text
|
|
21
|
-
isolated single-entry build per --filter match (self-contained .mjs)
|
|
22
|
-
→ download active prod tarball
|
|
23
|
-
→ extract → overlay rebuilt files → merge route-manifest by moduleFile
|
|
24
|
-
→ re-tar → normal artifacts.create / PUT / complete
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Isolated builds
|
|
28
|
-
|
|
29
|
-
Each filtered module is built alone in a scratch dir. Output is self-contained (no shared `../chunk-*.mjs` imports). The merge overlays only `dist/<kind>/<id>.mjs` (+ `.map`) onto the extracted prod tree.
|
|
30
|
-
|
|
31
|
-
## Manifest merge
|
|
32
|
-
|
|
33
|
-
For each rebuilt module, base manifest entries with the same `moduleFile` are dropped; rebuilt entries are appended. Health, plugins, skills, integrations, and untouched modules stay from the active artifact.
|
|
34
|
-
|
|
35
|
-
## What is not rebuilt
|
|
36
|
-
|
|
37
|
-
- `config.mjs` and built-in integration routes
|
|
38
|
-
- `dist/.keystroke/assets.mjs` and skill files on disk
|
|
39
|
-
- Any module not listed in `--filter`
|
|
40
|
-
|
|
41
|
-
Change `keystroke.config.ts` or project-wide assets → run a **full** deploy.
|
|
42
|
-
|
|
43
|
-
## Verify
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
keystroke project deployments list --project <slug> # new version active
|
|
47
|
-
keystroke workflow run <workflow-slug> --input '{}'
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Confirm untouched routes still work after a filtered deploy of a different module.
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# WIP modules and `@keystroke ignore`
|
|
2
|
-
|
|
3
|
-
Place the directive in the **leading comment block** of a module file under `src/agents/`, `src/workflows/`, or `src/triggers/` (the scan stops at the first non-comment line). Line (`//`) and block (`/* … */`, `*`) comments both work.
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
// @keystroke ignore
|
|
7
|
-
// @keystroke ignore:deploy
|
|
8
|
-
/** @keystroke ignore */
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Only `ignore` and `ignore:deploy` are valid. Any other scope (e.g. `// @keystroke ignore:build`) throws `Unknown @keystroke ignore target "…"` at build time.
|
|
12
|
-
|
|
13
|
-
## Directives
|
|
14
|
-
|
|
15
|
-
| Directive | Local build (`build`, `dev`, `start`) | Deploy (`phase: deploy`) |
|
|
16
|
-
| ---------------------- | ------------------------------------- | ------------------------ |
|
|
17
|
-
| `// @keystroke ignore` | Skipped | Skipped |
|
|
18
|
-
| `// @keystroke ignore:deploy` | Included | Skipped |
|
|
19
|
-
|
|
20
|
-
## When to use which
|
|
21
|
-
|
|
22
|
-
- **`ignore`** — module is broken, experimental, or not ready to run anywhere.
|
|
23
|
-
- **`ignore:deploy`** — safe to run locally; not ready for production yet.
|
|
24
|
-
|
|
25
|
-
## Import guard
|
|
26
|
-
|
|
27
|
-
If a **shipped** module imports a file marked `@keystroke ignore`, the build **fails in every phase**. If it imports an `@keystroke ignore:deploy` file, the local build/dev succeeds and only the **deploy** build fails (because `ignore:deploy` files are excluded only in the deploy phase). Compose in workflows instead of importing ignored actions from production modules.
|
|
28
|
-
|
|
29
|
-
## Triggers
|
|
30
|
-
|
|
31
|
-
A trigger attached to an ignored workflow should use the same ignore treatment, or the attachment will not behave as expected across build phases.
|
|
32
|
-
|
|
33
|
-
## Filtered deploy note
|
|
34
|
-
|
|
35
|
-
`ignore:deploy` omits a module from the **new** build. It does **not** keep the last prod version — full deploy replaces the entire artifact. Use filtered deploy when you want to update one live module while carrying forward the rest of prod.
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-files
|
|
3
|
-
description: Add static files to agent workspaces from src/files/ — product docs, instructions, context. Use with defineSandbox on defineAgent.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: files
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Files
|
|
9
|
-
|
|
10
|
-
Files under `src/files/` mount into the agent sandbox at `/workspace/agent/`.
|
|
11
|
-
|
|
12
|
-
## Layout
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
src/files/support/
|
|
16
|
-
product-guide.md
|
|
17
|
-
support-instructions.md
|
|
18
|
-
runbooks/
|
|
19
|
-
refunds.md
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Nested directories are preserved — `src/files/support/runbooks/refunds.md` mounts at `/workspace/agent/runbooks/refunds.md`.
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
import { defineAgent } from "@keystrokehq/keystroke/agent";
|
|
26
|
-
import { defineSandbox } from "@keystrokehq/keystroke/sandbox";
|
|
27
|
-
|
|
28
|
-
export default defineAgent({
|
|
29
|
-
slug: "support",
|
|
30
|
-
systemPrompt: "Read /workspace/agent/product-guide.md before answering.",
|
|
31
|
-
sandbox: defineSandbox({ files: true }), // uses agent slug → src/files/support/
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Use `defineSandbox({ files: "shared" })` to mount `src/files/shared/` instead — the string value names the folder under `src/files/`.
|
|
36
|
-
|
|
37
|
-
If the named folder doesn't exist, the build fails with a clear error — create the folder (it must contain at least one file) before deploying.
|
|
38
|
-
|
|
39
|
-
Files are seeded **write-once** at sandbox startup: the agent can edit them at runtime, but those edits aren't persisted back to `src/files/` and reset on the next run. Treat `src/files/` as read-only seed content.
|
|
40
|
-
|
|
41
|
-
Point the agent at paths in `systemPrompt`. After deploy, verify behavior with `keystroke agent prompt support --message "…"`.
|
|
42
|
-
|
|
43
|
-
Related: [agents](.agents/skills/keystroke-agents/SKILL.md), [skills](.agents/skills/keystroke-skills/SKILL.md).
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-skills
|
|
3
|
-
description: Add Agent Skills playbooks in src/skills/ for defineAgent — SKILL.md format, optional references. Use when giving agents domain-specific instructions.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: skills
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Project skills
|
|
9
|
-
|
|
10
|
-
Skills are playbooks the agent loads on demand. Author in `src/skills/`, attach by folder name on the agent.
|
|
11
|
-
|
|
12
|
-
## Layout
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
src/skills/support/
|
|
16
|
-
SKILL.md
|
|
17
|
-
references/refund-policy.md # optional
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
defineAgent({
|
|
22
|
-
slug: "support",
|
|
23
|
-
skills: ["support"] /* … */,
|
|
24
|
-
});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
At runtime → `/workspace/agent/skills/support/`.
|
|
28
|
-
|
|
29
|
-
## SKILL.md
|
|
30
|
-
|
|
31
|
-
Follow [Agent Skills](https://agentskills.io/specification): YAML frontmatter with `name` (matches the folder) and `description` are the required fields; everything else (e.g. `metadata`) is optional. Keep the body short; put detail in `references/`.
|
|
32
|
-
|
|
33
|
-
## Two kinds of skills (don't confuse them)
|
|
34
|
-
|
|
35
|
-
- **`src/skills/`** — *project* Agent Skills attached to your agents via `skills: [...]`. They deploy with your project; inspect deployed ones with `keystroke skill list`.
|
|
36
|
-
- **`.agents/skills/`** — the *bundled coding-agent* guides (like this one) that `keystroke init` scaffolds. Refresh them to the current CLI version with `keystroke skills sync` (note the plural `skills`).
|
|
37
|
-
|
|
38
|
-
## External registries
|
|
39
|
-
|
|
40
|
-
Browse [skills.sh](https://skills.sh) — copy into `src/skills/{name}/` and fix `name` to match the folder.
|
|
41
|
-
|
|
42
|
-
Related: [agents](.agents/skills/keystroke-agents/SKILL.md), [files](.agents/skills/keystroke-files/SKILL.md).
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-triggers
|
|
3
|
-
description: Wire keystroke workflows to cron, webhook, and poll sources in src/triggers/. Use when adding automation or auditing trigger runs.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: triggers
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Triggers
|
|
9
|
-
|
|
10
|
-
Triggers **attach** a source to a target (a workflow or an agent). No business logic here — only schedule, endpoint, validation, and filters.
|
|
11
|
-
|
|
12
|
-
Attachment id: `{sourceSlug}:{targetSlug}` (e.g. `signup:signup-pipeline`), where the suffix is the workflow's (or agent's) `slug`.
|
|
13
|
-
|
|
14
|
-
## Cron
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { defineCronSource } from "@keystrokehq/keystroke/trigger";
|
|
18
|
-
import workflow from "../workflows/morning-check";
|
|
19
|
-
|
|
20
|
-
export default defineCronSource({
|
|
21
|
-
slug: "morning-check",
|
|
22
|
-
schedule: "0 9 * * *",
|
|
23
|
-
}).attach({ workflow });
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Webhook
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
import { defineWebhookSource } from "@keystrokehq/keystroke/trigger";
|
|
30
|
-
import { z } from "zod";
|
|
31
|
-
import workflow from "../workflows/signup-pipeline";
|
|
32
|
-
|
|
33
|
-
export default defineWebhookSource({
|
|
34
|
-
slug: "signup",
|
|
35
|
-
endpoint: "signup",
|
|
36
|
-
request: z.object({
|
|
37
|
-
name: z.string().trim().min(1),
|
|
38
|
-
email: z.string().email(),
|
|
39
|
-
company: z.string().trim().min(1),
|
|
40
|
-
}),
|
|
41
|
-
}).attach({ workflow });
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Use optional Zod `filter` for extra constraints beyond `request`:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
filter: z.object({ type: z.literal("invoice.paid") }),
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Webhooks ack asynchronously.** The POST returns immediately — `202 { runId }` when a binding matches, or `{ ok: true, skipped: true }` when nothing does. The workflow runs in the background; its output is **not** returned in the HTTP response (there's no "respond to webhook"). To return data to the caller, make an outbound call from the workflow, or have the caller poll the run via the runs API / `keystroke workflow runs get`.
|
|
51
|
-
|
|
52
|
-
### Shared endpoint (e.g. Stripe)
|
|
53
|
-
|
|
54
|
-
Multiple trigger files can use the same `endpoint` — one URL `POST /triggers/{endpoint}`, each with its own `slug`, `request`, `filter`, and `transform`. Unmatched payloads return `{ ok: true, skipped: true }`.
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
// src/triggers/stripe-invoice-paid.ts
|
|
58
|
-
export default defineWebhookSource({
|
|
59
|
-
slug: "stripe-invoice-paid",
|
|
60
|
-
endpoint: "stripe",
|
|
61
|
-
request: z.object({ type: z.string(), data: z.object({ id: z.string() }) }),
|
|
62
|
-
filter: z.object({ type: z.literal("invoice.paid") }),
|
|
63
|
-
}).attach({ workflow: invoicePaidWorkflow, transform: (p) => ({ invoiceId: p.data.id }) });
|
|
64
|
-
|
|
65
|
-
// src/triggers/stripe-subscription-deleted.ts — same endpoint, different slug/schema/filter
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
List or inspect all triggers on an endpoint:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
keystroke trigger list --endpoint stripe
|
|
72
|
-
keystroke trigger get stripe # same rows as list --endpoint (shared route)
|
|
73
|
-
keystroke trigger url stripe # one webhook URL for the route
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Use each trigger's `slug` for `trigger get` / run history (`keystroke trigger runs list stripe-invoice-paid:…`).
|
|
77
|
-
|
|
78
|
-
## Poll
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
import { definePollSource } from "@keystrokehq/keystroke/trigger";
|
|
82
|
-
import workflow from "../workflows/new-inbox";
|
|
83
|
-
|
|
84
|
-
export default definePollSource({
|
|
85
|
-
slug: "new-inbox",
|
|
86
|
-
schedule: "*/5 * * * *",
|
|
87
|
-
run: () => ({ emails: [] }),
|
|
88
|
-
}).attach({ workflow });
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Poll filtering uses a **function predicate** (not a Zod schema like webhooks) — either `.filter((result) => …)` chained on the source, or a `filter:` option. Returning falsy skips the tick. Group polls that should run together with `definePollSource({ id: "...", … })`.
|
|
92
|
-
|
|
93
|
-
### Ephemeral poll (agents)
|
|
94
|
-
|
|
95
|
-
Agents can register a scheduled codemode script with `set_trigger`:
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
set_trigger({
|
|
99
|
-
kind: "poll",
|
|
100
|
-
slug: "inbox",
|
|
101
|
-
schedule: "*/5 * * * *",
|
|
102
|
-
code: [
|
|
103
|
-
'const emails = await tools["list-emails"]({ query: "is:unread" });',
|
|
104
|
-
"if (emails.items.length === 0) return;",
|
|
105
|
-
"console.log(JSON.stringify({ count: emails.items.length, items: emails.items }));",
|
|
106
|
-
].join("\n"),
|
|
107
|
-
prompt: "You have {{payload.count}} unread emails.",
|
|
108
|
-
lifecycle: { maxExecutions: 10 }, // optional: cap runs (also `until`)
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
- Write the script the same way you would for codemode (`bash` + `js-exec`).
|
|
113
|
-
- `console.log(JSON.stringify(result))` when there is work to do.
|
|
114
|
-
- Log nothing (or `null`) to skip — skipped ticks do not count toward `lifecycle.maxExecutions`.
|
|
115
|
-
- Prompt interpolation matches webhooks (`{{payload.path}}`).
|
|
116
|
-
|
|
117
|
-
## Attachment patterns
|
|
118
|
-
|
|
119
|
-
- **Attach to an agent** instead of a workflow: `.attach({ agent, prompt })` — the source's payload drives the agent prompt (interpolated like webhooks).
|
|
120
|
-
- **Fan-out to multiple targets**: chain `.attach(...).attach(...)` (or export an array of attachments) to bind one source to several workflows/agents.
|
|
121
|
-
- **Shared source definitions** can live under `src/triggers/sources/` — that subfolder is excluded from attachment discovery, so it's a safe place for source defs you import elsewhere.
|
|
122
|
-
- Sources also accept optional `name` / `description` metadata.
|
|
123
|
-
|
|
124
|
-
## Develop & audit
|
|
125
|
-
|
|
126
|
-
While building, invoke the workflow directly:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
keystroke workflow run signup-pipeline --input '{"name":"Ada","email":"a@acme.com","company":"Acme"}'
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Inspect trigger-driven runs:
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
keystroke trigger runs list signup:signup-pipeline # --limit / --cursor / --trigger-type
|
|
136
|
-
keystroke trigger runs get signup:signup-pipeline <run-id> --include workflows,trace
|
|
137
|
-
keystroke trigger poll <poll-attachment-id> # on-demand poll (--group to run a poll group)
|
|
138
|
-
keystroke trigger attachment disable <trigger-slug> <attachment-id> # pause; `enable` to resume
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Check `src/triggers/` in your project for existing patterns before adding new ones.
|
|
142
|
-
|
|
143
|
-
Related: [workflows](.agents/skills/keystroke-workflows/SKILL.md), [channels](.agents/skills/keystroke-channels/SKILL.md).
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-workflows
|
|
3
|
-
description: Build keystroke workflows with defineWorkflow — chain actions, call agents, typed Zod IO. Use when authoring src/workflows/ or auditing workflow runs.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: workflows
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Workflows
|
|
9
|
-
|
|
10
|
-
Workflows are **deterministic orchestration**: Zod input/output, a `run` function, and **actions as steps**. Triggers and the CLI invoke them; agents can participate inside actions.
|
|
11
|
-
|
|
12
|
-
## Example: action chain
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { defineWorkflow } from "@keystrokehq/keystroke/workflow";
|
|
16
|
-
import { slackSendMessage } from "@keystrokehq/slack/actions";
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
import { researchSignup } from "../actions/research-signup";
|
|
19
|
-
import { signupBriefMessage } from "../lib/signup";
|
|
20
|
-
|
|
21
|
-
export default defineWorkflow({
|
|
22
|
-
slug: "signup-pipeline",
|
|
23
|
-
input: z.object({ name: z.string(), email: z.string().email(), company: z.string() }),
|
|
24
|
-
output: z.object({ brief: z.string(), channel: z.string(), ts: z.string() }),
|
|
25
|
-
async run(input) {
|
|
26
|
-
const { brief } = await researchSignup.run(input);
|
|
27
|
-
const sent = await slackSendMessage.run({
|
|
28
|
-
channel: "#pipeline",
|
|
29
|
-
markdown_text: signupBriefMessage({ ...input, brief }),
|
|
30
|
-
});
|
|
31
|
-
return { brief, ...sent }; // sent provides { channel, ts }; merge in brief for the output schema
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`research-signup` calls an agent; `slackSendMessage` is an integration action used **directly as a step** here — never wrapped in a custom action. Note the `run` return is spread to satisfy the `output` schema (the Slack action only returns `channel`/`ts`). Keep orchestration in the workflow; an action is a single leaf step and never calls another action.
|
|
37
|
-
|
|
38
|
-
## Run & audit
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
keystroke workflow run signup-pipeline --input '{"name":"Ada","email":"ada@acme.com","company":"Acme"}'
|
|
42
|
-
keystroke workflow runs list signup-pipeline
|
|
43
|
-
keystroke workflow runs get signup-pipeline <run-id> --include steps,trace
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## How workflows get invoked
|
|
47
|
-
|
|
48
|
-
| From | How |
|
|
49
|
-
| ---------- | --------------------------------------------------------------------- |
|
|
50
|
-
| CLI | `keystroke workflow run {slug} --input '{...}'` |
|
|
51
|
-
| HTTP | `POST /workflows/{slug}` |
|
|
52
|
-
| Trigger | cron / webhook / poll attachment in `src/triggers/` |
|
|
53
|
-
| Agent tool | import the workflow into an agent's `tools: [workflow]` |
|
|
54
|
-
| Sub-workflow step | import the workflow and `await otherWorkflow.run(input)` inside another workflow's `run` body |
|
|
55
|
-
|
|
56
|
-
To reuse a workflow inside another workflow, import it and call `await otherWorkflow.run(input)` — a first-class durable step, just like an action or agent (validates the sub-workflow's IO, checkpoints its output, gets its own trace span). The sub-workflow runs inline in the same run, so its inner steps and `ctx.sleep`/`ctx.hook` stay durable; it does **not** spawn a separate tracked run. Never thread `ctx` into it (`otherWorkflow.run(input, ctx)` is not a thing). To run a workflow as its *own* tracked run, import it into an agent's `tools` or invoke it over HTTP.
|
|
57
|
-
|
|
58
|
-
The workflow `slug` is its identifier and route key. Slugs are unique per primitive kind, so a workflow can share a slug with an agent or trigger, but two workflows cannot share one.
|
|
59
|
-
|
|
60
|
-
## Durability (the model you must design for)
|
|
61
|
-
|
|
62
|
-
Workflows are **durable**: each `await` of an action, agent `.prompt()`, or `promptLlm` is recorded as a `step_completed` event. If a later step fails, the run **replays** the log — completed steps return their recorded result instead of running again — and resumes at the first unfinished step. Two rules follow:
|
|
63
|
-
|
|
64
|
-
- **Side effects go inside steps.** Code in the `run` body that isn't a step (network calls, writes, `Date.now()`, random) re-executes on every replay. Wrap it in an action/agent/`promptLlm` call so it's recorded once.
|
|
65
|
-
- **Keep control flow deterministic, steps idempotent.** Branch on input and recorded step results, not on values that change between attempts. A step can be retried after a transient failure, so design actions to tolerate being called twice with the same input. Retries are automatic — you don't write retry loops; the runtime emits `step_retrying` / `step_failed`.
|
|
66
|
-
|
|
67
|
-
Step ids are correlation ids: `step:<slug>#<occurrence>` (`#0`, `#1`, …), or `step:<id>` when pinned with `.stepId()`. If an action's position can shift, pin a stable `.stepId()` so replays line up. Durable waits (`ctx.sleep`, `ctx.hook`) suspend the run without holding a process open. See [authoring.md](references/authoring.md).
|
|
68
|
-
|
|
69
|
-
## Testing
|
|
70
|
-
|
|
71
|
-
Unit-test through `executeWorkflow` from `@keystrokehq/keystroke/workflow` — never call `workflow.run(...)` directly at the top level of a test (outside a run it has no durable context). Stub costly actions by seeding `step_completed` events in a `MemoryEventLog`. See [testing.md](references/testing.md).
|
|
72
|
-
|
|
73
|
-
## Next references
|
|
74
|
-
|
|
75
|
-
- [authoring.md](references/authoring.md) — agents in actions, app connections, composition
|
|
76
|
-
- [testing.md](references/testing.md) — unit tests, stubbing actions, run/trace debugging
|
|
77
|
-
|
|
78
|
-
Related: [actions](.agents/skills/keystroke-actions/SKILL.md), [triggers](.agents/skills/keystroke-triggers/SKILL.md), [agents](.agents/skills/keystroke-agents/SKILL.md).
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Workflow authoring
|
|
2
|
-
|
|
3
|
-
## Keep steps in actions (when they are not agent prompts)
|
|
4
|
-
|
|
5
|
-
Workflow `run` orchestrates — call actions for deterministic logic and integrations, call agents directly for LLM steps:
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
async run(input) {
|
|
9
|
-
const { brief } = await researchSignup.run(input);
|
|
10
|
-
const summary = await support.prompt({
|
|
11
|
-
message: `Summarize signup research:\n${brief}`,
|
|
12
|
-
});
|
|
13
|
-
const sent = await slackSendMessage.run({
|
|
14
|
-
channel: "#pipeline",
|
|
15
|
-
markdown_text: signupBriefMessage({ ...input, brief }),
|
|
16
|
-
});
|
|
17
|
-
return { brief, ...sent };
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Actions are leaf units: an action never calls another action — compose them in the workflow. Need formatting or shared logic between steps? Put it in `src/lib/`, not in a wrapper action.
|
|
22
|
-
|
|
23
|
-
## Agents as workflow steps
|
|
24
|
-
|
|
25
|
-
Import the agent and call `.prompt()` directly in the workflow `run`. Each call is a durable step keyed as `step:<agent-slug>#<occurrence>` (same scheme as actions). No wrapper action required when the step is just "prompt this agent."
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
const result = await support.prompt({
|
|
29
|
-
message: `From: ${input.sender}\nSubject: ${input.latestSubject}`,
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Use a wrapper action only when the agent call should also be an agent tool, or when the step bundles non-prompt logic.
|
|
34
|
-
|
|
35
|
-
For an explicit step id, actions chain `.stepId("x")`; agent prompts take it as an option: `agent.prompt(input, { stepId: "x" })`.
|
|
36
|
-
|
|
37
|
-
## LLM steps (`promptLlm`)
|
|
38
|
-
|
|
39
|
-
For a one-shot LLM call without a full agent, use `promptLlm` (from `@keystrokehq/keystroke/workflow`). It's a first-class durable step keyed `step:promptLlm#<occurrence>`. The signature is `promptLlm(prompt, opts)` — the prompt string comes **first**, options second:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { promptLlm } from "@keystrokehq/keystroke/workflow";
|
|
43
|
-
|
|
44
|
-
// Returns a string when no outputSchema is given:
|
|
45
|
-
const summary = await promptLlm(`Summarize:\n${brief}`, {
|
|
46
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Pass `outputSchema` (Zod) to get a parsed, schema-validated object back instead of a string — the return type is inferred from the schema:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { z } from "zod";
|
|
54
|
-
|
|
55
|
-
const Category = z.object({ category: z.enum(["bug", "feature", "question"]) });
|
|
56
|
-
|
|
57
|
-
const { category } = await promptLlm(`Classify this ticket:\n${ticket}`, {
|
|
58
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
59
|
-
outputSchema: Category,
|
|
60
|
-
});
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Other options: `system`, `thinkingLevel`, `temperature`, `maxTokens`, `stepId`.
|
|
64
|
-
|
|
65
|
-
## Sub-workflows as steps
|
|
66
|
-
|
|
67
|
-
Reuse a whole workflow by importing it and awaiting `.run(input)` — a first-class durable step, same as an action. Keystroke validates the sub-workflow's `input`/`output`, records its output as a checkpoint keyed `step:<sub-slug>#<occurrence>`, and gives it a trace span.
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import enrichLead from "./enrich-lead";
|
|
71
|
-
|
|
72
|
-
async run(input) {
|
|
73
|
-
const enriched = await enrichLead.run({ email: input.email });
|
|
74
|
-
return { score: enriched.score };
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The sub-workflow runs **inline** in the same run: its inner action/agent/`promptLlm` steps and durable waits (`ctx.sleep` / `ctx.hook`) all stay durable, and it does **not** spawn a separate tracked run. Never thread `ctx` — `enrichLead.run(input, ctx)` is not the pattern; the engine supplies the context. Pin an explicit id with `.stepId("x")` when the call's position can shift. For an *independently tracked* run instead, import the workflow into an agent's `tools` or call it over HTTP.
|
|
79
|
-
|
|
80
|
-
## Durability & retries
|
|
81
|
-
|
|
82
|
-
As each step completes, its result is written to a run event log. If a later step fails and the run is retried, Keystroke replays the log: completed steps return their recorded result instead of running again, and execution resumes at the first unfinished step. Design for this:
|
|
83
|
-
|
|
84
|
-
- **Put side effects inside steps.** Work in the `run` body that isn't an action / agent / `promptLlm` call (a raw `fetch`, a DB write, `Date.now()`, `Math.random()`) runs again on every replay. Move it into a step so it's recorded once.
|
|
85
|
-
- **Steps should be idempotent.** A step can be retried after a transient failure, so an action should tolerate being called twice with the same input (use idempotency keys for external writes where it matters).
|
|
86
|
-
- **Keep control flow deterministic.** Branch on the run's input and recorded step results — not on wall-clock time or randomness — so replays take the same path. Each step is recorded under `step:<slug>#<occurrence>`; pin `.stepId("x")` when an action's order can shift so replays still line up. Ids must be unique within a run.
|
|
87
|
-
|
|
88
|
-
You don't manage retries yourself. The runtime records `step_completed`, `step_retrying`, and `step_failed` events and surfaces them in `keystroke workflow runs get <slug> <run-id> --include steps,trace`.
|
|
89
|
-
|
|
90
|
-
## Parallel steps
|
|
91
|
-
|
|
92
|
-
Independent steps run concurrently with `Promise.all` — each `.run()` / `.prompt()` / `promptLlm` inside it is still its own durable step, recorded and replayed individually:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
const [enriched, scored] = await Promise.all([
|
|
96
|
-
enrichLead.run({ leadId }),
|
|
97
|
-
scoreLead.run({ leadId }),
|
|
98
|
-
]);
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Correlation ids are assigned per slug in array order (`step:enrich-lead#0`, `step:score-lead#0`). If the branches can change order between attempts (conditionally included, dynamic arrays), pin a stable `.stepId()` on each so replays line up:
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
const results = await Promise.all(
|
|
105
|
-
leads.map((lead) => enrichLead.run({ leadId: lead.id }).stepId(`enrich:${lead.id}`)),
|
|
106
|
-
);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
There's no built-in concurrency limit or fan-out helper — `Promise.all` runs everything at once. For bounded concurrency, batch the array yourself (e.g. chunk and `await` each chunk).
|
|
110
|
-
|
|
111
|
-
## Handling failures
|
|
112
|
-
|
|
113
|
-
Retries are **queue-level**: a failed run is re-enqueued (default 3 attempts, exponential backoff) and replays from the event log, so completed steps don't re-run. Each failed attempt emits `step_retrying`; the final failure emits `step_failed`. You don't write retry loops — make steps idempotent so a retried step is safe.
|
|
114
|
-
|
|
115
|
-
There's no built-in saga, compensation engine, or dead-letter queue. When a later step fails and you need to undo earlier work, orchestrate it yourself with `try/catch` in `run` — catching the error lets the run complete normally (so attach a `compensated` flag to the output rather than rethrowing if you've handled it):
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
async run(input) {
|
|
119
|
-
await reserveBooking.run({ id: input.bookingId });
|
|
120
|
-
try {
|
|
121
|
-
await chargeCustomer.run({ id: input.bookingId });
|
|
122
|
-
return { ok: true };
|
|
123
|
-
} catch (error) {
|
|
124
|
-
await releaseBooking.run({ id: input.bookingId }); // best-effort cleanup
|
|
125
|
-
return { ok: false, compensated: true };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
If you let the error propagate instead, the run fails and the queue retries it — fine for transient failures, but it replays from the log, so any cleanup must itself be a recorded, idempotent step.
|
|
131
|
-
|
|
132
|
-
## Durable run context (`ctx`)
|
|
133
|
-
|
|
134
|
-
`run(input, ctx)` receives a second argument exposing `runId`, `trigger` (`api` | `cron` | `webhook` | `poll` | `retry`), and the durable wait primitives `ctx.sleep(...)` and `ctx.hook(...)`. Both **suspend** the run (the replay engine resumes it later) so long waits and external callbacks don't hold a process open.
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
// Durable delay: number (ms), duration string, or a Date
|
|
138
|
-
await ctx.sleep("1h");
|
|
139
|
-
|
|
140
|
-
// Durable hook: suspend until an external system POSTs to the resume URL
|
|
141
|
-
async run(input, ctx) {
|
|
142
|
-
const approval = ctx.hook<{ approved: boolean }>();
|
|
143
|
-
await slackSendMessage.run({
|
|
144
|
-
channel: "#approvals",
|
|
145
|
-
markdown_text: `Approve deploy? ${approval.resumeUrl}`,
|
|
146
|
-
});
|
|
147
|
-
const { approved } = await approval; // suspends here until resumed
|
|
148
|
-
return { approved };
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`ctx.hook(options?)` takes an optional `token` (reuse a stable resume token) and `schema` (Zod validation of the resume payload). The returned handle exposes `token` and `resumeUrl` before you await it — surface that URL so the approver/callback can resume the run.
|
|
153
|
-
|
|
154
|
-
## Agents inside actions
|
|
155
|
-
|
|
156
|
-
When an action must call an agent (e.g. the action is reused as an agent tool), call `.prompt()` inside the action's `run`. That prompt is not a separate workflow step — the action is.
|
|
157
|
-
|
|
158
|
-
## Connecting apps
|
|
159
|
-
|
|
160
|
-
Sync the app into `src/apps/`, author with `app.action()`, then `keystroke connect <slug>` before testing. Catalog integration actions from npm packages work once the app is connected.
|
|
161
|
-
|
|
162
|
-
## Slugs
|
|
163
|
-
|
|
164
|
-
Slugs are scoped per primitive kind: a `slug` must be unique among workflows, among agents, and among triggers, but the same `slug` may be reused across kinds (a `pricing` agent and a `pricing` workflow can coexist). The workflow's `slug` is also its HTTP route segment (`POST /workflows/{slug}`).
|
|
165
|
-
|
|
166
|
-
## Subscription mode
|
|
167
|
-
|
|
168
|
-
`defineWorkflow` accepts an optional `subscription: { mode: "system" | "subscribable" }` to control how the workflow can be subscribed to. Omit it for the default behavior.
|