> ## 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 Guard

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" />

Bails out of the current path when an assign is in (or isn't in) an
expected state. The go-to tool for "stop if the lookup came up empty"
and similar guardrails.

### Configuration

* **Assign** — which assign to inspect.
* **Bail when**
  * *value is empty* (default): trips on `nil`, empty string, empty
    list, or empty map. Matches the common "upstream lookup found
    nothing" pattern.
  * *value is nil*: strict nil-only check. Empty strings/lists/maps
    pass through — useful when an empty collection is a real value.
  * *value is falsey*: trips on `nil` or `false`. Use when a boolean
    assign signals "go" vs "stop".
  * *value is defined*: inverse — trips when the assign has any value.
    Useful for "skip if already exists" flows.
* **On bail**
  * *Skip branch* (default): stop only this path. Parallel branches
    keep going. Inside Flow Each, the next iteration runs normally.
    Nothing is recorded as an error.
  * *Halt flow*: stop the whole run. Marked as aborted in the run
    history.
* **Reason** *(optional)* — the message surfaced when the guard trips.
  Supports `{assign_key}` interpolation. Defaults to an auto-generated
  message.
* **Completion key** *(optional)* — when set, a *Skip branch* bail
  also writes a completion marker for this key (the same store a
  *Completion Guard* reads). Supports `{assign_key}` interpolation.
  *Halt flow* bails never write a marker. Leave blank to disable.
* **Completion key assign** *(optional)* — alternatively, name an
  assign whose raw value is the marker key — the natural choice when
  the key is an external reference string. The literal key above wins
  when both are set.
* **Counter** *(optional)* — when a counter name is set, a *Skip
  branch* bail also increments that run counter (shown on the run
  page, same as a *Flow Counter* node). Name and category support
  `{assign_key}` interpolation; the increment defaults to 1. *Halt
  flow* bails never count. Leave the name blank to disable.

### Tips

* Use *Halt flow* for invariants you want to fail loudly (e.g.
  "external id must be set before proceeding"). Use *Skip branch* for
  "best-effort" paths.
* `0`, `false`, a whitespace-only string, a list containing nil, a
  map with nil values, and any record are **not** empty — they're
  real values. Pick *value is nil* if you want only the literal nil
  to trip, or *value is falsey* to treat `false` as a stop signal
  alongside nil.
* Set **Completion key** to make this guard backfill the local
  completion store from a remote "already done" check: a cheap
  upstream *Completion Guard* on the same key then short-circuits
  future runs without re-querying the remote. Handy when adding
  completion-guarding to a flow that has already processed entities
  the local store never recorded.
* Best practice: only set **Completion key** when a bail means the
  entity is *permanently* done — the natural fit is *value is
  defined* / "already exists". With *value is empty* it permanently
  skips an entity whose lookup was only transiently empty.

## Ports

<NodePorts
  nodeName="Flow Guard"
  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 },
]}
/>
