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

# Network Job Pipelines

> The workload classes the Livepeer Network runs (video transcoding, batch AI inference, real-time AI) and how the capability set grows.

export const ScrollableDiagram = ({children, title = '', maxHeight = '500px', minWidth = '100%', showControls = false, className = '', style = {}, ...rest}) => {
  const buildDiagramKey = (currentTitle = '', currentClassName = '') => {
    const source = `${currentTitle}|${currentClassName}|scrollable-diagram`;
    let hash = 0;
    for (let index = 0; index < source.length; index += 1) {
      hash = hash * 31 + source.charCodeAt(index) >>> 0;
    }
    return `docs-diagram-${hash.toString(36)}`;
  };
  const diagramKey = buildDiagramKey(title, className);
  const zoomName = `${diagramKey}-zoom`;
  const zoomLevels = [{
    label: '75%',
    value: 0.75
  }, {
    label: '100%',
    value: 1
  }, {
    label: '125%',
    value: 1.25
  }, {
    label: '150%',
    value: 1.5
  }];
  const containerStyle = {
    overflow: 'auto',
    maxHeight,
    border: '1px solid var(--lp-color-border-default)',
    borderRadius: "8px",
    padding: "var(--lp-spacing-4)",
    background: 'var(--lp-color-bg-card)',
    position: 'relative'
  };
  return <div className={className} style={{
    position: 'relative',
    marginBottom: "var(--lp-spacing-4)",
    ...style
  }} {...rest}>
      {title && <p style={{
    textAlign: 'center',
    fontStyle: 'italic',
    color: 'var(--lp-color-text-secondary)',
    marginBottom: "var(--lp-spacing-2)",
    fontSize: '0.875rem'
  }}>
          {title}
        </p>}

      {showControls ? <style>{`
          [data-docs-diagram-key="${diagramKey}"] [data-docs-diagram-content] {
            transform: scale(1);
            transform-origin: top left;
            width: max-content;
          }
          ${zoomLevels.map(zoomLevel => `
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-shell] [data-docs-diagram-content] {
            transform: scale(${zoomLevel.value});
          }
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-controls] label[for="${diagramKey}-${zoomLevel.label.replace('%', '')}"] {
            background: var(--lp-color-accent);
            color: var(--lp-color-on-accent);
            border-color: var(--lp-color-accent);
          }`).join('\n')}
        `}</style> : null}

      {showControls ? zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <input key={inputId} id={inputId} type="radio" name={zoomName} defaultChecked={zoomLevel.value === 1} style={{
      position: 'absolute',
      opacity: 0,
      pointerEvents: 'none'
    }} />;
  }) : null}

      <div data-docs-diagram-key={diagramKey} data-docs-diagram-shell style={containerStyle}>
        <div data-docs-diagram-content style={{
    minWidth,
    transformOrigin: 'top left',
    width: 'max-content'
  }}>
          {children}
        </div>
      </div>

      {showControls ? <div data-docs-diagram-controls style={{
    display: 'flex',
    justifyContent: 'flex-end',
    alignItems: 'center',
    gap: "var(--lp-spacing-2)",
    marginTop: "var(--lp-spacing-2)",
    flexWrap: 'wrap'
  }}>
          <span style={{
    fontSize: "0.75rem",
    color: 'var(--lp-color-text-muted)',
    marginRight: 'auto'
  }}>
            Scroll to pan
          </span>
          {zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <label key={inputId} htmlFor={inputId} style={{
      background: 'transparent',
      color: 'var(--lp-color-text-secondary)',
      border: '1px solid var(--lp-color-border-default)',
      borderRadius: "4px",
      padding: '4px 10px',
      cursor: 'pointer',
      fontSize: "0.75rem",
      fontWeight: '600'
    }}>
                {zoomLevel.label}
              </label>;
  })}
        </div> : null}
    </div>;
};

export const StyledStep = ({title, icon, titleSize = 'h3', iconColor = null, titleColor = null, children, className = '', style = {}, ...rest}) => {
  const styledTitle = titleColor ? <span style={{
    color: titleColor
  }}>{title}</span> : title;
  return <Step title={styledTitle} icon={icon} iconColor={iconColor || undefined} titleSize={titleSize} className={className} style={style} {...rest}>
      {children}
    </Step>;
};

