> ## Documentation Index
> Fetch the complete documentation index at: https://na-36-mintlify-aebde2c5.mintlify.site/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', className = '', ...rest}) => {
  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,
    ...style
  }} {...rest}>
      {text}
      {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' }} />

## Why Pipelines Matter

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' }} />

## Three 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' }} />

## Pipeline 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' }} />

## 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' }} />

## Custom Pipelines (BYOC)

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.

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

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

{  /* ---
title: Livepeer Job Pipelines
sidebarTitle: Job Pipelines
description: >-
How a job moves through the Livepeer Network from submission to settlement, and how the lifecycle differs across live
video, file transcode, and AI inference workloads.
lifecycleStage: discover
complexity: intermediate
pageType: concept
keywords:
- livepeer
- about
- livepeer network
- job pipelines
- job lifecycle
- state machine
- probabilistic micropayments
'og:image': /snippets/assets/media/og-images/en/about.png
'og:image:alt': Livepeer Docs social preview image
'og:image:type': image/png
'og:image:width': 1200
'og:image:height': 630
audience: general
lastVerified: 2026-05-04T00:00:00.000Z
purpose: concept
---
import { DynamicTable } from "/snippets/components/displays/tables/Tables.jsx"
import { ScrollableDiagram } from "/snippets/components/displays/diagrams/ScrollableDiagram.jsx"

A Livepeer job is a unit of media compute submitted by a Gateway and executed by an Orchestrator. Because every workload – live transcode, file transcode, AI inference – is broken into segments, the lifecycle is modelled per-segment rather than per-stream. Settlement runs continuously off-chain via probabilistic tickets and periodically on-chain via reward calls.

This page covers the shape of a job, the shared state machine, the per-workload pipeline differences, and the metrics and failure modes that operators observe in production. For how jobs are matched and priced, see [Marketplace](./marketplace-model). For the node software that runs them, see [Architecture](./architecture).

## Shape of a job

Every Livepeer job moves through four phases. Two are off-chain and continuous; two touch the protocol.

<DynamicTable
headerList={["Phase", "Where", "What happens", "Protocol touchpoint"]}
itemsList={[
  { "Phase": "Submit", "Where": "Off-chain", "What happens": "Client posts a stream, file, or inference request to a Gateway", "Protocol touchpoint": "Gateway deposit/reserve in TicketBroker" },
  { "Phase": "Route", "Where": "Off-chain", "What happens": "Gateway discovers Orchestrators, accepts a price, opens a session", "Protocol touchpoint": "ServiceRegistry advertises Orchestrator URI" },
  { "Phase": "Compute", "Where": "Off-chain", "What happens": "Segments dispatched, processed by transcoder or AI worker, returned with tickets", "Protocol touchpoint": "Tickets generated against on-chain deposit" },
  { "Phase": "Settle", "Where": "On-chain", "What happens": "Winning tickets redeemed; per-round reward distributes inflation to stake", "Protocol touchpoint": "TicketBroker.redeemWinningTicket, Minter.reward" }
]}
/>

The on-chain layer never sees individual segments. It sees deposits, winning tickets, and reward calls. Everything else is peer-to-peer between Gateway and Orchestrator.

## Lifecycle state machine

The session-level state machine below tracks one job from ingest to drained reserve. Per-segment dispatch loops inside `SegmentDispatch → Executing → Returning → Verifying → Publishing → Settling`. Per-round reward accounting runs in parallel as a periodic transition out of `Publishing`.

<ScrollableDiagram title="Livepeer Job Lifecycle State Machine" maxHeight="600px">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2b9a66', 'primaryTextColor': '#E0E4E0', 'primaryBorderColor': '#2b9a66', 'lineColor': '#2b9a66', 'mainBkg': '#18794E', 'nodeBorder': '#2b9a66', 'clusterBkg': 'transparent', 'clusterBorder': '#2b9a66', 'titleColor': '#2b9a66', 'edgeLabelBackground': 'transparent', 'textColor': '#E0E4E0', 'nodeTextColor': '#E0E4E0', 'fontFamily': 'Inter, sans-serif'}}}%%
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>

## Steps in detail

<Steps>
<Step title="Ingest and segmentation">
The Gateway accepts an RTMP stream, an HTTP push, or an AI inference request and produces segments to be processed downstream.
</Step>
<Step title="Discovery and selection">
The Gateway queries advertised Orchestrators via ServiceRegistry, scores them on latency, capability, and price, and opens a session with one or more.
</Step>
<Step title="Price agreement">
Orchestrators publish a price per pixel (Wei denominated) off-chain. Pricing may auto-adjust to cover ticket-redemption gas overhead during congestion.
</Step>
<Step title="Segment dispatch and compute">
The Gateway uploads each segment with a probabilistic payment ticket. The Orchestrator runs the work locally or hands it to attached transcoder or AI-worker processes.
</Step>
<Step title="Result return and verification">
Results are returned to the Gateway. Optional fast verification compares re-encoded output. Failures trigger retries or an Orchestrator swap.
</Step>
<Step title="Continuous settlement">
Tickets accumulate off-chain. The Orchestrator submits winning tickets to the TicketBroker on Arbitrum, redeeming them for ETH from the Gateway's deposit.
</Step>
<Step title="Periodic reward accounting">
Each round, an active Orchestrator calls `reward()` on Arbitrum. Newly minted LPT is distributed across the Orchestrator and its Delegators by stake share.
</Step>
</Steps>

## Pipelines by workload

The state machine is shared. The pipeline details – ingest mode, segment cadence, payment style, latency budget – vary by workload.

<DynamicTable
headerList={["Pipeline", "Ingest", "Segment cadence", "Compute path", "Latency budget", "Payment style"]}
itemsList={[
  { "Pipeline": "Live transcode", "Ingest": "RTMP or WHIP push to Gateway", "Segment cadence": "2-6 second segments, continuous", "Compute path": "Orchestrator → FFmpeg transcoder (GPU)", "Latency budget": "Sub-second to a few seconds", "Payment style": "ETH ticket per segment, periodic on-chain redemption" },
  { "Pipeline": "File transcode", "Ingest": "HTTP upload of full asset", "Segment cadence": "Fixed-size chunks, parallel dispatch", "Compute path": "Orchestrator → FFmpeg transcoder (GPU)", "Latency budget": "Throughput-oriented, not real-time", "Payment style": "Batch credits or ETH tickets, single redemption" },
  { "Pipeline": "AI inference", "Ingest": "Frame stream, image, or prompt to Gateway", "Segment cadence": "Per-frame or per-job", "Compute path": "Orchestrator → AI worker (Docker, multi-stage Cascade pipelines)", "Latency budget": "Real-time for live use cases (Daydream, ComfyStream)", "Payment style": "ETH ticket per inference unit, periodic redemption" }
]}
/>

Cascade is Livepeer's coordination layer for multi-stage AI pipelines, where one inference request flows through a sequence of models (for example text encoder → image generator → upscaler) before the final result returns to the Gateway.

## Events and metrics

Each transition in the state machine is observable. The metrics below are the primary signals operators track in production.

<DynamicTable
headerList={["Event", "Metric", "Transition", "Notes"]}
itemsList={[
  { "Event": "Stream starts", "Metric": "livepeer_stream_started_total", "Transition": "Idle → Ingesting", "Notes": "Defined in node metrics surface." },
  { "Event": "Discovery fails", "Metric": "livepeer_discovery_errors_total", "Transition": "Discovering → Failed", "Notes": "Selection algorithm is implementation-defined." },
  { "Event": "Segment transcode fails", "Metric": "livepeer_segment_transcode_failed_total", "Transition": "Executing → Retry", "Notes": "Retry budget controlled by maxAttempts (default 3)." },
  { "Event": "Orchestrator swap", "Metric": "livepeer_orchestrator_swaps", "Transition": "Retry or Verifying → SwapOrchestrator", "Notes": "Swap policy is observable via metrics, not formally specified." },
  { "Event": "Payment sent", "Metric": "livepeer_tickets_sent, livepeer_ticket_value_sent", "Transition": "Publishing → Settling", "Notes": "Deposit and reserve surfaced per Gateway." },
  { "Event": "Reserve depleted", "Metric": "livepeer_gateway_reserve, livepeer_gateway_deposit", "Transition": "Settling → Drained", "Notes": "Operator-specific sizing of deposit vs reserve." },
  { "Event": "Ticket redemption error", "Metric": "livepeer_ticket_redemption_errors", "Transition": "Settling (degraded)", "Notes": "Affects realised Orchestrator revenue." },
  { "Event": "On-chain tx timeout", "Metric": "txTimeout (default 5 min)", "Transition": "Settling or RoundReward (retry or replace)", "Notes": "Replacement knobs exposed via CLI options." },
  { "Event": "Per-round reward", "Metric": "Orchestrator reward service enabled", "Transition": "Publishing ↔ RoundReward", "Notes": "Auto reward calls per round on Arbitrum by default." }
]}
monospaceColumns={[1]}
/>

## Failure modes and recovery

<DynamicTable
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" }
]}
/>

## See also

- [Marketplace](./marketplace-model) – how supply and demand match, pricing, routing
- [Architecture](./architecture) – node types, go-livepeer components, worker layer
- [Blockchain contracts](../protocol/blockchain-contracts) – TicketBroker, BondingManager, Minter addresses
- [Actors](./actors) – Gateway, Orchestrator, Delegator roles

## References

- [go-livepeer source](https://github.com/livepeer/go-livepeer)
- [TicketBroker contract](https://github.com/livepeer/protocol/tree/master/contracts/pm)
- [Cascade and real-time AI pipelines](https://blog.livepeer.org/real-time-ai-comfyui)
- [Forum: LIPs](https://forum.livepeer.org/c/lips/) */}
