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

# Complex Agreement

> Use a richer complete agreement JSON example to inspect a realistic lifecycle with more states, event types, metadata, and branching behavior.

export const AgreementStateMachineExample = ({agreement = {}, title = 'State Machine Map'}) => {
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const [isDragging, setIsDragging] = useState(false);
  const [nodeOffsets, setNodeOffsets] = useState({});
  const dragRef = useRef(null);
  const truncate = (value, max = 36) => {
    const text = String(value || '');
    if (text.length <= max) return text;
    return `${text.slice(0, Math.max(10, Math.floor(max / 2)))}...${text.slice(-8)}`;
  };
  const execution = agreement?.execution || ({});
  const rawStates = execution.states || ({});
  const states = Array.isArray(rawStates) ? Object.fromEntries(rawStates.map(state => {
    if (typeof state === 'string') return [state, {
      name: ''
    }];
    if (!state?.id) return null;
    return [String(state.id), state];
  }).filter(Boolean)) : rawStates;
  const transitions = Array.isArray(execution.transitions) ? execution.transitions : [];
  const stateIds = Object.keys(states);
  const initialState = execution?.initialize?.initialState || stateIds[0] || null;
  const inputs = execution.inputs || ({});
  const brand = 'var(--shodai-diagram-accent)';
  const transitionInputIds = transition => {
    const directInput = transition?.input ? [transition.input] : [];
    const conditionInputs = Array.isArray(transition?.conditions) ? transition.conditions.flatMap(condition => {
      if (condition?.input) return [condition.input];
      if (Array.isArray(condition?.inputs)) return condition.inputs;
      return [];
    }) : [];
    return Array.from(new Set([...directInput, ...conditionInputs].filter(Boolean).map(String)));
  };
  const inputDisplayName = inputId => {
    const input = inputs?.[inputId] || ({});
    return input.displayName || input.name || input.id || inputId;
  };
  const describeTransition = edge => {
    if (!edge) return '';
    const label = edge.inputIds.length > 0 ? ` using ${edge.inputIds.map(inputDisplayName).join(', ')}` : '';
    return `${edge.from} moves to ${edge.to}${label}.`;
  };
  const zoomIn = () => setZoom(value => Math.min(2.4, Number((value + 0.2).toFixed(2))));
  const zoomOut = () => setZoom(value => Math.max(0.7, Number((value - 0.2).toFixed(2))));
  const resetView = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setNodeOffsets({});
  };
  const nodeWidth = 260;
  const nodeHeight = 66;
  const gapX = 128;
  const gapY = 96;
  const positions = new Map();
  const levels = new Map();
  const outgoing = new Map();
  const transitionOrder = new Map();
  transitions.forEach((transition, index) => {
    if (!transition?.from || !transition?.to) return;
    const from = String(transition.from);
    const to = String(transition.to);
    if (!outgoing.has(from)) outgoing.set(from, []);
    outgoing.get(from).push(to);
    if (!transitionOrder.has(from)) transitionOrder.set(from, index);
    if (!transitionOrder.has(to)) transitionOrder.set(to, index);
  });
  for (const [from, targets] of outgoing.entries()) {
    outgoing.set(from, Array.from(new Set(targets)));
  }
  const canReach = (start, target) => {
    if (!start || !target) return false;
    const seen = new Set();
    const queue = [start];
    while (queue.length > 0) {
      const current = queue.shift();
      if (!current || seen.has(current)) continue;
      if (current === target) return true;
      seen.add(current);
      (outgoing.get(current) || []).forEach(next => {
        if (!seen.has(next)) queue.push(next);
      });
    }
    return false;
  };
  if (initialState) levels.set(initialState, 0);
  for (let pass = 0; pass < Math.max(1, stateIds.length * 2); pass += 1) {
    let changed = false;
    for (const transition of transitions) {
      if (!transition?.from || !transition?.to) continue;
      const from = String(transition.from);
      const to = String(transition.to);
      const fromLevel = levels.get(from);
      if (fromLevel == null) continue;
      const nextLevel = fromLevel + 1;
      const currentLevel = levels.get(to);
      const closesCycle = currentLevel != null && currentLevel <= fromLevel && canReach(to, from);
      if (currentLevel == null || nextLevel > currentLevel && !closesCycle) {
        levels.set(to, nextLevel);
        changed = true;
      }
    }
    if (!changed) break;
  }
  const maxRankedLevel = Math.max(0, ...Array.from(levels.values()));
  stateIds.filter(id => levels.get(id) == null).sort((a, b) => a.localeCompare(b)).forEach((id, index) => levels.set(id, maxRankedLevel + 1 + index));
  const groupedByLevel = new Map();
  for (const id of stateIds) {
    const level = levels.get(id) || 0;
    if (!groupedByLevel.has(level)) groupedByLevel.set(level, []);
    groupedByLevel.get(level).push(id);
  }
  const levelNumbers = Array.from(groupedByLevel.keys()).sort((a, b) => a - b);
  const rowGap = nodeWidth + gapX;
  for (const level of levelNumbers) {
    const ids = groupedByLevel.get(level) || [];
    const idsWithIdeal = ids.map(id => {
      const incomingSources = transitions.filter(transition => String(transition?.to || '') === id && positions.has(String(transition?.from || '')) && (levels.get(String(transition?.from || '')) || 0) < level).map(transition => positions.get(String(transition.from)).x);
      const idealX = incomingSources.length > 0 ? incomingSources.reduce((sum, x) => sum + x, 0) / incomingSources.length : 0;
      return {
        id,
        idealX,
        order: transitionOrder.get(id) ?? stateIds.findIndex(stateId => stateId === id)
      };
    });
    idsWithIdeal.sort((a, b) => {
      if (a.idealX !== b.idealX) return a.idealX - b.idealX;
      return a.order - b.order;
    });
    const centerX = idsWithIdeal.length > 0 ? idsWithIdeal.reduce((sum, item) => sum + item.idealX, 0) / idsWithIdeal.length : 0;
    const startX = centerX - (idsWithIdeal.length - 1) * rowGap / 2;
    idsWithIdeal.forEach((item, index) => {
      positions.set(item.id, {
        x: startX + index * rowGap,
        y: level * (nodeHeight + gapY)
      });
    });
  }
  const baseRawNodes = stateIds.map(id => ({
    id,
    name: states[id]?.name || '',
    x: positions.get(id)?.x || 0,
    y: positions.get(id)?.y || 0,
    width: nodeWidth,
    height: nodeHeight
  }));
  const rawNodes = baseRawNodes.map(node => {
    const offset = nodeOffsets[node.id] || ({
      x: 0,
      y: 0
    });
    return {
      ...node,
      x: node.x + offset.x,
      y: node.y + offset.y
    };
  });
  const baseEdges = transitions.filter(transition => transition?.from && transition?.to).map((transition, index) => {
    const conditions = Array.isArray(transition?.conditions) ? transition.conditions : [];
    const condition = conditions.find(item => item?.input) || conditions[0];
    return {
      id: `${transition.from}-${transition.to}-${index}`,
      from: String(transition.from),
      to: String(transition.to),
      inputIds: transitionInputIds(transition),
      label: transitionInputIds(transition).join(', ') || (condition?.input ? String(condition.input) : '')
    };
  });
  const pairCounts = new Map();
  for (const edge of baseEdges) {
    const key = `${edge.from}->${edge.to}`;
    pairCounts.set(key, (pairCounts.get(key) || 0) + 1);
  }
  const pairSeen = new Map();
  const returnLaneBySource = new Map();
  const sideLaneByTarget = new Map();
  const rawEdges = baseEdges.map(edge => {
    const key = `${edge.from}->${edge.to}`;
    const pairIndex = pairSeen.get(key) || 0;
    pairSeen.set(key, pairIndex + 1);
    const fromLevel = levels.get(edge.from) || 0;
    const toLevel = levels.get(edge.to) || 0;
    const returnEdge = edge.from === edge.to || toLevel <= fromLevel;
    const sideEdge = !returnEdge && toLevel - fromLevel > 1;
    const returnLane = returnEdge ? returnLaneBySource.get(edge.from) || 0 : 0;
    const sideLane = sideEdge ? sideLaneByTarget.get(edge.to) || 0 : 0;
    if (returnEdge) returnLaneBySource.set(edge.from, returnLane + 1);
    if (sideEdge) sideLaneByTarget.set(edge.to, sideLane + 1);
    return {
      ...edge,
      pairCount: pairCounts.get(key) || 1,
      pairIndex,
      returnEdge,
      returnLane,
      sideEdge,
      sideLane
    };
  });
  const rawNodesById = new Map(rawNodes.map(node => [node.id, node]));
  const baseRawNodesById = new Map(baseRawNodes.map(node => [node.id, node]));
  const edgeLabelText = edge => String(edge.label || '');
  const edgeLabelWidth = edge => Math.max(76, edgeLabelText(edge).length * 8.5 + 20);
  const returnControlX = (edge, source, target) => Math.min(source.x, target.x) - 150 - edge.returnLane * 54;
  const sideControlX = (edge, source, target) => Math.max(source.x + source.width, target.x + target.width) + 230 + edge.sideLane * 66;
  const minReturnEdgeX = Math.min(0, ...rawEdges.filter(edge => edge.returnEdge).map(edge => {
    const source = baseRawNodesById.get(edge.from);
    const target = baseRawNodesById.get(edge.to);
    if (!source || !target) return 0;
    return returnControlX(edge, source, target) + 28 - edgeLabelWidth(edge) / 2 - 16;
  }));
  const maxSideEdgeX = Math.max(nodeWidth, ...rawEdges.filter(edge => edge.sideEdge).map(edge => {
    const source = baseRawNodesById.get(edge.from);
    const target = baseRawNodesById.get(edge.to);
    if (!source || !target) return nodeWidth;
    return sideControlX(edge, source, target) + 16 + edgeLabelWidth(edge) / 2 + 16;
  }));
  const nodeDragSlack = 80;
  const minX = Math.min(0, minReturnEdgeX, ...baseRawNodes.map(node => node.x)) - nodeDragSlack;
  const minY = Math.min(0, ...baseRawNodes.map(node => node.y)) - nodeDragSlack;
  const maxX = Math.max(nodeWidth, maxSideEdgeX, ...baseRawNodes.map(node => node.x + node.width)) + nodeDragSlack;
  const maxY = Math.max(nodeHeight, ...baseRawNodes.map(node => node.y + node.height)) + nodeDragSlack;
  const paddingX = 24;
  const paddingY = 72;
  const offsetX = paddingX - minX;
  const offsetY = paddingY - minY;
  const diagramWidth = Math.max(720, maxX - minX + paddingX * 2);
  const diagramHeight = Math.max(520, maxY - minY + paddingY * 2);
  const nodes = rawNodes.map(node => ({
    ...node,
    x: node.x + offsetX,
    y: node.y + offsetY
  }));
  const nodesById = new Map(nodes.map(node => [node.id, node]));
  const diagramTitle = agreement?.metadata?.name || 'Agreement state machine';
  const svgId = String(agreement?.metadata?.templateId || agreement?.metadata?.id || 'agreement').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 48);
  const gridId = `${svgId}-grid`;
  const arrowId = `${svgId}-arrow`;
  const viewBoxWidth = diagramWidth / zoom;
  const viewBoxHeight = diagramHeight / zoom;
  const maxViewBoxX = Math.max(0, diagramWidth - viewBoxWidth);
  const maxViewBoxY = Math.max(0, diagramHeight - viewBoxHeight);
  const centerViewBoxX = Math.max(0, (diagramWidth - viewBoxWidth) / 2);
  const centerViewBoxY = Math.max(0, (diagramHeight - viewBoxHeight) / 4);
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
  const panSlack = 120 / zoom;
  const minPanX = -centerViewBoxX - panSlack;
  const maxPanX = maxViewBoxX - centerViewBoxX + panSlack;
  const minPanY = -centerViewBoxY - panSlack;
  const maxPanY = maxViewBoxY - centerViewBoxY + panSlack;
  const viewBoxX = centerViewBoxX + clamp(pan.x, minPanX, maxPanX);
  const viewBoxY = centerViewBoxY + clamp(pan.y, minPanY, maxPanY);
  const edgeOffset = (edge, source, target) => {
    const parallelOffset = edge.pairCount > 1 ? (edge.pairIndex - (edge.pairCount - 1) / 2) * 42 : 0;
    const returnOffset = target.y <= source.y ? source.x > target.x ? 120 : -120 : 0;
    return parallelOffset + returnOffset;
  };
  const edgePath = (edge, source, target) => {
    if (edge.returnEdge) {
      const startX = source.x;
      const startY = source.y + source.height / 2;
      const endX = target.x;
      const endY = target.y + target.height / 2;
      const controlX = returnControlX(edge, source, target);
      return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
    }
    if (edge.sideEdge) {
      const startX = source.x + source.width;
      const startY = source.y + source.height / 2;
      const endX = target.x + target.width;
      const endY = target.y + target.height / 2;
      const controlX = sideControlX(edge, source, target);
      return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
    }
    const offset = edgeOffset(edge, source, target);
    const startX = source.x + source.width / 2 + offset;
    const startY = source.y + source.height;
    const endX = target.x + target.width / 2 + offset;
    const endY = target.y;
    const midY = (startY + endY) / 2;
    return `M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`;
  };
  const edgeLabel = (edge, source, target) => {
    if (edge.returnEdge) {
      const sourceY = source.y + source.height / 2;
      const targetY = target.y + target.height / 2;
      return {
        x: returnControlX(edge, source, target) + 28,
        y: (sourceY + targetY) / 2 - 9 + edge.returnLane * 18
      };
    }
    if (edge.sideEdge) {
      const targetY = target.y + target.height / 2;
      return {
        x: sideControlX(edge, source, target) + 16,
        y: targetY - 42 - edge.sideLane * 18
      };
    }
    const offset = edgeOffset(edge, source, target);
    const sourceY = source.y + source.height;
    const targetY = target.y;
    return {
      x: (source.x + source.width / 2 + target.x + target.width / 2) / 2 + offset,
      y: (sourceY + targetY) / 2 - 9
    };
  };
  const startDrag = ({id, type = 'pan', nodeId, clientX, clientY, width, height}) => {
    dragRef.current = {
      id,
      startClientX: clientX,
      startClientY: clientY,
      startPan: pan,
      startNodeOffset: nodeId ? nodeOffsets[nodeId] || ({
        x: 0,
        y: 0
      }) : null,
      nodeId,
      type,
      width: width || 1,
      height: height || 1
    };
    setIsDragging(true);
  };
  const moveDrag = ({id, clientX, clientY}) => {
    const drag = dragRef.current;
    if (!drag || drag.id !== id) return;
    const deltaX = (clientX - drag.startClientX) / drag.width * viewBoxWidth;
    const deltaY = (clientY - drag.startClientY) / drag.height * viewBoxHeight;
    if (drag.type === 'node' && drag.nodeId) {
      const baseNode = baseRawNodesById.get(drag.nodeId);
      const nextOffset = {
        x: drag.startNodeOffset.x + deltaX,
        y: drag.startNodeOffset.y + deltaY
      };
      if (baseNode) {
        nextOffset.x = clamp(baseNode.x + nextOffset.x, minX, maxX - baseNode.width) - baseNode.x;
        nextOffset.y = clamp(baseNode.y + nextOffset.y, minY, maxY - baseNode.height) - baseNode.y;
      }
      setNodeOffsets(current => ({
        ...current,
        [drag.nodeId]: nextOffset
      }));
      return;
    }
    setPan({
      x: clamp(drag.startPan.x - deltaX, minPanX, maxPanX),
      y: clamp(drag.startPan.y - deltaY, minPanY, maxPanY)
    });
  };
  const stopDrag = id => {
    if (dragRef.current?.id === id) {
      dragRef.current = null;
      setIsDragging(false);
    }
  };
  const handlePointerDown = event => {
    event.currentTarget.setPointerCapture(event.pointerId);
    startDrag({
      id: `pointer-${event.pointerId}`,
      type: 'pan',
      clientX: event.clientX,
      clientY: event.clientY,
      width: event.currentTarget.clientWidth,
      height: event.currentTarget.clientHeight
    });
  };
  const handleNodePointerDown = (event, nodeId) => {
    if (event.button !== 0 || dragRef.current) return;
    const canvas = event.currentTarget.closest('[data-state-machine-canvas]');
    event.preventDefault();
    event.stopPropagation();
    event.currentTarget.setPointerCapture(event.pointerId);
    startDrag({
      id: `pointer-${event.pointerId}`,
      type: 'node',
      nodeId,
      clientX: event.clientX,
      clientY: event.clientY,
      width: canvas?.clientWidth,
      height: canvas?.clientHeight
    });
  };
  const handlePointerMove = event => {
    moveDrag({
      id: `pointer-${event.pointerId}`,
      clientX: event.clientX,
      clientY: event.clientY
    });
  };
  const handlePointerUp = event => {
    stopDrag(`pointer-${event.pointerId}`);
  };
  const handleMouseDown = event => {
    if (event.button !== 0 || dragRef.current) return;
    event.preventDefault();
    startDrag({
      id: 'mouse',
      type: 'pan',
      clientX: event.clientX,
      clientY: event.clientY,
      width: event.currentTarget.clientWidth,
      height: event.currentTarget.clientHeight
    });
  };
  const handleMouseMove = event => {
    const activeDragId = dragRef.current?.id || 'mouse';
    moveDrag({
      id: activeDragId,
      clientX: event.clientX,
      clientY: event.clientY
    });
  };
  const handleMouseUp = () => {
    if (dragRef.current) {
      stopDrag(dragRef.current.id);
    }
  };
  const zoomAtPoint = (event, nextZoom) => {
    const bounds = event.currentTarget.getBoundingClientRect();
    const pointerX = (event.clientX - bounds.left) / Math.max(1, bounds.width);
    const pointerY = (event.clientY - bounds.top) / Math.max(1, bounds.height);
    const currentWorldX = viewBoxX + pointerX * viewBoxWidth;
    const currentWorldY = viewBoxY + pointerY * viewBoxHeight;
    const nextViewBoxWidth = diagramWidth / nextZoom;
    const nextViewBoxHeight = diagramHeight / nextZoom;
    const nextCenterViewBoxX = Math.max(0, (diagramWidth - nextViewBoxWidth) / 2);
    const nextCenterViewBoxY = Math.max(0, (diagramHeight - nextViewBoxHeight) / 4);
    const nextMaxViewBoxX = Math.max(0, diagramWidth - nextViewBoxWidth);
    const nextMaxViewBoxY = Math.max(0, diagramHeight - nextViewBoxHeight);
    const nextPanSlack = 120 / nextZoom;
    const nextViewBoxX = currentWorldX - pointerX * nextViewBoxWidth;
    const nextViewBoxY = currentWorldY - pointerY * nextViewBoxHeight;
    setZoom(nextZoom);
    setPan({
      x: clamp(nextViewBoxX - nextCenterViewBoxX, -nextCenterViewBoxX - nextPanSlack, nextMaxViewBoxX - nextCenterViewBoxX + nextPanSlack),
      y: clamp(nextViewBoxY - nextCenterViewBoxY, -nextCenterViewBoxY - nextPanSlack, nextMaxViewBoxY - nextCenterViewBoxY + nextPanSlack)
    });
  };
  const handleWheel = event => {
    if (!event.ctrlKey && !event.metaKey) return;
    event.preventDefault();
    event.stopPropagation();
    const scale = event.deltaY < 0 ? 1.12 : 1 / 1.12;
    const nextZoom = clamp(Number((zoom * scale).toFixed(3)), 0.7, 2.4);
    zoomAtPoint(event, nextZoom);
  };
  return <div className="shodai-state-machine-frame not-prose my-8 overflow-hidden rounded-lg border shadow-sm">
      <section className="shodai-state-machine-shell flex min-h-[460px] flex-col lg:min-h-[600px]">
        <div className="shodai-state-machine-header flex items-center justify-between border-b px-4 py-3">
          <div>
            <div className="shodai-state-machine-title text-base font-semibold">
              {title}
            </div>
            <div className="shodai-state-machine-meta text-sm">
              Derived from execution.states and execution.transitions
            </div>
          </div>
          <span className="shodai-state-machine-count rounded border px-2 py-1 font-mono text-sm">
            {stateIds.length} states
          </span>
        </div>

        <div className="shodai-state-machine-canvas-bg relative h-full min-h-[420px] overflow-hidden lg:min-h-[560px]" onWheelCapture={handleWheel}>
          <div className="absolute inset-0" data-state-machine-canvas onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerUp} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp}>
            <svg role="img" aria-label={`${diagramTitle} state machine diagram`} viewBox={`${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`} preserveAspectRatio="xMidYMin meet" className={`h-full min-h-[420px] w-full touch-none select-none lg:min-h-[520px] ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`} draggable={false}>
              <defs>
                <pattern id={gridId} width="18" height="18" patternUnits="userSpaceOnUse">
                  <circle cx="1" cy="1" r="1" className="shodai-state-machine-grid-dot" />
                </pattern>
                <marker id={arrowId} markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" markerUnits="strokeWidth" className="shodai-state-machine-connector">
                  <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
                </marker>
              </defs>

              <rect width={diagramWidth} height={diagramHeight} fill={`url(#${gridId})`} className="shodai-state-machine-connector" />

              {rawEdges.map(edge => {
    const source = nodesById.get(edge.from);
    const target = nodesById.get(edge.to);
    if (!source || !target) return null;
    const label = edgeLabel(edge, source, target);
    return <g key={edge.id} aria-label={`Transition from ${edge.from} to ${edge.to}`}>
                    <title>{describeTransition(edge)}</title>
                    <path d={edgePath(edge, source, target)} fill="none" stroke="currentColor" strokeWidth="1.5" markerEnd={`url(#${arrowId})`} className="shodai-state-machine-connector" />
                    {edge.label ? <g>
                        {(() => {
      const visibleLabel = edgeLabelText(edge);
      const labelWidth = edgeLabelWidth(edge);
      return <>
                              <rect x={label.x - labelWidth / 2} y={label.y - 13} width={labelWidth} height="22" rx="11" className="shodai-state-machine-label-bg" />
                              <text x={label.x} y={label.y + 4} textAnchor="middle" className="shodai-state-machine-label-text font-mono text-[14px] font-semibold">
                                {visibleLabel}
                              </text>
                            </>;
    })()}
                      </g> : null}
                  </g>;
  })}

              {nodes.map(node => {
    const start = initialState === node.id;
    return <g key={node.id} aria-label={`State ${node.id}`} className="cursor-move" onPointerDown={event => handleNodePointerDown(event, node.id)} onMouseDown={event => event.stopPropagation()}>
                    <title>
                      {states[node.id]?.description || states[node.id]?.name || node.id}
                    </title>
                    {start ? <rect x={node.x} y={node.y} width={node.width} height={node.height} rx="10" stroke={brand} strokeWidth="2" filter="drop-shadow(0 1px 2px rgba(0,0,0,0.06))" className="shodai-state-machine-start-node" /> : <rect x={node.x} y={node.y} width={node.width} height={node.height} rx="10" stroke="currentColor" strokeWidth="1" filter="drop-shadow(0 1px 2px rgba(0,0,0,0.06))" className="shodai-state-machine-node" />}
                    <text x={node.x + 16} y={node.y + 26} className="shodai-state-machine-node-id font-mono text-[16px] font-semibold">
                      {truncate(node.id, 30)}
                    </text>
                    <text x={node.x + 16} y={node.y + 50} className="shodai-state-machine-node-name text-[14px]">
                      {truncate(node.name || (start ? 'Initial state' : ''), 34)}
                    </text>
                  </g>;
  })}
            </svg>
          </div>

          <div className="shodai-state-machine-controls absolute bottom-5 left-5 flex flex-col overflow-hidden rounded border shadow-sm">
            <button type="button" aria-label="Zoom in on state machine" title="Zoom in" className="shodai-state-machine-control flex h-9 w-9 items-center justify-center border-b text-xl leading-none transition disabled:cursor-not-allowed disabled:opacity-40" disabled={zoom >= 2.4} onClick={zoomIn}>
              +
            </button>
            <button type="button" aria-label="Zoom out of state machine" title="Zoom out" className="shodai-state-machine-control flex h-9 w-9 items-center justify-center border-b text-xl leading-none transition disabled:cursor-not-allowed disabled:opacity-40" disabled={zoom <= 0.7} onClick={zoomOut}>
              -
            </button>
            <button type="button" aria-label="Reset state machine view" title="Reset view" className="shodai-state-machine-control flex h-9 w-9 items-center justify-center text-[10px] font-semibold uppercase tracking-normal transition" onClick={resetView}>
              fit
            </button>
          </div>
        </div>
      </section>
    </div>;
};

