> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twine.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Flow Action

export const NodeMode = ({mode = 'data'}) => {
  const ENGINES = {
    data: [{
      label: 'Data Engine',
      href: '/data-engine'
    }],
    flow: [{
      label: 'Flow Engine',
      href: '/flow-engine'
    }],
    any: [{
      label: 'Data Engine',
      href: '/data-engine'
    }, {
      label: 'Flow Engine',
      href: '/flow-engine'
    }]
  };
  const engines = ENGINES[mode] || ENGINES.data;
  return <>
      <style>{`
        .node-mode {
          display: flex;
          flex-wrap: wrap;
          align-items: center;
          gap: 8px;
          margin: 0 0 20px;
        }
        .node-mode-label {
          font-size: 11px;
          font-weight: 600;
          text-transform: uppercase;
          letter-spacing: 0.05em;
          color: var(--colors-content-secondary, #6b7280);
        }
        .node-mode-pill {
          display: inline-flex;
          align-items: center;
          padding: 3px 10px;
          border-radius: 999px;
          font-size: 12px;
          font-weight: 600;
          text-decoration: none;
          color: inherit;
          background: rgb(var(--primary-light) / .15);
          border: 1px solid rgb(var(--primary-light) / .5);
        }
        .node-mode-pill:hover {
          background: rgb(var(--primary-light) / .25);
        }
      `}</style>
      <div className="node-mode">
        <span className="node-mode-label">Available in</span>
        {engines.map(engine => <a key={engine.label} className="node-mode-pill" href={engine.href}>
            {engine.label}
          </a>)}
      </div>
    </>;
};

export const NodePorts = ({nodeName, inputs = [], outputs = []}) => {
  if (inputs.length === 0 && outputs.length === 0) return null;
  const formatTypes = types => {
    if (!types || types.length === 0) return 'Any';
    if (Array.isArray(types)) return types.join(', ');
    return types;
  };
  const Card = ({port, side}) => <div className={`node-port-card node-port-card-${side}`}>
      <div className="node-port-head">
        <span className="node-port-name">{port.name}</span>
        <span className="node-port-id">{port.id}</span>
      </div>
      {port.doc && <div className="node-port-doc">{port.doc}</div>}
      <div className="node-port-tags">
        <span className="node-port-tag node-port-tag-types">{formatTypes(port.types)}</span>
        {port.required && <span className="node-port-tag node-port-tag-required">required</span>}
        {port.arity === 'many' && <span className="node-port-tag">many</span>}
        {port.defaultConnection && <span className="node-port-tag node-port-tag-default">default</span>}
      </div>
    </div>;
  return <>
      <style>{`
        .node-diagram {
          display: grid;
          grid-template-columns: 1fr auto 1fr;
          gap: 32px;
          align-items: stretch;
          margin: 24px 0 32px;
        }
        .node-diagram-col {
          display: flex;
          flex-direction: column;
          gap: 12px;
          min-width: 0;
        }
        .node-diagram-col-label {
          font-size: 11px;
          font-weight: 600;
          text-transform: uppercase;
          letter-spacing: 0.05em;
          color: var(--colors-content-secondary, #6b7280);
          margin-bottom: 4px;
        }
        .node-diagram-col-center {
          display: flex;
          align-items: stretch;
          justify-content: center;
          position: relative;
          /* Push the center node down so it aligns with the cards, not the column labels */
          padding-top: 27px;
        }
        .node-diagram-node {
          display: flex;
          align-items: center;
          justify-content: center;
          padding: 18px 14px;
          border-radius: 10px;
          background: rgb(var(--primary-light) / .15);
          border: 1.5px solid rgb(var(--primary-light) / .55);
          font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
          font-size: 15px;
          font-weight: 700;
          color: inherit;
          box-shadow: 0 1px 3px rgb(0 0 0 / .04);
          writing-mode: vertical-rl;
          transform: rotate(180deg);
          letter-spacing: 0.05em;
        }
        .node-port-card {
          position: relative;
          border: 1px solid rgb(var(--primary-light) / .25);
          border-radius: 8px;
          padding: 10px 12px;
          background: rgb(var(--primary-light) / .04);
          font-size: 12px;
          line-height: 1.4;
        }
        .node-port-card::after {
          content: '';
          position: absolute;
          top: 50%;
          width: 32px;
          border-top: 1px dashed rgb(var(--primary-light) / .7);
        }
        .node-port-card-left::after {
          right: -32px;
        }
        .node-port-card-right::after {
          left: -32px;
        }
        .node-port-head {
          display: flex;
          align-items: baseline;
          justify-content: space-between;
          gap: 8px;
          margin-bottom: 4px;
        }
        .node-port-name {
          font-weight: 600;
          font-size: 13px;
          color: inherit;
        }
        .node-port-id {
          font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
          font-size: 10.5px;
          color: var(--colors-content-secondary, #6b7280);
        }
        .node-port-doc {
          color: #4b5563;
          margin-bottom: 6px;
        }
        .dark .node-port-doc,
        html.dark .node-port-doc,
        :is(.dark, [data-theme="dark"]) .node-port-doc {
          color: #d1d5db;
        }
        .node-port-tags {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;
        }
        .node-port-tag {
          display: inline-flex;
          align-items: center;
          padding: 2px 6px;
          border-radius: 4px;
          font-size: 10px;
          font-weight: 600;
          background: rgb(var(--primary-light) / .15);
          color: var(--colors-content-secondary, #6b7280);
          text-transform: lowercase;
          letter-spacing: 0.02em;
        }
        .node-port-tag-types {
          font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
          text-transform: none;
          background: rgb(var(--primary-light) / .25);
          color: inherit;
        }
        .node-port-tag-required {
          background: var(--colors-warning-light, #fef3c7);
          color: var(--colors-warning, #b45309);
        }
        .node-port-tag-default {
          background: var(--colors-accent-light, #ede9fe);
          color: var(--colors-accent, #7c3aed);
        }
        @media (max-width: 720px) {
          .node-diagram {
            grid-template-columns: 1fr;
            gap: 16px;
          }
          .node-diagram-col-center {
            order: -1;
          }
          .node-port-card::before,
          .node-port-card::after {
            display: none !important;
          }
        }
      `}</style>
      <div className="node-diagram">
        <div className="node-diagram-col node-diagram-col-left">
          {inputs.length > 0 && <div className="node-diagram-col-label">Inputs</div>}
          {inputs.map(p => <Card key={p.id} port={p} side="left" />)}
        </div>
        <div className="node-diagram-col-center">
          <div className="node-diagram-node">{nodeName}</div>
        </div>
        <div className="node-diagram-col node-diagram-col-right">
          {outputs.length > 0 && <div className="node-diagram-col-label">Outputs</div>}
          {outputs.map(p => <Card key={p.id} port={p} side="right" />)}
        </div>
      </div>
    </>;
};

