Scoped workflows let you chain multiple host-mediated process steps into a single structured request. Use them when a user action requires a multi-step flow — for example, inspect a deployment before restarting it, or generate a Terraform plan before applying it.
The FDO host executes each step in sequence, enforces per-step policies, applies confirmation gates before destructive steps, and returns typed per-step result data. This is safer and more auditable than orchestrating steps in plugin-private code.
Workflows reuse the same capability pair as single-step process execution: system.process.exec and system.process.scope.<scope-id>. You do not need an additional broad workflow capability.
When to use workflows
Use requestScopedWorkflow() instead of calling requestOperatorTool() multiple times when:
- the steps must run in a defined sequence (abort on failure is important)
- a step requires user confirmation before it executes (e.g., an apply or delete)
- you want the host to audit the full flow as a single correlated unit
- you need typed per-step result data (
exitCode, stdout, stderr, durationMs) from each step
Workflow kinds
The SDK currently supports one workflow kind:
| Kind | Description |
|---|
"process-sequence" | Executes a sequence of process steps in order |
Step phases
Each step can declare a phase that describes its semantic role in the flow:
| Phase | Purpose |
|---|
"inspect" | Read-only examination of current state |
"preview" | Generate a preview or plan without making changes |
"mutate" | Make a targeted change (scoped, not a full apply) |
"apply" | Apply a change set that was previewed |
"cleanup" | Post-apply cleanup or teardown |
Phases are informational — the host uses them for auditing and confirmation routing, not for execution logic.
ScopedWorkflowProcessStep
Each step in a workflow is a ScopedWorkflowProcessStep:
type ScopedWorkflowProcessStep = {
id: string; // unique step identifier
title: string; // human-readable step name
phase?: ScopedWorkflowStepPhase; // "inspect" | "preview" | "mutate" | "apply" | "cleanup"
command: string; // absolute path to the executable
args?: string[];
cwd?: string; // absolute working directory
env?: Record<string, string>;
timeoutMs?: number;
input?: string; // stdin content
encoding?: "utf8" | "base64";
reason?: string; // human-readable reason for auditing
onError?: "abort" | "continue"; // behavior when the step fails
};
onError behavior
"abort" — stop the workflow immediately if this step fails. Subsequent steps are skipped.
"continue" — log the failure and proceed to the next step.
Default behavior when onError is omitted is host-defined. Set "abort" explicitly for steps where failure makes subsequent steps meaningless (e.g., a plan step before an apply).
Confirmation gates
Use confirmation to require user approval before specific steps execute:
type ScopedWorkflowConfirmation = {
message: string;
requiredForStepIds?: string[]; // step IDs that require confirmation before running
};
When a step is listed in requiredForStepIds, the host pauses before executing that step and prompts the user with message. If the user declines, the workflow aborts.
Executing a workflow
requestScopedWorkflow(scopeId, payload, options?) builds and sends the workflow request:
import {
requestScopedWorkflow,
isPrivilegedActionSuccessResponse,
isPrivilegedActionErrorResponse,
} from "@anikitenko/fdo-sdk";
const response = await requestScopedWorkflow("terraform", {
kind: "process-sequence",
title: "Terraform preview and apply",
summary: "Preview infrastructure changes before apply",
dryRun: true,
steps: [
{
id: "plan",
title: "Generate plan",
phase: "preview",
command: "/usr/local/bin/terraform",
args: ["plan", "-input=false"],
timeoutMs: 10000,
reason: "preview infrastructure plan",
onError: "abort",
},
{
id: "apply",
title: "Apply plan",
phase: "apply",
command: "/usr/local/bin/terraform",
args: ["apply", "-input=false", "tfplan"],
timeoutMs: 10000,
reason: "apply approved infrastructure plan",
onError: "abort",
},
],
confirmation: {
message: "Apply infrastructure changes?",
requiredForStepIds: ["apply"],
},
});
if (isPrivilegedActionSuccessResponse(response)) {
const result = response.result; // ScopedWorkflowResult
}
Payload fields
| Field | Type | Required | Description |
|---|
kind | "process-sequence" | Yes | Workflow kind |
title | string | Yes | Human-readable workflow name |
summary | string | No | Additional description for auditing |
dryRun | boolean | No | Validate without executing any steps |
steps | ScopedWorkflowProcessStep[] | Yes | Ordered list of steps |
confirmation | ScopedWorkflowConfirmation | No | Confirmation gate configuration |
Building a request without sending it
createScopedWorkflowRequest(scopeId, payload) builds a ScopedWorkflowRunActionRequest without dispatching it. Use this to inspect or log the request, or to construct it at init time and send it later via requestPrivilegedAction():
import {
createScopedWorkflowRequest,
requestPrivilegedAction,
} from "@anikitenko/fdo-sdk";
const request = createScopedWorkflowRequest("kubectl", {
kind: "process-sequence",
title: "Inspect and restart deployment",
summary: "Inspect deployment state before running a scoped rollout restart",
dryRun: true,
steps: [
{
id: "inspect-deployment",
title: "Inspect deployment",
phase: "inspect",
command: "/usr/local/bin/kubectl",
args: ["get", "deployment", "api", "-n", "default", "-o", "json"],
timeoutMs: 5000,
reason: "inspect deployment state before restart",
onError: "abort",
},
{
id: "restart-deployment",
title: "Restart deployment",
phase: "mutate",
command: "/usr/local/bin/kubectl",
args: ["rollout", "restart", "deployment/api", "-n", "default"],
timeoutMs: 5000,
reason: "restart deployment after inspection",
onError: "abort",
},
],
confirmation: {
message: "Restart deployment api in namespace default?",
requiredForStepIds: ["restart-deployment"],
},
});
// Inspect the built request:
this.info("Built workflow request", { request });
// Send it later:
const response = await requestPrivilegedAction(request, {
correlationIdPrefix: "kubectl-workflow",
});
You can also use createWorkflowRunActionRequest(request) to build a ScopedWorkflowRunActionRequest with full control over the action and scope:
import { createWorkflowRunActionRequest } from "@anikitenko/fdo-sdk";
const request = createWorkflowRunActionRequest({
action: "system.workflow.run",
payload: {
scope: "terraform",
kind: "process-sequence",
title: "Terraform preview and apply",
steps: [ /* ... */ ],
},
});
Workflow result types
A successful workflow response contains a ScopedWorkflowResult in response.result:
type ScopedWorkflowResult = {
workflowId: string;
scope: string;
kind: ScopedWorkflowKind;
status: "completed" | "partial" | "failed";
summary: ScopedWorkflowSummary;
steps: ScopedWorkflowStepResult[];
};
type ScopedWorkflowSummary = {
totalSteps: number;
completedSteps: number;
failedSteps: number;
skippedSteps: number;
};
type ScopedWorkflowStepResult = {
stepId: string;
title: string;
status: "ok" | "error" | "skipped";
correlationId?: string;
result?: ScopedWorkflowProcessStepResultData;
error?: string;
code?: string;
};
type ScopedWorkflowProcessStepResultData = {
command: string;
args: string[];
cwd?: string;
exitCode?: number | null;
stdout?: string;
stderr?: string;
durationMs?: number;
dryRun?: boolean;
};
Workflow status values
| Status | Meaning |
|---|
"completed" | All steps ran and succeeded |
"partial" | Some steps completed, some failed or were skipped |
"failed" | The workflow failed before completing |
Workflow diagnostics
The SDK provides structured helpers for analyzing workflow results without ad hoc logic.
summarizeWorkflowResult()
Returns a flat summary object from a ScopedWorkflowResult:
import { summarizeWorkflowResult } from "@anikitenko/fdo-sdk";
const summary = summarizeWorkflowResult(result);
// {
// workflowId: "...",
// scope: "terraform",
// kind: "process-sequence",
// status: "partial",
// totalSteps: 2,
// completedSteps: 1,
// failedSteps: 1,
// skippedSteps: 0,
// }
getFailedWorkflowSteps()
Returns all steps with status === "error":
import { getFailedWorkflowSteps } from "@anikitenko/fdo-sdk";
const failedSteps = getFailedWorkflowSteps(result);
// ScopedWorkflowStepResult[] where status === "error"
createWorkflowFailureDiagnostic()
Returns a structured WorkflowFailureDiagnostic when the workflow has failed steps, or null when all steps succeeded:
import { createWorkflowFailureDiagnostic } from "@anikitenko/fdo-sdk";
const diagnostic = createWorkflowFailureDiagnostic(result);
if (diagnostic) {
// diagnostic.workflowId
// diagnostic.scope
// diagnostic.status
// diagnostic.failedStepIds — string[]
// diagnostic.failedStepTitles — string[]
// diagnostic.firstFailedStep — { stepId, title, correlationId, error, code }
// diagnostic.remediation — human-readable remediation hint
console.error("Workflow failed:", diagnostic.remediation);
}
The following is adapted from examples/fixtures/operator-terraform-plugin.fixture.ts:
import {
FDOInterface,
FDO_SDK,
PluginMetadata,
PluginRegistry,
createOperatorToolCapabilityPreset,
requestScopedWorkflow,
isPrivilegedActionSuccessResponse,
createWorkflowFailureDiagnostic,
summarizeWorkflowResult,
} from "@anikitenko/fdo-sdk";
export default class TerraformOperatorPlugin extends FDO_SDK implements FDOInterface {
private readonly _metadata: PluginMetadata = {
name: "Fixture: Terraform Operator",
version: "1.0.0",
author: "FDO SDK Team",
description: "Reference fixture for Terraform-style operator plugins",
icon: "predictive-analysis",
};
get metadata(): PluginMetadata {
return this._metadata;
}
declareCapabilities() {
return createOperatorToolCapabilityPreset("terraform");
// ["system.process.exec", "system.process.scope.terraform"]
}
init(): void {
PluginRegistry.registerHandler(
"terraform.previewApplyWorkflow",
async () => this.previewAndApplyWorkflow()
);
}
render(): string {
return `
<div style="padding: 16px;">
<h1>Terraform Operator</h1>
<button id="terraform-preview-apply">Preview + Apply</button>
<pre id="terraform-result"></pre>
</div>
`;
}
async previewAndApplyWorkflow(): Promise<unknown> {
const response = await requestScopedWorkflow("terraform", {
kind: "process-sequence",
title: "Terraform preview and apply",
summary: "Preview infrastructure changes before apply",
dryRun: true,
steps: [
{
id: "plan",
title: "Generate plan",
phase: "preview",
command: "/usr/local/bin/terraform",
args: ["plan", "-input=false"],
timeoutMs: 10000,
reason: "preview infrastructure plan",
onError: "abort",
},
{
id: "apply",
title: "Apply plan",
phase: "apply",
command: "/usr/local/bin/terraform",
args: ["apply", "-input=false", "tfplan"],
timeoutMs: 10000,
reason: "apply approved infrastructure plan",
onError: "abort",
},
],
confirmation: {
message: "Apply infrastructure changes?",
requiredForStepIds: ["apply"],
},
});
if (isPrivilegedActionSuccessResponse(response) && response.result) {
const result = response.result;
const summary = summarizeWorkflowResult(result);
this.info("Workflow completed", summary);
const diagnostic = createWorkflowFailureDiagnostic(result);
if (diagnostic) {
this.warn("Workflow has failed steps", diagnostic);
}
}
return response;
}
}
Full example: kubectl inspect and restart
The following is adapted from examples/fixtures/operator-kubernetes-plugin.fixture.ts:
import {
requestScopedWorkflow,
} from "@anikitenko/fdo-sdk";
async inspectAndRestartWorkflow(): Promise<unknown> {
return requestScopedWorkflow("kubectl", {
kind: "process-sequence",
title: "Inspect and restart deployment",
summary: "Inspect deployment state before running a scoped rollout restart",
dryRun: true,
steps: [
{
id: "inspect-deployment",
title: "Inspect deployment",
phase: "inspect",
command: "/usr/local/bin/kubectl",
args: ["get", "deployment", "api", "-n", "default", "-o", "json"],
timeoutMs: 5000,
reason: "inspect deployment state before restart",
onError: "abort",
},
{
id: "restart-deployment",
title: "Restart deployment",
phase: "mutate",
command: "/usr/local/bin/kubectl",
args: ["rollout", "restart", "deployment/api", "-n", "default"],
timeoutMs: 5000,
reason: "restart deployment after inspection",
onError: "abort",
},
],
confirmation: {
message: "Restart deployment api in namespace default?",
requiredForStepIds: ["restart-deployment"],
},
});
}
The apply and mutate steps in a real workflow are not dry-run safe by default. Always test with dryRun: true first, and only remove that flag when you have confirmed the scope policy with the host operator.