Lifecycle Hooks
Lifecycle hooks trigger operations automatically when content changes. When a record is created, updated, deleted, published, or unpublished, Foir can execute one or more operations in response.
Overview
Hooks connect content lifecycle events to operations. When an event fires on a model, Foir executes the configured operations with the full record context.
Page published
-> Execute "notify-slack" operation (async)
-> Execute "sync-cdn" operation (async)
-> Execute "update-search-index" operation (sync)Lifecycle Events
Record events — fired when content changes:
| Event | When It Fires |
|---|---|
RECORD_CREATED | A new record is created |
RECORD_UPDATED | A record is updated |
RECORD_DELETED | A record is deleted |
RECORD_PUBLISHED | A version is published (goes live) |
RECORD_UNPUBLISHED | A published version is unpublished |
Operation events — fired when an operation execution reaches a terminal state. Useful for chaining follow-up work (notify Slack when a long-running export finishes, retry a related job, reconcile external state, etc.):
| Event | When It Fires |
|---|---|
OPERATION_COMPLETED | Operation finished successfully; payload carries executionId, operationKey, status, and the typed result. |
OPERATION_FAILED | Operation finished with an error; payload includes the error {code, message, retryable}. |
OPERATION_CANCELLED | Operation was cancelled (by admin or by the extension itself) before completing. |
OPERATION_TIMED_OUT | An async-callback operation didn’t call back within callbackTtlSeconds and the watchdog reaped it. |
Hook Configuration
Each hook specifies:
| Field | Description |
|---|---|
operationKey | The operation to execute |
async | true to dispatch the hook’s operation without blocking the originating event, false to wait for the operation to complete before the event call returns. Orthogonal to the operation’s own execution mode — you can set async: true on a hook that triggers a sync operation (fire the op, don’t wait) or async: false on a hook that triggers an async-callback operation (wait for the callback before returning). |
condition | Optional rule expression to conditionally execute |
input | Optional static input data passed to the operation |
In the Admin
Configuring Hooks
- Go to Settings > Models
- Select a model (e.g., “Blog Post”)
- Click Lifecycle Hooks
- Add triggers for the desired events:
- Select the event (e.g.,
RECORD_PUBLISHED) - Choose the operation to execute
- Set async mode
- Optionally add a condition
- Select the event (e.g.,
- Save and publish the model
Creating an Operation for Hooks
Before adding a hook, you need an operation to target:
- Go to Automation > Operations
- Click Create Operation
- Configure the key, endpoint URL, and touch points (select “Record” for lifecycle hooks)
- Save
See Operations for the full guide.
Execution Modes
| Mode | Behavior | Use Case |
|---|---|---|
Async (async: true) | Enqueued to a background job queue with automatic retries | Notifications, external syncs, non-critical tasks |
Sync (async: false) | Executed inline, result awaited before the operation completes | Validation, critical transformations |
Async operations retry automatically if the target endpoint is temporarily unavailable.
Via the CLI
# List all hooks
foir hooks list
# Get a hook by key or ID
foir hooks get <keyOrId>
# Create a hook
foir hooks create --data '{
"key": "notify-on-publish",
"name": "Notify on Publish",
"event": "RECORD_PUBLISHED",
"modelKey": "blog-post",
"operationKey": "slack-notify-publish",
"async": true
}'
# Update a hook
foir hooks update <id> --data '{"async": false}'
# Delete a hook
foir hooks delete <id>
# List recent deliveries for a hook (filter by status)
foir hooks deliveries <hookId> --status failed --limit 20
# Retry a failed delivery
foir hooks retry-delivery <deliveryId>
# Send a synthetic delivery to verify the receiver
foir hooks test <hookId> --data '{"recordId": "rec_abc123"}'See foir hooks for the full subcommand and option reference.
Via the API
Operations triggered by hooks receive a standardized payload containing:
| Field | Description |
|---|---|
operationKey | Which operation is being called |
trigger.type | lifecycle |
record.id | The record ID |
record.modelKey | The model key |
record.content | The record’s current content |
context.projectId | Project ID |
context.userId | User who triggered the event |
context.timestamp | When the event occurred |
See Operations for the full HTTP contract and response format.
Config System
Hooks can also be defined in foir.config.ts using defineHook. See the Configuration reference for details.
Common Patterns
Slack Notification on Publish
Create an operation that POSTs to a Slack webhook, then add it as a hook on the RECORD_PUBLISHED event:
{
"event": "RECORD_PUBLISHED",
"operationKey": "slack-notify-publish",
"async": true
}External System Sync
Keep an external system in sync by triggering on create, update, and delete. Create separate hooks for each event, all pointing to the same sync operation:
[
{ "event": "RECORD_CREATED", "operationKey": "sync-to-external", "async": true },
{ "event": "RECORD_UPDATED", "operationKey": "sync-to-external", "async": true },
{ "event": "RECORD_DELETED", "operationKey": "delete-from-external", "async": true }
]Conditional Webhook
Only notify when content meets specific criteria. Use a condition to filter which records trigger the hook:
{
"event": "RECORD_PUBLISHED",
"operationKey": "notify-uk-team",
"async": true,
"condition": {
"type": "condition",
"left": { "type": "field", "path": "market" },
"operator": "equals",
"right": { "type": "literal", "value": "uk" }
}
}Best Practices
- Use async for external calls — do not slow down content operations with network latency.
- Add retry policies on operations — configure retries for resilience against transient failures.
- Use conditions sparingly — simple hooks are easier to debug.
- Monitor deliveries — check the hook deliveries list for failures.
- Test with draft content — use
foir hooks testto verify hooks work before going live. - Document hook purpose — use operation descriptions to explain why each hook exists.