<NodeMode mode="flow" />

Runs a pipeline step — the named unit of work that talks to a source or target
system (e.g. "Load all employees", "Save changes", "Get one project"). Most
flows are built around Flow Actions with transformation and control-flow nodes
in between.

### Configuration

* **Step** — pick the step to run. Grouped by system in the picker; internal
  steps live in the "Internal" group.
* **System integration** — which tenant's credentials the step uses. Required
  for steps that call remote APIs, ignored for local steps.
* **Step options** — typed form rendered from the step's declared options.
  Defaults are materialized the first time you pick the step; cleared fields
  stay cleared.
* **Cache mode** — `Default` reuses results from earlier in the same run when
  the step supports caching; `Force` busts the flow cache and asks the system
  to refresh any upstream caches it exposes.
* **On error** — what happens if the step fails:
  * *Halt flow* (default): stop the whole run.
  * *Halt branch*: stop this path only; parallel paths keep going.
  * *Continue*: log the error and keep running. Most useful for best-effort
    sync actions.
* **Use real data in test runs** — when on and the node lacks complete test
  fixtures, test runs execute the step for real, side effects included
  (writes to external systems, once per iteration inside loops). Complete
  fixtures always win and skip execution. When off, a test run errors if any
  declared assign lacks a fixture.

### Error port

Wire the **Error** output port to a subgraph (e.g. an Anomaly node) to handle
a failure. On error the subgraph runs as a detour — it records what its nodes
emit (anomalies, assigns) — and only *then* does the *On error* policy apply:
*Halt flow* aborts the run, *Halt branch* stops this path, *Continue* resumes
the normal **Out** path. The error subgraph never runs when the step succeeds.
Either way the error is logged and the run finishes as `partial`, not `ok`.

### Tips

* Steps declare what they read from `token.opts` and what they write to
  `token.assigns`. Downstream Flow nodes read those assigns by name.
* Steps that write to an assign show the assign in the Evaluator and Settings
  panes so you can pick what to persist per run.

## Ports

<NodePorts
  nodeName="Flow Action"
  inputs={[
{ id: 'default_in', name: 'In', doc: 'Default input port', types: 'Any', required: true, defaultConnection: true },
]}
  outputs={[
{ id: 'default_out', name: 'Out', doc: 'Default output port', types: 'Any', required: true, defaultConnection: true },
{ id: 'error_out', name: 'Error', doc: 'Subgraph run as a detour when the step fails, before the On error policy applies. Never runs on success.', types: 'Any' },
]}
/>