For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt).

The complex agreement is the richer complete agreement JSON artifact in these docs. Use it to inspect or adapt a realistic lifecycle with participant roles, operational events, and branching transitions before [validating](/workflow/validate-agreement-structure) and [deploying](/workflow/deploy-an-agreement) with the SDK.

<Note>
  This example models the agreement state and submitted attestations around payment-related obligations. Payment execution can be composed externally through payment rails, escrow contracts, application-layer integrations, or modular actions.
</Note>

## When to use this example

Use this example when [Simple Agreement](/examples/simple) is too small, when you want to study a richer lifecycle design, or when you want to see how metadata makes an agreement more useful in frontends and agentic interfaces.

It is the better example to adapt when your workflow has multiple states, participant roles, event types, and branches.

<Card title="Run this example end to end" icon="route" href="/examples/end-to-end-workflow">
  Use this service retainer agreement to validate, preflight, deploy, submit signed inputs, read state, and inspect input history.
</Card>

## What to notice before the lifecycle and JSON

Before reading the lifecycle diagram and JSON, notice:

1. participant wallet variables that define real roles in the lifecycle
2. business terms and descriptive metadata on variables
3. rendered content that explains how the retainer works
4. lifecycle states for payment, work, invoice review, termination, and inactivity
5. input events for payment, invoicing, approval, rejection, dispute, and termination
6. issuer rules that sometimes allow one role and sometimes allow multiple roles
7. transitions that branch based on which business event was submitted