export const StyledSteps = ({children, iconColor, titleColor, lineColor, iconSize = '24px', className = '', style = {}, ...rest}) => {
  const resolvedIconColor = iconColor || 'var(--accent-dark, #18794E)';
  const resolvedTitleColor = titleColor || 'var(--lp-color-accent)';
  const resolvedLineColor = lineColor || 'var(--lp-color-accent)';
  return <div className={['docs-styled-steps', className].filter(Boolean).join(' ')} style={style} {...rest}>
      <style>{`
        .docs-styled-steps .steps > div > div.absolute > div {
          background-color: ${resolvedIconColor};
        }
        .docs-styled-steps .steps > div > div.w-full > p {
          color: ${resolvedTitleColor};
        }
        .docs-styled-steps .steps > div > div.absolute.w-px {
          background-color: ${resolvedLineColor};
        }
        .docs-styled-steps .steps > div:last-child > div.absolute.w-px::after {
          content: '';
          position: absolute;
          bottom: 0;
          left: 50%;
          transform: translateX(-50%);
          width: 6px;
          height: 6px;
          background-color: ${resolvedLineColor};
          transform: translateX(-50%) rotate(45deg);
        }
      `}</style>
      <div>
        <Steps>{children}</Steps>
      </div>
    </div>;
};

