Skip to content

Commit 72dce87

Browse files
mrprkrclaude
andcommitted
docs: add documentation for hooks, actions, and workflows
- hooks.mdoc: lifecycle events, registration, context, cancellation, data modification, execution order, global hooks - actions.mdoc: registration, context, results, conditional visibility, UI placement, write-back - workflows.mdoc: WDK integration, writing workflows with directives, API endpoint setup, connecting to actions and hooks, useWorkflow and awaitWorkflow adapters, toast notifications, deployment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 01b38c1 commit 72dce87

File tree

4 files changed

+551
-0
lines changed

4 files changed

+551
-0
lines changed

docs/src/content/navigation.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ navGroups:
8080
discriminant: page
8181
value: content-components
8282
status: default
83+
- label: Hooks
84+
link:
85+
discriminant: page
86+
value: hooks
87+
status: new
88+
- label: Actions
89+
link:
90+
discriminant: page
91+
value: actions
92+
status: new
93+
- label: Workflows
94+
link:
95+
discriminant: page
96+
value: workflows
97+
status: new
8398
- groupName: Recipes
8499
items:
85100
- label: Use Astro's Image component
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
title: Actions
3+
summary: >-
4+
Add custom action buttons to the Keystatic editor toolbar that content editors
5+
can trigger manually.
6+
---
7+
8+
Actions are developer-defined operations that appear as a dropdown menu in the item editor toolbar. They let content editors trigger workflows, run validations, or perform tasks without leaving the Keystatic dashboard.
9+
10+
## Registering actions
11+
12+
Use `registerActions` to attach actions to a collection or singleton:
13+
14+
```typescript
15+
import { registerActions } from '@keystatic/core';
16+
17+
registerActions({ collection: 'posts' }, [
18+
{
19+
label: 'Run Content Audit',
20+
description: 'Check SEO, readability, and publishing readiness',
21+
handler: async (ctx) => {
22+
// Perform the action
23+
const issues = await auditContent(ctx.data);
24+
return { message: `Audit complete: ${issues.length} issues found` };
25+
},
26+
},
27+
]);
28+
```
29+
30+
## Action definition
31+
32+
Each action has the following shape:
33+
34+
```typescript
35+
type Action = {
36+
label: string; // Button/menu label
37+
description?: string; // Shown as secondary text in the dropdown
38+
icon?: ReactElement; // Optional icon (from @keystar/ui/icon)
39+
handler: (ctx) => Promise<ActionResult | void>;
40+
when?: { // Conditional visibility
41+
match?: (ctx: { slug?: string; data: Record<string, unknown> }) => boolean;
42+
};
43+
};
44+
```
45+
46+
## Action context
47+
48+
The handler receives a context object:
49+
50+
```typescript
51+
type ActionContext = {
52+
trigger: 'manual';
53+
collection?: string;
54+
singleton?: string;
55+
slug?: string;
56+
data: Record<string, unknown>; // current field values
57+
storage: { kind: 'local' | 'github' | 'cloud' };
58+
update(data: Partial<Record<string, unknown>>): Promise<void>;
59+
};
60+
```
61+
62+
The `update` function lets your action write back to the current entry:
63+
64+
```typescript
65+
handler: async (ctx) => {
66+
const summary = await generateSummary(ctx.data.content);
67+
await ctx.update({ summary });
68+
return { message: 'Summary generated and saved' };
69+
},
70+
```
71+
72+
## Action results
73+
74+
The handler can return a result that controls the toast notification shown to the editor:
75+
76+
```typescript
77+
// Success toast
78+
return { message: 'Action completed successfully' };
79+
80+
// Error toast
81+
return { error: 'Something went wrong' };
82+
83+
// No toast (silent)
84+
return;
85+
```
86+
87+
## Conditional actions
88+
89+
Use the `when.match` function to show actions only when certain conditions are met:
90+
91+
```typescript
92+
registerActions({ collection: 'posts' }, [
93+
{
94+
label: 'Translate to Spanish',
95+
when: {
96+
match: (ctx) => (ctx.data.language as string) === 'en',
97+
},
98+
handler: async (ctx) => {
99+
// Only shown for English posts
100+
},
101+
},
102+
]);
103+
```
104+
105+
## UI placement
106+
107+
Actions appear as a **dropdown menu** triggered by a zap icon button in the editor toolbar, next to the existing action icons (reset, delete, copy, paste) and the Save button.
108+
109+
- The button only appears when at least one action is registered for the current collection or singleton
110+
- All registered (and visible) actions appear in the dropdown
111+
- While an action is running, the button is disabled to prevent double-execution
112+
113+
## Multiple actions
114+
115+
Register multiple actions in a single call:
116+
117+
```typescript
118+
registerActions({ collection: 'posts' }, [
119+
{
120+
label: 'Preview on Site',
121+
handler: async (ctx) => {
122+
window.open(`/posts/${ctx.slug}`, '_blank');
123+
return { message: 'Preview opened' };
124+
},
125+
},
126+
{
127+
label: 'Export as Markdown',
128+
handler: async (ctx) => {
129+
// Export logic
130+
return { message: 'Exported' };
131+
},
132+
},
133+
{
134+
label: 'Run SEO Check',
135+
description: 'Validate title length, slug format, and meta fields',
136+
handler: async (ctx) => {
137+
// SEO validation logic
138+
return { message: 'SEO check passed' };
139+
},
140+
},
141+
]);
142+
```