## Agreement lifecycle

This section renders the complex agreement state machine before the complete deployable JSON artifact. The state machine has 7 states, 14 inputs, and 14 transitions. It starts at AWAITING\_PAYMENT, loops through WORK\_IN\_PROGRESS invoice review paths, branches for top-up requests and termination, and ends at INACTIVE after final invoice settlement.

The diagram below shows the states and transitions defined by this agreement's `execution` object. Use it to understand the recurring retainer workflow before reviewing the full JSON artifact.

<AgreementStateMachineExample
  title="Service retainer execution path"
  agreement={{
  "metadata": {
    "id": "did:example:service-retainer-manual-balance",
    "templateId": "did:template:service-retainer-manual-balance-v0-1",
    "version": "0.1.0",
    "createdAt": "2026-04-03T00:00:00Z",
    "name": "Service Retainer - Manual Balance",
    "author": "CNS Labs",
    "description": "Retainer template with manually entered balance tracking and invoice-driven replenishment."
  },
  "execution": {
    "states": {
      "AWAITING_PAYMENT": {
        "name": "Awaiting Payment",
        "description": "Waiting for the initial payment to fill the retainer."
      },
      "WORK_IN_PROGRESS": {
        "name": "Work in Progress",
        "description": "Services are active. The service provider will submit the next invoice during the working cycle, with a replenishment request if the balance would fall below the retainer floor."
      },
      "INVOICE_SUBMITTED": {
        "name": "Invoice Submitted",
        "description": "The latest invoice does not require additional payment because the remaining retainer stays above the floor."
      },
      "INVOICE_SUBMITTED_WITH_TOPUP": {
        "name": "Invoice Submitted with Topup",
        "description": "The latest invoice would reduce the retainer below the floor, so a replenishment payment is required."
      },
      "PENDING_FINAL_INVOICE": {
        "name": "Pending Final Invoice",
        "description": "The termination process has begun. The service provider will now submit a final invoice to settle the retainer."
      },
      "FINAL_INVOICE_REVIEW": {
        "name": "Final Invoice Review",
        "description": "The final invoice has been submitted and is awaiting review and settlement."
      },
      "INACTIVE": {
        "name": "Inactive",
        "description": "This agreement has been terminated and is now inactive."
      }
    },
    "initialize": {
      "name": "Initialize Service Retainer Manual Balance",
      "description": "Initialize the agreement.",
      "initialState": "AWAITING_PAYMENT",
      "data": {
        "serviceProviderRepresentative": "${variables.serviceProviderRepresentative}",
        "clientRepresentative": "${variables.clientRepresentative}",
        "retainerTitle": "${variables.retainerTitle}",
        "retainerDescription": "${variables.retainerDescription}",
        "serviceProviderName": "${variables.serviceProviderName}",
        "clientName": "${variables.clientName}",
        "retainerCeiling": "${variables.retainerCeiling}",
        "retainerFloor": "${variables.retainerFloor}",
        "paymentInstructions": "${variables.paymentInstructions}"
      }
    },
    "inputs": {
      "submitInitialPaymentProof": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Payment Proof",
        "description": "Submit the proof of initial retainer topup along with any helpful comments.",
        "data": {
          "awaitingPaymentPaymentLink": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "awaitingPaymentComment": "${variables.awaitingPaymentComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "awaitingPaymentInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement.",
        "data": {
          "awaitingPaymentTerminationReason": "${variables.awaitingPaymentTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "submitInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Invoice",
        "description": "Submit this invoice without a top-up payment request if the invoice amount will not reduce the retainer below its floor.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "submitInvoiceComment": "${variables.submitInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "submitInvoiceWithTopup": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Invoice with Topup",
        "description": "Submit this invoice with the top-up payment request if the invoice amount will reduce the retainer below its floor.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "topupInvoiceComment": "${variables.topupInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "workInProgressInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "workInProgressTerminationReason": "${variables.workInProgressTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedApprove": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Approve Invoice",
        "description": "Record approval of this invoice. No payment proof is required because this invoice does not request additional payment.",
        "data": {
          "invoiceSubmittedComment": "${variables.invoiceSubmittedComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedReject": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Reject Invoice",
        "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.",
        "data": {
          "invoiceSubmittedFeedback": "${variables.invoiceSubmittedFeedback}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "invoiceSubmittedTerminationReason": "${variables.invoiceSubmittedTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceApprove": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Approve and Topup",
        "description": "Record approval of this invoice and submit proof of the replenishment payment.",
        "data": {
          "topupInvoicePaymentLink": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "topupInvoiceComment": "${variables.topupInvoiceComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceReject": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Reject Topup Invoice",
        "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.",
        "data": {
          "topupInvoiceFeedback": "${variables.topupInvoiceFeedback}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "topupInvoiceTerminationReason": "${variables.topupInvoiceTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "finalInvoiceSubmit": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Final Invoice",
        "description": "Submit the final invoice to close out this retainer. If the remaining retainer does not fully cover the final invoice, the outstanding amount will be due. If funds remain after the final invoice, the client refund will appear as a negative balance.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "finalInvoiceComment": "${variables.finalInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "disputeFinalInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Dispute Final Invoice",
        "description": "Dispute the final invoice with feedback if something is incorrect or incomplete so the service provider can resubmit it. If agreement cannot be reached, off-platform resolution may be required.",
        "data": {
          "finalInvoiceFeedback": "${variables.finalInvoiceFeedback}"
        },
        "issuer": "${variables.clientRepresentative.value}"
      },
      "settleFinalInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Settle Final Invoice",
        "description": "Submit proof that the final invoice has been settled and close this agreement.",
        "data": {
          "finalInvoicePaymentProof": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "finalTerminationComment": "${variables.finalTerminationComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      }
    },
    "transitions": [
      {
        "from": "AWAITING_PAYMENT",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInitialPaymentProof"
          }
        ]
      },
      {
        "from": "AWAITING_PAYMENT",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "awaitingPaymentInitiateTermination"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "INVOICE_SUBMITTED",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInvoice"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "INVOICE_SUBMITTED_WITH_TOPUP",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInvoiceWithTopup"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "workInProgressInitiateTermination"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedApprove"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedReject"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedInitiateTermination"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceApprove"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceReject"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceTermination"
          }
        ]
      },
      {
        "from": "PENDING_FINAL_INVOICE",
        "to": "FINAL_INVOICE_REVIEW",
        "conditions": [
          {
            "type": "isValid",
            "input": "finalInvoiceSubmit"
          }
        ]
      },
      {
        "from": "FINAL_INVOICE_REVIEW",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "disputeFinalInvoice"
          }
        ]
      },
      {
        "from": "FINAL_INVOICE_REVIEW",
        "to": "INACTIVE",
        "conditions": [
          {
            "type": "isValid",
            "input": "settleFinalInvoice"
          }
        ]
      }
    ]
  }
}}
/>

## Lifecycle phases

| Phase                            | States                                                                  | What to inspect                                                                                                             |
| -------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Initial funding                  | `AWAITING_PAYMENT`                                                      | Either party can submit initial payment proof or initiate termination before work begins.                                   |
| Active work loop                 | `WORK_IN_PROGRESS`, `INVOICE_SUBMITTED`, `INVOICE_SUBMITTED_WITH_TOPUP` | The service provider submits invoices; review inputs either return the agreement to active work or move toward termination. |
| Top-up branch                    | `INVOICE_SUBMITTED_WITH_TOPUP`                                          | Approval requires replenishment proof because the invoice would reduce the retainer below the floor.                        |
| Termination and final settlement | `PENDING_FINAL_INVOICE`, `FINAL_INVOICE_REVIEW`, `INACTIVE`             | The service provider submits a final invoice, the client can dispute it, and settlement closes the agreement.               |

## Canonical agreement JSON

Use the JSON code block under “Canonical agreement JSON” as the deployable agreement artifact. Ignore feedback examples, OpenAPI examples, and other non-agreement JSON blocks in Markdown export.

The following code block is the complete deployable agreement JSON for this example.

```json title="complex-agreement.json" theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "metadata": {
    "id": "did:example:service-retainer-manual-balance",
    "templateId": "did:template:service-retainer-manual-balance-v0-1",
    "version": "0.1.0",
    "createdAt": "2026-04-03T00:00:00Z",
    "name": "Service Retainer - Manual Balance",
    "author": "CNS Labs",
    "description": "Retainer template with manually entered balance tracking and invoice-driven replenishment."
  },
  "variables": {
    "serviceProviderRepresentative": {
      "type": "address",
      "subtype": "participant",
      "name": "Service Provider Representative",
      "helperText": "Wallet address designating the service provider representative",
      "description": "Participant address used when the service provider representative submits and reviews agreement inputs.",
      "validation": {
        "required": true
      }
    },
    "clientRepresentative": {
      "type": "address",
      "subtype": "participant",
      "name": "Client Representative",
      "helperText": "Wallet address designating the client representative",
      "description": "Participant address used when the client representative submits and reviews agreement inputs.",
      "validation": {
        "required": true
      }
    },
    "retainerTitle": {
      "type": "string",
      "name": "Retainer Title",
      "helperText": "Enter descriptive title",
      "description": "Primary identifier used for this retainer",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "retainerDescription": {
      "type": "string",
      "subtype": "longText",
      "name": "Retainer Description",
      "helperText": "Describe the retainer",
      "description": "Text that helps contextualize this agreement",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "serviceProviderName": {
      "type": "string",
      "name": "Service Provider Name",
      "helperText": "Name of the service provider",
      "description": "Brand name or individual name of the service provider to display on invoice and prose content.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "clientName": {
      "type": "string",
      "name": "Client Name",
      "helperText": "Name of the client (e.g. the customer)",
      "description": "Brand name or individual name of the client to display on invoice and prose content.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "retainerCeiling": {
      "type": "uint256",
      "name": "Retainer Ceiling",
      "helperText": "Maximum value of retainer",
      "description": "The maximum amount of value to be held in the retainer at any point in time. When a retainer topup is requested, it will fill to this amount.",
      "validation": {
        "required": true,
        "min": 0
      }
    },
    "retainerFloor": {
      "type": "uint256",
      "name": "Retainer Floor",
      "helperText": "Threshold at which retainer topup requested",
      "description": "The minimum amount expected to be held in the retainer from month to month. If an invoice would reduce the retainer below this amount, topup of the retainer should be requested.",
      "validation": {
        "required": true,
        "min": 0
      }
    },
    "paymentInstructions": {
      "type": "string",
      "subtype": "longText",
      "name": "Payment Instructions",
      "description": "Plain-text payment instructions rendered on initial funding and top-up invoice PDFs for this agreement.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "awaitingPaymentComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Additional Comments",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    },
    "awaitingPaymentTerminationReason": {
      "type": "string",
      "name": "Reason for Termination",
      "helperText": "Why is the agreement being terminated?",
      "description": "Provide context as to why the agreement is being terminated.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "submitInvoiceComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Additional Comments",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    },
    "topupInvoiceComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Additional Comments",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    },
    "workInProgressTerminationReason": {
      "type": "string",
      "name": "Reason for Termination",
      "helperText": "Why is the agreement being terminated?",
      "description": "Provide context as to why the agreement is being terminated.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "invoiceSubmittedComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Additional Comments",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    },
    "invoiceSubmittedFeedback": {
      "type": "string",
      "subtype": "longText",
      "name": "Feedback",
      "helperText": "Please provide any feedback about the invoice that may require edits or additional work.",
      "description": "An opportunity to provide comments, feedback, or request additional information.",
      "validation": {
        "required": true
      }
    },
    "invoiceSubmittedTerminationReason": {
      "type": "string",
      "name": "Reason for Termination",
      "helperText": "Why is the agreement being terminated?",
      "description": "Provide context as to why the agreement is being terminated.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "topupInvoiceFeedback": {
      "type": "string",
      "subtype": "longText",
      "name": "Feedback",
      "helperText": "Please provide any feedback about the invoice that may require edits or additional work.",
      "description": "An opportunity to provide comments, feedback, or request additional information.",
      "validation": {
        "required": true
      }
    },
    "topupInvoiceTerminationReason": {
      "type": "string",
      "name": "Reason for Termination",
      "helperText": "Why is the agreement being terminated?",
      "description": "Provide context as to why the agreement is being terminated.",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "finalInvoiceComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Additional Comments",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    },
    "finalInvoiceFeedback": {
      "type": "string",
      "subtype": "longText",
      "name": "Dispute with Feedback",
      "helperText": "Please provide any feedback about the invoice that may require edits or additional work.",
      "description": "An opportunity to provide comments, feedback, or request additional information.",
      "validation": {
        "required": true
      }
    },
    "finalTerminationComment": {
      "type": "string",
      "subtype": "longText",
      "name": "Comment",
      "helperText": "Please provide any additional comments that might provide context.",
      "description": "An opportunity to provide comments, feedback, or additional information.",
      "validation": {
        "required": false
      }
    }
  },
  "content": {
    "type": "md",
    "data": "# ${variables.retainerTitle}\n\n${variables.retainerDescription}\n\n## Participants\n\n- **Service Provider:** ${variables.serviceProviderName}\n- **Client:** ${variables.clientName}\n\n## Retainer Setup\n\n- **Balance Tracking:** Manual\n- **Retainer Floor:** ${variables.retainerFloor}\n- **Retainer Ceiling:** ${variables.retainerCeiling}\n- **Payment Instructions:** Included on payment-requesting invoices for this agreement\n\n## How This Retainer Works\n\n1. The client funds the retainer and either participant may record the initial payment proof.\n2. The service provider submits invoices against the retainer as work is completed.\n3. The service provider records the retainer balance immediately before each invoice is applied.\n4. If an invoice keeps the remaining balance at or above the floor, no additional payment is requested.\n5. If an invoice would reduce the balance below the floor, the invoice includes a replenishment request.\n6. Either participant may initiate termination. Once termination begins, the service provider submits a final invoice and the agreement closes after final settlement is recorded."
  },
  "execution": {
    "states": {
      "AWAITING_PAYMENT": {
        "name": "Awaiting Payment",
        "description": "Waiting for the initial payment to fill the retainer."
      },
      "WORK_IN_PROGRESS": {
        "name": "Work in Progress",
        "description": "Services are active. The service provider will submit the next invoice during the working cycle, with a replenishment request if the balance would fall below the retainer floor."
      },
      "INVOICE_SUBMITTED": {
        "name": "Invoice Submitted",
        "description": "The latest invoice does not require additional payment because the remaining retainer stays above the floor."
      },
      "INVOICE_SUBMITTED_WITH_TOPUP": {
        "name": "Invoice Submitted with Topup",
        "description": "The latest invoice would reduce the retainer below the floor, so a replenishment payment is required."
      },
      "PENDING_FINAL_INVOICE": {
        "name": "Pending Final Invoice",
        "description": "The termination process has begun. The service provider will now submit a final invoice to settle the retainer."
      },
      "FINAL_INVOICE_REVIEW": {
        "name": "Final Invoice Review",
        "description": "The final invoice has been submitted and is awaiting review and settlement."
      },
      "INACTIVE": {
        "name": "Inactive",
        "description": "This agreement has been terminated and is now inactive."
      }
    },
    "initialize": {
      "name": "Initialize Service Retainer Manual Balance",
      "description": "Initialize the agreement.",
      "initialState": "AWAITING_PAYMENT",
      "data": {
        "serviceProviderRepresentative": "${variables.serviceProviderRepresentative}",
        "clientRepresentative": "${variables.clientRepresentative}",
        "retainerTitle": "${variables.retainerTitle}",
        "retainerDescription": "${variables.retainerDescription}",
        "serviceProviderName": "${variables.serviceProviderName}",
        "clientName": "${variables.clientName}",
        "retainerCeiling": "${variables.retainerCeiling}",
        "retainerFloor": "${variables.retainerFloor}",
        "paymentInstructions": "${variables.paymentInstructions}"
      }
    },
    "inputs": {
      "submitInitialPaymentProof": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Payment Proof",
        "description": "Submit the proof of initial retainer topup along with any helpful comments.",
        "data": {
          "awaitingPaymentPaymentLink": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "awaitingPaymentComment": "${variables.awaitingPaymentComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "awaitingPaymentInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement.",
        "data": {
          "awaitingPaymentTerminationReason": "${variables.awaitingPaymentTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "submitInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Invoice",
        "description": "Submit this invoice without a top-up payment request if the invoice amount will not reduce the retainer below its floor.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "submitInvoiceComment": "${variables.submitInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "submitInvoiceWithTopup": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Invoice with Topup",
        "description": "Submit this invoice with the top-up payment request if the invoice amount will reduce the retainer below its floor.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "topupInvoiceComment": "${variables.topupInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "workInProgressInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "workInProgressTerminationReason": "${variables.workInProgressTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedApprove": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Approve Invoice",
        "description": "Record approval of this invoice. No payment proof is required because this invoice does not request additional payment.",
        "data": {
          "invoiceSubmittedComment": "${variables.invoiceSubmittedComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedReject": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Reject Invoice",
        "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.",
        "data": {
          "invoiceSubmittedFeedback": "${variables.invoiceSubmittedFeedback}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "invoiceSubmittedInitiateTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "invoiceSubmittedTerminationReason": "${variables.invoiceSubmittedTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceApprove": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Approve and Topup",
        "description": "Record approval of this invoice and submit proof of the replenishment payment.",
        "data": {
          "topupInvoicePaymentLink": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "topupInvoiceComment": "${variables.topupInvoiceComment}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceReject": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Reject Topup Invoice",
        "description": "Reject this invoice with feedback if something is incorrect or incomplete so that the service provider has an opportunity to resubmit.",
        "data": {
          "topupInvoiceFeedback": "${variables.topupInvoiceFeedback}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "topupInvoiceTermination": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Initiate Termination",
        "description": "Start the termination cycle for this agreement. Caution: this is not reversible.",
        "data": {
          "topupInvoiceTerminationReason": "${variables.topupInvoiceTerminationReason}"
        },
        "issuer": [
          "${variables.serviceProviderRepresentative.value}",
          "${variables.clientRepresentative.value}"
        ]
      },
      "finalInvoiceSubmit": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Submit Final Invoice",
        "description": "Submit the final invoice to close out this retainer. If the remaining retainer does not fully cover the final invoice, the outstanding amount will be due. If funds remain after the final invoice, the client refund will appear as a negative balance.",
        "data": {
          "retainerBalanceBeforeInvoice": {
            "type": "uint256",
            "name": "Retainer Balance Before Invoice",
            "helperText": "Enter the retainer balance immediately before applying this invoice",
            "description": "The retainer balance immediately before this invoice is applied.",
            "validation": {
              "required": true,
              "min": 0
            }
          },
          "invoiceLineItems": {
            "type": "string",
            "subtype": "invoice-csv",
            "name": "Invoice Line Items",
            "helperText": "Use our interactive tool to define individual line items",
            "description": "Tabular data capturing the date, description, quantity, rate, and amount of each line item in the invoice.",
            "validation": {
              "required": true
            }
          },
          "finalInvoiceComment": "${variables.finalInvoiceComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      },
      "disputeFinalInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Dispute Final Invoice",
        "description": "Dispute the final invoice with feedback if something is incorrect or incomplete so the service provider can resubmit it. If agreement cannot be reached, off-platform resolution may be required.",
        "data": {
          "finalInvoiceFeedback": "${variables.finalInvoiceFeedback}"
        },
        "issuer": "${variables.clientRepresentative.value}"
      },
      "settleFinalInvoice": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Settle Final Invoice",
        "description": "Submit proof that the final invoice has been settled and close this agreement.",
        "data": {
          "finalInvoicePaymentProof": {
            "type": "string",
            "subtype": "url",
            "name": "Link to Payment Proof",
            "helperText": "Enter transaction url",
            "description": "Block explorer link for external payment or settlement proof",
            "validation": {
              "required": true,
              "minLength": 1
            }
          },
          "finalTerminationComment": "${variables.finalTerminationComment}"
        },
        "issuer": "${variables.serviceProviderRepresentative.value}"
      }
    },
    "transitions": [
      {
        "from": "AWAITING_PAYMENT",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInitialPaymentProof"
          }
        ]
      },
      {
        "from": "AWAITING_PAYMENT",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "awaitingPaymentInitiateTermination"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "INVOICE_SUBMITTED",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInvoice"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "INVOICE_SUBMITTED_WITH_TOPUP",
        "conditions": [
          {
            "type": "isValid",
            "input": "submitInvoiceWithTopup"
          }
        ]
      },
      {
        "from": "WORK_IN_PROGRESS",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "workInProgressInitiateTermination"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedApprove"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedReject"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "invoiceSubmittedInitiateTermination"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceApprove"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "WORK_IN_PROGRESS",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceReject"
          }
        ]
      },
      {
        "from": "INVOICE_SUBMITTED_WITH_TOPUP",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "topupInvoiceTermination"
          }
        ]
      },
      {
        "from": "PENDING_FINAL_INVOICE",
        "to": "FINAL_INVOICE_REVIEW",
        "conditions": [
          {
            "type": "isValid",
            "input": "finalInvoiceSubmit"
          }
        ]
      },
      {
        "from": "FINAL_INVOICE_REVIEW",
        "to": "PENDING_FINAL_INVOICE",
        "conditions": [
          {
            "type": "isValid",
            "input": "disputeFinalInvoice"
          }
        ]
      },
      {
        "from": "FINAL_INVOICE_REVIEW",
        "to": "INACTIVE",
        "conditions": [
          {
            "type": "isValid",
            "input": "settleFinalInvoice"
          }
        ]
      }
    ]
  }
}
```

## Related pages

* [Agreement data standard](/system-architecture/data-standard)
* [Author Agreement JSON](/workflow/author-agreement-json)
* [Simple Agreement](/examples/simple)
* [Run an end-to-end agreement workflow](/examples/end-to-end-workflow)
* [Deploy an Agreement](/workflow/deploy-an-agreement)
