Skip to main content
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:
KindDescription
"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:
PhasePurpose
"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

FieldTypeRequiredDescription
kind"process-sequence"YesWorkflow kind
titlestringYesHuman-readable workflow name
summarystringNoAdditional description for auditing
dryRunbooleanNoValidate without executing any steps
stepsScopedWorkflowProcessStep[]YesOrdered list of steps
confirmationScopedWorkflowConfirmationNoConfirmation 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

StatusMeaning
"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);
}

Full example: Terraform plan and apply

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.