docs/src/content/pages/hooks.mdoc

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
title: Hooks
3+
summary: >-
4+
React to content lifecycle events with before and after hooks on collections
5+
and singletons.
6+
---
7+
8+
Hooks let you run custom logic when content is created, saved, or deleted. They are registered at runtime using the `registerHooks` function from `@keystatic/core`.
9+
10+
## Hook events
11+
12+
Six lifecycle events are available:
13+
14+
| Event | When it fires | Can cancel? |
15+
|---|---|---|
16+
| `beforeCreate` | Before a new entry is created | Yes |
17+
| `afterCreate` | After a new entry is created | No |
18+
| `beforeSave` | Before an existing entry is saved | Yes |
19+
| `afterSave` | After an existing entry is saved | No |
20+
| `beforeDelete` | Before an entry is deleted | Yes |
21+
| `afterDelete` | After an entry is deleted | No |
22+
23+
## Registering hooks
24+
25+
Use `registerHooks` to attach hooks to a collection or singleton:
26+
27+
```typescript
28+
import { registerHooks } from '@keystatic/core';
29+
30+
registerHooks({ collection: 'posts' }, {
31+
beforeSave: [
32+
async (ctx) => {
33+
console.log(`Saving post: ${ctx.slug}`);
34+
},
35+
],
36+
afterSave: [
37+
async (ctx) => {
38+
console.log(`Post saved: ${ctx.slug}`);
39+
},
40+
],
41+
});
42+
```
43+
44+
For singletons:
45+
46+
```typescript
47+
registerHooks({ singleton: 'settings' }, {
48+
afterSave: [
49+
async (ctx) => {
50+
console.log('Settings updated');
51+
},
52+
],
53+
});
54+
```
55+
56+
## Hook context
57+
58+
Every hook receives a context object with information about the operation:
59+
60+
```typescript
61+
type HookContext = {
62+
event: HookEvent; // which event triggered this hook
63+
trigger: 'event' | 'manual';
64+
collection?: string; // collection name (if applicable)
65+
singleton?: string; // singleton name (if applicable)
66+
slug?: string; // entry slug (collections only)
67+
data: Record<string, unknown>; // current field values
68+
previousData?: Record<string, unknown>; // previous values (for updates)
69+
storage: { kind: 'local' | 'github' | 'cloud' };
70+
};
71+
```
72+
73+
After hooks also receive an `update` function for writing back to the entry:
74+
75+
```typescript
76+
afterSave: [
77+
async (ctx) => {
78+
// Update a field on the entry after save
79+
await ctx.update({ lastModified: new Date().toISOString() });
80+
},
81+
],
82+
```
83+
84+
## Cancelling operations
85+
86+
`before*` hooks can cancel the operation by returning `{ cancel: true }`:
87+
88+
```typescript
89+
beforeSave: [
90+
async (ctx) => {
91+
const title = ctx.data.title as string;
92+
if (title.length < 3) {
93+
return { cancel: true, reason: 'Title must be at least 3 characters' };
94+
}
95+
},
96+
],
97+
```
98+
99+
When a hook cancels, subsequent hooks do not run and a toast notification displays the reason to the editor.
100+
101+
## Modifying data
102+
103+
`before*` hooks can also modify the data before it is saved:
104+
105+
```typescript
106+
beforeSave: [
107+
async (ctx) => {
108+
return {
109+
data: {
110+
...ctx.data,
111+
updatedAt: new Date().toISOString(),
112+
},
113+
};
114+
},
115+
],
116+
```
117+
118+
Modified data is passed to subsequent hooks and to the save operation.
119+
120+
## Execution order
121+
122+
1. `before*` hooks run **sequentially** — first cancellation wins
123+
2. The actual operation (create/save/delete) runs
124+
3. `after*` hooks run **in parallel** — errors are logged but don't block
125+
126+
When both global and resource-level hooks exist, global hooks run first.
127+
128+
## Global hooks
129+
130+
Register hooks that run for all collections and singletons using `registerGlobalHooks`:
131+
132+
```typescript
133+
import { registerGlobalHooks } from '@keystatic/core';
134+
135+
registerGlobalHooks({
136+
afterSave: [
137+
async (ctx) => {
138+
console.log(`Content saved: ${ctx.collection || ctx.singleton}`);
139+
},
140+
],
141+
});
142+
```
143+
144+
Global hooks execute before resource-level hooks.

0 commit comments

Comments
 (0)