Skip to Content
Config SystemDefining Operations

Defining Operations

Operations register HTTP endpoints for custom logic. When triggered — by a user action, a lifecycle hook, a schedule, or an API call — the platform sends a signed payload to your endpoint and records the result.

Operation Interface

interface ApplyConfigOperationInput { key: string; // Unique identifier (e.g., 'sync-data') name: string; // Display name description?: string; // What this operation does icon?: string; // Admin-UI icon key category?: string; // Grouping category endpoint?: string; // HTTP endpoint URL (relative, resolved on push) isActive?: boolean; // Whether the operation is enabled (default: true) timeoutMs?: number; // Per-operation timeout in milliseconds inputSchema?: Record<string, unknown>; // JSON Schema for input outputSchema?: Record<string, unknown>; // JSON Schema for the operation result allowedRoles?: string[]; // Role keys allowed to execute this operation retryPolicy?: Record<string, unknown>; // HTTP-dispatch retry policy precondition?: Precondition; // Gate before execution quotas?: { rules: QuotaRule[] }; // Usage quotas /** Default execution mode: 'sync' (default) or 'async'. */ mode?: 'sync' | 'async'; /** TTL the platform waits for an async callback before timing out (clamped 5m–7d, default 24h). */ callbackTtlSeconds?: number; /** Retry policy applied specifically when the async callback times out. */ callbackTimeoutRetryPolicy?: { maxRetries?: number }; }

The endpoint field specifies the relative URL path for the operation. When you run foir push, the platform resolves relative endpoints against your deployed service URL — see operationBaseUrl on the top-level config.

Basic Example

import { defineConfig, defineOperation } from '@eide/foir-cli/configs'; export default defineConfig({ key: 'my-app', name: 'My App', operations: [ defineOperation({ key: 'sync-to-cdn', name: 'Sync to CDN', description: 'Pushes published content to the CDN edge network', endpoint: '/sync/cdn', }), ], });

Endpoint URLs

Operation endpoints are relative paths that are resolved against your service’s base URL at push time:

operations: [ { key: 'generate-pdf', name: 'Generate PDF', endpoint: '/generate/pdf', }, { key: 'send-notification', name: 'Send Notification', endpoint: '/notify', }, ]

When the platform triggers an operation, it POSTs a signed payload to the resolved URL. See Operations for details on the request/response contract.

Categorizing Operations

Use the category field to group related operations:

operations: [ { key: 'generate-css', name: 'Generate Theme CSS', description: 'Generates Tailwind v4 CSS from design tokens', endpoint: '/generate-css', category: 'design-system', }, { key: 'generate-tokens', name: 'Generate Token File', description: 'Exports design tokens as JSON', endpoint: '/generate-tokens', category: 'design-system', }, ]

Execution Modes

Operations declare a default execution mode. Callers can override at dispatch time when the operation supports the requested mode.

ModeBehaviourUse For
syncFoir holds the connection until the endpoint responds. Result returned to the caller inline.Sub-minute deterministic transforms, validation, lookups.
asyncFoir dispatches and gets 202 Accepted back. Endpoint queues the work, then calls back to the public API to report progress + terminal state.Long-running work, progress reporting, anything that depends on a slow upstream you don’t want blocking the dispatch.
defineOperation({ key: 'full-export', name: 'Full Data Export', endpoint: '/exports/full', mode: 'async', callbackTtlSeconds: 3600, // 1 hour to call back callbackTimeoutRetryPolicy: { maxRetries: 2 }, })

The callbackTtlSeconds field controls how long the platform waits for a callback before marking the execution TIMED_OUT. If unset, the platform falls back to the project’s default callback TTL, then to 24 hours. Clamped to [5m, 7d].

callbackTimeoutRetryPolicy is distinct from retryPolicy: retryPolicy governs HTTP dispatch failures (network errors, 5xx); callbackTimeoutRetryPolicy governs callback timeouts. Splitting them lets you retry dispatch failures aggressively while being conservative about callback timeouts — re-running an hour-long job that timed out tends to burn another hour.

See Operations for the full async-callback handshake.

Active/Inactive Toggle

Disable an operation without removing it by setting isActive to false:

{ key: 'legacy-sync', name: 'Legacy Sync', description: 'Deprecated sync endpoint', endpoint: '/sync/legacy', isActive: false, }

Inactive operations cannot be triggered by hooks, schedules, or API calls. Set isActive back to true (or remove the field) to re-enable.

Real-World Example

From the Cloudflare KV Redirect extension — three operations for syncing, checking status, and tearing down:

operations: [ { key: 'redirect-sync', name: 'Sync Redirects to Edge', description: 'Syncs redirect records to Cloudflare KV', endpoint: '/sync/all', }, { key: 'redirect-sync-status', name: 'Get Sync Status', description: 'Returns current sync metadata for display in the editor', endpoint: '/sync/status', }, { key: 'redirect-undeploy', name: 'Undeploy Edge Redirects', description: 'Tear down Cloudflare Worker and KV namespace', endpoint: '/deploy/destroy', }, ]

Preconditions

A precondition gates whether an operation can execute at all. It evaluates before execution and returns a FailedPrecondition error if the check fails. Use preconditions for state-based gating: “is this user allowed to do this?”

There are two precondition shapes:

Segment Precondition

Blocks execution unless the customer belongs to a named segment:

defineOperation({ key: 'ai-suggest', name: 'AI Suggest', endpoint: '/suggest', precondition: { segmentKey: 'pro_users', message: 'AI suggestions are available on the Pro plan. Upgrade to unlock.', }, })

If the customer is not a member of the pro_users segment, the platform returns the message as the error. If message is omitted, a generic “operation precondition not met” error is returned.

Expression Precondition

Blocks execution unless a rule expression evaluates to true against the operation input and context:

defineOperation({ key: 'publish-post', name: 'Publish Post', endpoint: '/publish', precondition: { type: 'condition', left: { type: 'field', path: 'status' }, operator: 'equals', right: { type: 'literal', value: 'draft' }, }, })

Expression preconditions support the same operators and grouping as segment rules (AND/OR groups, field/context references, etc.).

Quotas

A quota meters how many times an operation can be executed within a time window. It evaluates after preconditions pass and returns a ResourceExhausted error when the limit is reached. Use quotas for usage-based metering: “how many times can this user do this?”

defineOperation({ key: 'ai-suggest', name: 'AI Suggest', endpoint: '/suggest', quotas: { rules: [ { points: 50, duration: 86400, scope: 'customer' }, ], }, })

Quota Rule Fields

FieldDescription
pointsMax executions in the window. 0 means unlimited.
durationWindow length in seconds (86400 = 1 day).
scopeWho the counter tracks: "customer", "user", or "tenant".
segmentKeyTarget a specific segment. Omit for a fallback rule.
messageCustom error when quota is exhausted (e.g., an upgrade CTA).
conditionOptional rule expression to conditionally apply the rule.
expiresAtISO 8601 date after which the rule is ignored.

Rule Ordering

Rules are evaluated top-to-bottom, first match wins. A rule matches if:

  1. It has not expired
  2. The customer belongs to the rule’s segment (or the rule has no segmentKey)
  3. The rule’s condition evaluates to true (or the rule has no condition)

Once a rule matches, it is enforced and no further rules are checked. If no rule matches, execution is allowed.

Tiered Pattern: Pro Unlimited / Free Metered

The most common pattern is a segment-gated unlimited rule followed by a metered fallback. Pro customers match the first rule and skip metering entirely; everyone else falls through to the metered rule:

defineOperation({ key: 'ai-suggest', name: 'AI Suggest', endpoint: '/suggest', quotas: { rules: [ // Rule 0: Pro users — unlimited { segmentKey: 'pro', points: 0, duration: 86400, scope: 'customer' }, // Rule 1: Everyone else — 10/day with upgrade CTA { points: 10, duration: 86400, scope: 'customer', message: "You've used your 10 free AI suggestions today. Upgrade to Pro for unlimited.", }, ], }, })

Precondition vs. Quota

PreconditionQuota
PurposeState-based gatingUsage-based metering
Question”Is this allowed?""How many times?”
Error codeFailedPreconditionResourceExhausted
Typical usePaywalls, feature flags, role checksRate limits, free-tier caps
EvaluationBefore executionAfter precondition passes

You can use both on the same operation. For example, a precondition that requires a logged-in customer, plus a quota that meters usage for free-tier customers.

Connecting Operations to Other Resources

Operations are referenced by key from other config resources:

  • Hooks trigger operations on lifecycle events. See Defining Hooks.
  • Schedules trigger operations on a cron schedule. See Defining Schedules.
  • Operations can also be triggered manually from the admin dashboard or via the Operations API.

Next Steps

Last updated on