export const DynamicTableV2 = ({tableTitle = null, headerList = [], itemsList = [], monospaceColumns = [], columnWidths = {}, columnConfig = {}, showSeparators = false, margin, className = '', style = {}, ...rest}) => {
  if (!headerList.length) {
    return <div>No headers provided</div>;
  }
  const tableRef = useRef(null);
  const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
  const measureFitColumns = () => {
    const tableElement = tableRef.current;
    if (!tableElement) {
      return;
    }
    const nextWidths = headerList.reduce((accumulator, header, index) => {
      const config = columnConfig?.[header] || ({});
      if (!config.fitContent) {
        return accumulator;
      }
      const contentNodes = tableElement.querySelectorAll(`[data-docs-column-key="${index}"] [data-docs-fit-content]`);
      let maxContentWidth = 0;
      contentNodes.forEach(node => {
        const width = Math.ceil(node.getBoundingClientRect().width);
        if (width > maxContentWidth) {
          maxContentWidth = width;
        }
      });
      if (maxContentWidth > 0) {
        accumulator[header] = `${maxContentWidth + 16}px`;
      }
      return accumulator;
    }, {});
    setMeasuredColumnWidths(currentWidths => {
      const currentEntries = Object.entries(currentWidths);
      const nextEntries = Object.entries(nextWidths);
      if (currentEntries.length === nextEntries.length && nextEntries.every(([header, width]) => currentWidths[header] === width)) {
        return currentWidths;
      }
      return nextWidths;
    });
  };
  useLayoutEffect(() => {
    measureFitColumns();
  }, [headerList, itemsList, columnConfig]);
  useEffect(() => {
    const tableElement = tableRef.current;
    if (!tableElement || typeof ResizeObserver === 'undefined') {
      return undefined;
    }
    const resizeObserver = new ResizeObserver(() => {
      measureFitColumns();
    });
    resizeObserver.observe(tableElement);
    if (tableElement.parentElement) {
      resizeObserver.observe(tableElement.parentElement);
    }
    return () => {
      resizeObserver.disconnect();
    };
  }, [headerList, itemsList, columnConfig]);
  const fitHeaders = headerList.filter(header => columnConfig?.[header]?.fitContent);
  const hasMeasuredFitColumns = fitHeaders.length === 0 || fitHeaders.every(header => Boolean(measuredColumnWidths[header]));
  const getColumnStyle = (header, isMonospace = false) => {
    const config = columnConfig?.[header] || ({});
    const fitContent = Boolean(config.fitContent);
    const fluid = Boolean(config.fluid);
    const nowrap = Boolean(config.nowrap) || fitContent || isMonospace;
    const preferredWidth = columnWidths[header];
    const measuredWidth = measuredColumnWidths[header];
    return {
      ...fitContent && measuredWidth ? {
        width: measuredWidth,
        minWidth: measuredWidth,
        maxWidth: measuredWidth
      } : {},
      ...!fitContent && !fluid && preferredWidth ? {
        minWidth: preferredWidth
      } : {},
      ...nowrap ? {
        whiteSpace: 'nowrap'
      } : {
        wordWrap: 'break-word',
        overflowWrap: 'break-word'
      }
    };
  };
  const getColumnTrackStyle = header => {
    const config = columnConfig?.[header] || ({});
    const fitContent = Boolean(config.fitContent);
    const fluid = Boolean(config.fluid);
    const preferredWidth = columnWidths[header];
    const measuredWidth = measuredColumnWidths[header];
    if (fitContent && measuredWidth) {
      return {
        width: measuredWidth,
        minWidth: measuredWidth,
        maxWidth: measuredWidth
      };
    }
    if (fluid) {
      return {};
    }
    if (preferredWidth) {
      return {
        width: preferredWidth
      };
    }
    return {};
  };
  const renderCellContent = (header, content) => {
    const config = columnConfig?.[header] || ({});
    if (!config.fitContent) {
      return content;
    }
    return <div data-docs-fit-content style={{
      display: 'inline-flex',
      alignItems: 'center',
      whiteSpace: 'nowrap',
      width: 'max-content',
      maxWidth: 'none'
    }}>
        {content}
      </div>;
  };
  return <div className={className} style={style} {...rest}>
      {tableTitle && <div style={{
    fontStyle: 'italic',
    margin: 0
  }}>
          <strong>{tableTitle}</strong>
        </div>}
      <div style={{
    overflowX: 'auto',
    ...margin != null && ({
      margin
    })
  }} role="region" tabIndex={0} aria-label={tableTitle ? `Scrollable table: ${tableTitle}` : 'Scrollable table'}>
        <table ref={tableRef} data-docs-dynamic-table-v2 style={{
    width: '100%',
    tableLayout: hasMeasuredFitColumns ? 'fixed' : 'auto',
    borderCollapse: 'collapse',
    fontSize: '0.9rem',
    marginTop: 0
  }}>
          <colgroup>
            {headerList.map((header, index) => <col key={index} style={getColumnTrackStyle(header)} />)}
          </colgroup>
          <thead>
            <tr style={{
    backgroundColor: 'var(--lp-color-accent)',
    color: 'var(--lp-color-on-accent)',
    borderBottom: '1px solid var(--lp-color-border-default)'
  }}>
              {headerList.map((header, index) => <th key={index} data-docs-column-key={index} style={{
    padding: '10px 8px',
    textAlign: 'left',
    fontWeight: '600',
    color: 'var(--lp-color-on-accent)',
    verticalAlign: 'top',
    ...getColumnStyle(header)
  }}>
                  {renderCellContent(header, header)}
                </th>)}
            </tr>
          </thead>
          <tbody>
            {itemsList.filter(item => showSeparators || !item?.__separator).map((item, rowIndex) => item?.__separator ? <tr key={rowIndex} style={{
    backgroundColor: 'var(--lp-color-accent)',
    color: 'var(--lp-color-on-accent)',
    borderBottom: '1px solid var(--lp-color-accent)'
  }}>
                    <td colSpan={headerList.length} style={{
    padding: '6px 8px',
    fontWeight: '700',
    color: 'var(--lp-color-on-accent)',
    letterSpacing: '0.01em'
  }}>
                      {(item[headerList[0]] ?? item.Category) ?? 'Category'}
                    </td>
                  </tr> : <tr key={rowIndex} style={{
    borderBottom: '1px solid var(--lp-color-border-default)'
  }}>
                    {headerList.map((header, colIndex) => {
    const value = (item[header] ?? item[header.toLowerCase()]) ?? '-';
    const isMonospace = monospaceColumns.includes(colIndex);
    return <td key={colIndex} data-docs-column-key={colIndex} style={{
      padding: '8px 8px',
      fontFamily: isMonospace ? 'monospace' : 'inherit',
      verticalAlign: 'top',
      ...getColumnStyle(header, isMonospace)
    }}>
                          {renderCellContent(header, isMonospace ? <code>{value}</code> : value)}
                        </td>;
  })}
                  </tr>)}
          </tbody>
        </table>
      </div>
    </div>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

export const Quote = ({children, className = "", style = {}, ...rest}) => {
  const quoteStyle = {
    fontSize: "1rem",
    textAlign: 'center',
    opacity: 1,
    fontStyle: 'italic',
    color: 'var(--lp-color-accent)',
    border: '1px solid var(--lp-color-border-default)',
    borderRadius: "8px",
    padding: "var(--lp-spacing-4)",
    margin: '1rem 0',
    ...style
  };
  return <blockquote className={className} style={quoteStyle} {...rest}>{children}</blockquote>;
};

export const Subtitle = ({style = {}, text, children, variant = 'default', fontSize = '', fontWeight = '', fontStyle = '', marginTop = '', marginBottom = '', color = '', className = '', ...rest}) => {
  const renderInlineCode = (value, keyPrefix) => {
    return value.split(/(`[^`]+`)/g).map((segment, index) => {
      if (segment.startsWith('`') && segment.endsWith('`')) {
        return <code key={`${keyPrefix}-code-${index}`}>{segment.slice(1, -1)}</code>;
      }
      return segment;
    });
  };
  const renderInlineMarkup = (value, keyPrefix = 'subtitle') => {
    if (typeof value !== 'string') {
      return value;
    }
    return value.split(/(\*\*[\s\S]+?\*\*)/g).map((segment, index) => {
      if (segment.startsWith('**') && segment.endsWith('**')) {
        const inner = segment.slice(2, -2);
        return <strong key={`${keyPrefix}-strong-${index}`}>
            {renderInlineCode(inner, `${keyPrefix}-strong-${index}`)}
          </strong>;
      }
      return renderInlineCode(segment, `${keyPrefix}-${index}`);
    });
  };
  const renderContent = (value, keyPrefix) => {
    if (Array.isArray(value)) {
      return value.map((item, index) => renderContent(item, `${keyPrefix}-${index}`));
    }
    return renderInlineMarkup(value, keyPrefix);
  };
  const variants = {
    default: {
      fontSize: '1rem',
      fontStyle: 'italic',
      color: 'var(--lp-color-accent)',
      marginBottom: 0
    },
    changelog: {
      fontSize: '0.8rem',
      fontStyle: 'normal',
      fontWeight: 700,
      color: 'var(--lp-color-text-primary)',
      marginBottom: 0
    }
  };
  const base = variants[variant] || variants.default;
  return <span className={className} style={{
    ...base,
    ...fontSize ? {
      fontSize
    } : {},
    ...fontWeight ? {
      fontWeight
    } : {},
    ...fontStyle ? {
      fontStyle
    } : {},
    ...marginTop ? {
      marginTop
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...color ? {
      color
    } : {},
    ...style
  }} {...rest}>
      {renderContent(text, 'text')}
      {renderContent(children, 'children')}
    </span>;
};

export const CustomCardTitle = ({icon, title, variant = "card", iconSize, style = {}, className = "", ...rest}) => {
  const variants = {
    card: {
      display: 'flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)",
      marginBottom: "var(--lp-spacing-3)",
      color: 'var(--lp-color-text-primary)',
      fontSize: '1rem',
      fontWeight: 600
    },
    accordion: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)"
    },
    tab: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.4rem',
      fontSize: '0.875rem'
    }
  };
  const sizes = {
    card: 20,
    accordion: 18,
    tab: 14
  };
  const size = iconSize || sizes[variant] || 20;
  const baseStyle = variants[variant] || variants.card;
  return variant === 'card' ? <div className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </div> : <span className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </span>;
};

<Quote>
  A **pipeline** is a unit of work the Network knows how to run. Pipelines come in three classes by how the work is shaped: video transcoding, batch AI, and real-time AI. Adding a new pipeline expands the Network's capability set without changing the protocol.
</Quote>

<CustomDivider style={{ margin: 0, marginBottom: '-2rem' }} />

The Network's capability set is defined by the pipelines its Orchestrators run. A pipeline is a workload the Orchestrator can accept, with a defined input format, output format, and pricing unit. Pipelines are the unit of capability growth: when a new model or workload becomes available, it ships as a new pipeline that operators choose to load.

The key consequence is that the Network's capability set is open and grows by participation. New AI models, new transcoding profiles, and new custom workloads all enter the Network the same way: an Orchestrator declares the pipeline in its configuration, loads the model, and starts advertising the capability. No protocol upgrade required.

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Workload Classes

Every pipeline falls into one of three classes by how the work is shaped. The class determines what an Orchestrator must run, how the Gateway dispatches work, and what latency the workload tolerates.

<DynamicTableV2
  headerList={['Class', 'Examples', 'How it runs', 'Latency budget']}
  itemsList={[
{
  Class: <Subtitle variant="changelog">**Video transcoding**</Subtitle>,
  Examples: 'Live RTMP/WHIP streams, file VOD assets',
  'How it runs': 'Orchestrator dispatches to FFmpeg-based transcoders with NVENC/NVDEC GPU acceleration',
  'Latency budget': 'Sub-second for live; throughput-oriented for file',
},
{
  Class: <Subtitle variant="changelog">**Batch AI inference**</Subtitle>,
  Examples: 'Text-to-image, image-to-image, image-to-video, audio-to-text, segment-anything-2, upscale, large language models',
  'How it runs': 'Orchestrator dispatches to AI worker, which runs a Python `ai-runner` container with the model loaded',
  'Latency budget': 'Seconds to tens of seconds, depending on pipeline',
},
{
  Class: <Subtitle variant="changelog">**Real-time AI**</Subtitle>,
  Examples: 'Live video-to-video, real-time effects, ComfyStream pipelines',
  'How it runs': 'Frames carried over the trickle protocol between Gateway and Orchestrator AI worker; per-frame inference inside `ai-runner`',
  'Latency budget': 'Sub-second per frame for interactive use',
},
]}
/>

The state machine each pipeline follows is the same in shape: ingest, dispatch, compute, return, settle. The differences are in cadence, transport, and compute path. Operator-side detail on how the state machine is implemented lives in the Orchestrator and Gateway tabs.

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Job Lifecycle

Every pipeline has the same lifecycle from job intake to settlement, regardless of class. Cadence and transport differ; the lifecycle does not.

<StyledSteps iconColor="var(--accent)" titleColor="var(--accent)">
  <StyledStep title="Intake" icon="arrow-right-to-bracket">
    The Gateway accepts the work from a client: an RTMP or WHIP stream, an HTTP file upload, or an AI inference request. It produces segments or frames suitable for downstream dispatch.
  </StyledStep>

  <StyledStep title="Discovery and selection" icon="magnifying-glass">
    The Gateway selects an Orchestrator that advertises the pipeline at a price the Gateway accepts. Pipeline-specific capability is part of the selection criteria.
  </StyledStep>

  <StyledStep title="Dispatch and compute" icon="bolt">
    The Gateway dispatches segments or frames with attached probabilistic micropayment tickets. The Orchestrator runs the pipeline against the work, locally or on attached workers.
  </StyledStep>

  <StyledStep title="Result return" icon="circle-check">
    The Orchestrator returns transcoded segments, generated frames, or inference output. The Gateway delivers the result to the client.
  </StyledStep>

  <StyledStep title="Settlement" icon="ticket">
    Tickets accumulate off-chain. Winning tickets are redeemed on-chain by the Orchestrator. Per-round inflation rewards run in parallel through `BondingManager`.
  </StyledStep>
</StyledSteps>

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Job State Machine

Inside the lifecycle phases, jobs follow a per-segment state machine. The session-level transitions below show how a single job moves through ingest, dispatch, retry, and drained reserve.

<ScrollableDiagram title="Job Lifecycle State Machine" maxHeight="600px">
  ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
  %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': "Inter, 'Inter Fallback', -apple-system, system-ui" }}}%%
  stateDiagram-v2
    [*] --> Idle
    Idle --> Ingesting: ingest starts
    Ingesting --> Discovering: segmenter emits segments
    Discovering --> SessionEstablished: orchestrator selected, price accepted
    Discovering --> Failed: discovery errors exceed tolerance
    SessionEstablished --> SegmentDispatch: next segment ready
    SegmentDispatch --> Executing: segment uploaded to orchestrator
    Executing --> Returning: result ready
    Executing --> Retry: transcode failed or timed out
    Retry --> SegmentDispatch: retry within budget
    Retry --> SwapOrchestrator: retry budget exhausted
    SwapOrchestrator --> Discovering: choose new orchestrator
    Returning --> Verifying: optional fast verification
    Verifying --> Publishing: verified
    Verifying --> SwapOrchestrator: verification failed
    Publishing --> Settling: tickets sent
    Settling --> SegmentDispatch: next segment
    Settling --> Drained: reserve or deposit depleted
    Drained --> Failed: insufficient funds
    Publishing --> RoundReward: per-round reward call
    RoundReward --> Publishing
    Failed --> [*]
  ```
</ScrollableDiagram>

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Built-In Pipelines

The Network ships a set of built-in pipelines maintained in the `ai-runner` repository. Each runs as a Python container an Orchestrator can load and serve. The set covers the most common AI workloads.

<DynamicTableV2
  headerList={['Pipeline', 'Class', 'What it produces']}
  itemsList={[
{ Pipeline: 'Video transcoding profiles', Class: 'Video', 'What it produces': 'Transcoded video at requested resolution, bitrate, and codec' },
{ Pipeline: 'Text-to-image', Class: 'Batch AI', 'What it produces': 'Generated image from a text prompt' },
{ Pipeline: 'Image-to-image', Class: 'Batch AI', 'What it produces': 'Transformed image from an input image and prompt' },
{ Pipeline: 'Image-to-video', Class: 'Batch AI', 'What it produces': 'Short generated video clip from an input image' },
{ Pipeline: 'Audio-to-text', Class: 'Batch AI', 'What it produces': 'Transcribed text from input audio' },
{ Pipeline: 'Text-to-speech', Class: 'Batch AI', 'What it produces': 'Generated audio from text input' },
{ Pipeline: 'Segment-anything-2', Class: 'Batch AI', 'What it produces': 'Image segmentation masks' },
{ Pipeline: 'Upscale', Class: 'Batch AI', 'What it produces': 'Upscaled image or video' },
{ Pipeline: 'Frame interpolation', Class: 'Batch AI', 'What it produces': 'Generated intermediate frames for smoother motion' },
{ Pipeline: 'Large language models', Class: 'Batch AI', 'What it produces': 'Text completions from a prompt' },
{ Pipeline: 'Live video-to-video', Class: 'Real-time AI', 'What it produces': 'Per-frame transformed video output for interactive applications' },
{ Pipeline: 'ComfyStream', Class: 'Real-time AI', 'What it produces': 'Real-time output from custom ComfyUI workflows' },
]}
/>

The set evolves continuously. New pipelines ship through the `ai-runner` release cadence; Orchestrators choose which to load based on hardware, demand, and operator strategy.

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## Real-Time AI

Real-time AI pipelines (live video-to-video, ComfyStream) move frames continuously between Gateway and Orchestrator over the trickle protocol, with audio and control on parallel channels. The end-to-end path differs from batch pipelines in cadence and transport, not in marketplace mechanics.

<ScrollableDiagram title="Real-Time AI via Trickle" maxHeight="600px">
  ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
  %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': "Inter, 'Inter Fallback', -apple-system, system-ui" }}}%%
  sequenceDiagram
      participant Viewer as Viewer
      participant App as Client app
      participant Gw as Gateway
      participant Tr as Trickle server
      participant Orch as Orchestrator
      participant AI as AI Worker (ai-runner)

      App->>Gw: WHIP publish (live video in)
      Gw->>Tr: open channels (video, audio, control)
      Gw->>Orch: GetOrchestratorInfo
      Orch-->>Gw: capabilities, price, ticket params
      Gw->>Tr: POST /channel/seq=0 (frame batch)

      loop frames flow continuously
          Tr-->>Orch: GET /channel/seq (subscribe)
          Orch->>AI: dispatch frames + ticket
          AI->>AI: per-frame inference
          AI-->>Orch: transformed frames
          Orch->>Tr: POST /out-channel/seq (output)
          Tr-->>Gw: GET /out-channel/seq=-1 (live edge)
      end

      Gw->>Viewer: WHEP playback (transformed video)
  ```
</ScrollableDiagram>

The trickle server is logical, not a separate service: it is implemented inside `go-livepeer` on both Gateway and Orchestrator sides. Channels are named streams; a real-time AI session typically opens three (video in, video out, control) plus an audio pair. Subscribers can preconnect to the next sequence number to remove latency between segment boundaries, which is what makes the protocol viable for sub-second pipelines.

<CustomDivider style={{ margin: '-1rem 0 -2rem 0' }} />

## BYOC Pipelines

Pipelines outside the built-in set arrive through Bring-Your-Own-Container. A developer or operator packages a custom pipeline as a container, declares its interface, and either runs it themselves as an Orchestrator or partners with operators to run it for them.

BYOC pipelines use the same payment flow, the same dispatch shape, and the same settlement boundary as built-in pipelines. The difference is that the pipeline definition lives outside the `ai-runner` repository, controlled by the BYOC author. This is how new workload types reach the Network without coordination through Livepeer Inc.

The BYOC capability is registered with an Orchestrator at runtime via `/capability/register`. Once registered, Gateways discover the new capability through the Orchestrator's `OrchestratorInfo` advertisement on the next handshake. The marketplace treats BYOC pipelines the same way it treats built-in pipelines for the purposes of selection, pricing, and settlement.

<Card title={<CustomCardTitle icon="code-branch" title="Build a BYOC Pipeline" />} href="/v2/developers/build/byoc" horizontal arrow> How developers package and ship custom pipelines on the Network. </Card>

<CustomDivider />

## Failure Modes

Each transition in the state machine has a known failure mode and a known recovery path. The most common are summarised below.

<DynamicTableV2
  headerList={['Failure', 'Trigger', 'Recovery']}
  itemsList={[
{ Failure: 'No suitable Orchestrator', Trigger: 'Discovery errors exceed tolerance', Recovery: 'Session enters Failed; client retries with relaxed criteria' },
{ Failure: 'Segment compute failure', Trigger: 'Transcoder timeout or worker error', Recovery: 'Retry up to maxAttempts, then SwapOrchestrator' },
{ Failure: 'Verification mismatch', Trigger: 'Fast verification rejects returned segment', Recovery: 'Immediate SwapOrchestrator' },
{ Failure: 'Reserve drained', Trigger: 'Gateway deposit or reserve depleted mid-session', Recovery: 'Session ends; Gateway tops up TicketBroker before resuming' },
{ Failure: 'Redemption tx stuck', Trigger: 'On-chain tx exceeds txTimeout', Recovery: 'Replace tx with higher gas; metric flags redemption errors' },
]}
/>

## Related Pages

<Columns cols={2}>
  <Card title={<CustomCardTitle icon="circle-nodes" title="Network Design" />} href="/v2/about/network/design" horizontal arrow>
    Purpose, properties, actors.
  </Card>

  <Card title={<CustomCardTitle icon="diagram-project" title="Network Architecture" />} href="/v2/about/network/architecture" horizontal arrow>
    Fleet structure and surfaces.
  </Card>

  <Card title={<CustomCardTitle icon="plug" title="Network Interfaces" />} href="/v2/about/network/interfaces" horizontal arrow>
    Reachable surfaces and protocols.
  </Card>

  <Card title={<CustomCardTitle icon="microchip" title="Run an Orchestrator" />} href="/v2/orchestrators/portal" horizontal arrow>
    Operator-side pipeline configuration.
  </Card>
</Columns>
