Workflow inventory and ownership

An automation inventory answers who owns each Workflow, what starts it, which external systems it can change, how it is monitored, and whether it is still required. Keep the ownership registry outside the Workflow graph and refresh its technical fields from the FastHook API.

Minimum inventory fields

| Field | Source | Purpose | | --- | --- | --- | | Team ID, Workflow ID, name, status, version | GET /v1/workflows | Stable identity and current lifecycle state | | Source ID, provider, Trigger, Source status | Workflow summary | Ingress and shared-Source impact | | Step and Edge counts | Workflow summary | Basic graph complexity signal | | Destination, Filter, and Transformation IDs | GET /v1/workflows/:id Steps | Reusable resource dependencies | | Action provider, operation, auth mode, provider account ID | Action Step configuration | External-side-effect and credential dependency | | Business owner and technical owner | Governance registry | Outcome approval and incident response | | Environment and data classification | Governance registry | Isolation and handling requirements | | Alert route, runbook, reconciliation, SLO | Governance registry | Operational readiness | | Last reviewed, next review, lifecycle decision | Governance registry | Detect abandoned automations |

FastHook does not currently store business owner, cost center, SLO, or retirement date as first-class Workflow fields. Do not overload a resource name with all governance metadata; keep a versioned registry keyed by team ID and Workflow ID.

Read-only API inventory

The Workflow list endpoint returns 100 records by default and at most 255. It has no pagination cursor or total field. A response containing exactly 255 records may be incomplete; fail the inventory rather than treating it as authoritative.

Run this example with Node.js 18 or later. It retrieves each current graph but emits only dependency metadata:

JS
const API_BASE = (process.env.FASTHOOK_API_BASE || "https://api.fasthook.io").replace(/\/$/, "");
const API_KEY = process.env.FASTHOOK_API_KEY;
const TEAM_ID = process.env.FASTHOOK_TEAM_ID;

if (!API_KEY || !TEAM_ID) {
  console.error("FASTHOOK_API_KEY and FASTHOOK_TEAM_ID are required");
  process.exit(3);
}

async function fasthook(path) {
  const response = await fetch(`${API_BASE}${path}`, {
    headers: {
      authorization: `Bearer ${API_KEY}`,
      "x-team-id": TEAM_ID,
      accept: "application/json"
    },
    signal: AbortSignal.timeout(20_000)
  });
  if (!response.ok) throw new Error(`${path} returned HTTP ${response.status}`);
  return response.json();
}

function dependency(step) {
  if (step.kind === "action") {
    return {
      step_id: step.id,
      kind: step.kind,
      provider_key: step.action?.provider_key || null,
      action_key: step.action?.action_key || null,
      auth_mode: step.action?.auth_mode || null,
      provider_account_id: step.action?.provider_account_id || null
    };
  }
  return {
    step_id: step.id,
    kind: step.kind,
    resource_id: step.resource_id || null
  };
}

async function main() {
  const listed = await fasthook("/v1/workflows?limit=255");
  const summaries = Array.isArray(listed.models) ? listed.models : [];
  if (summaries.length >= 255) {
    throw new Error("Workflow inventory may be incomplete: API returned the 255-record maximum");
  }

  const models = [];
  for (let offset = 0; offset < summaries.length; offset += 10) {
    const batch = summaries.slice(offset, offset + 10);
    const graphs = await Promise.all(batch.map((item) =>
      fasthook(`/v1/workflows/${encodeURIComponent(item.id)}`)
    ));
    for (const graph of graphs) {
      models.push({
        team_id: TEAM_ID,
        workflow_id: graph.id,
        name: graph.name,
        status: graph.status,
        version: graph.version,
        source: {
          id: graph.source_id,
          name: graph.source?.name || null,
          status: graph.source?.status || null,
          provider_key: graph.source?.provider_key || null,
          definition_key: graph.source?.definition_key || null
        },
        steps_count: Array.isArray(graph.steps) ? graph.steps.length : 0,
        edges_count: Array.isArray(graph.edges) ? graph.edges.length : 0,
        dependencies: Array.isArray(graph.steps) ? graph.steps.map(dependency) : [],
        updated_at: graph.updated_at
      });
    }
  }

  models.sort((a, b) => a.workflow_id.localeCompare(b.workflow_id));
  console.log(JSON.stringify({
    schema_version: 1,
    generated_at: new Date().toISOString(),
    team_id: TEAM_ID,
    count: models.length,
    models
  }, null, 2));
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : "inventory_failed");
  process.exitCode = 3;
});

Store the API key in the runner’s secret manager. The output deliberately excludes graph mappings, defaults, conditions, Trigger payloads, Step outputs, credentials, and provider response messages.

Ownership registry example

Merge API output with a reviewed registry such as:

JSON
{
  "team_id": "tm_production",
  "workflow_id": "wfl_payment_failure_alert",
  "environment": "production",
  "business_owner": "billing-operations",
  "technical_owner": "automation-platform",
  "data_classification": "customer-financial-metadata",
  "runbook": "runbook://billing/payment-failure-alert",
  "alert_route": "on-call://billing-automation",
  "reconciliation": "report://payment-alert-coverage",
  "last_reviewed_at": "2026-08-27",
  "next_review_at": "2026-11-27",
  "lifecycle": "approved"
}

Choose URIs that work in your internal registry. Do not put passwords, access tokens, signing secrets, raw customer data, or copied Audit previews in ownership metadata.

Review cadence

Review active production Workflows after material provider, schema, permission, ownership, or data-policy changes and on a fixed schedule. Flag:

Use Dependency impact analysis before changing a shared resource and Workflow deprecation and retirement for stale entries.