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

# Simple Agreement

> Use the smallest complete agreement JSON example to inspect the full document shape without much branching complexity.

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 simple agreement is the smallest complete agreement JSON artifact in these docs. Use it to inspect or adapt the full artifact shape before [validating](/workflow/validate-agreement-structure) and [deploying](/workflow/deploy-an-agreement) with the SDK.

## When to use this example

Use this example when you want a complete agreement with minimal branching, or when you need a straightforward starting point for a genuinely linear workflow.

For a broader lifecycle with more states, event types, and branching behavior, use [Complex Agreement](/examples/complex).

## What to notice before the canonical agreement JSON

Before reading the JSON, notice:

1. participant address variables marked with `subtype: "participant"`
2. rendered `content` that uses the same variables
3. a short set of lifecycle states
4. inputs that correspond to signature and acceptance events
5. transitions that move the agreement forward with minimal branching

## Agreement lifecycle

This section renders the simple agreement state machine before the complete deployable JSON artifact. The state machine is: PENDING\_PARTY\_A\_SIGNATURE --partyAData--> PENDING\_PARTY\_B\_SIGNATURE --partyBData--> PENDING\_ACCEPTANCE; PENDING\_ACCEPTANCE --accepted--> ACCEPTED; PENDING\_ACCEPTANCE --rejected--> REJECTED.

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

<AgreementStateMachineExample
  title="MOU execution path"
  agreement={{
"metadata": {
  "id": "did:example:mou-v1",
  "templateId": "did:template:mou-v1",
  "version": "1.0.0",
  "createdAt": "2024-03-20T12:00:00Z",
  "name": "Memorandum of Understanding",
  "author": "Agreements Protocol",
  "description": "Template for non-binding memorandum of understanding between two parties"
},
"execution": {
  "states": {
    "PENDING_PARTY_A_SIGNATURE": {
      "name": "Pending Signature From A",
      "description": "This state awaits until Party A supplies Party B's address, effective date, scope, duration, and their own name."
    },
    "PENDING_PARTY_B_SIGNATURE": {
      "name": "Pending Signature From B",
      "description": "This state awaits until Party B confirms their identity by supplying their name."
    },
    "PENDING_ACCEPTANCE": {
      "name": "Pending Final Acceptance",
      "description": "This state awaits Party A's final acceptance of Party B's data."
    },
    "ACCEPTED": {
      "name": "Agreement Accepted",
      "description": "The agreement has been accepted by both parties and is now in force."
    },
    "REJECTED": {
      "name": "Agreement Rejected",
      "description": "The agreement has been rejected by Party A and will not proceed."
    }
  },
  "initialize": {
    "name": "Initialize",
    "description": "Initialize the agreement",
    "initialState": "PENDING_PARTY_A_SIGNATURE",
    "data": {
      "partyAEthAddress": "${variables.partyAEthAddress}",
      "partyBEthAddress": "${variables.partyBEthAddress}"
    }
  },
  "inputs": {
    "partyAData": {
      "type": "VerifiedCredentialEIP712",
      "schema": "verified-credential-eip712.schema.json",
      "displayName": "Party A Signature",
      "description": "EIP712 signature from Party A proposing the MOU terms including scope, duration, and effective date",
      "data": {
        "partyAName": "${variables.partyAName}",
        "scope": "${variables.scope}",
        "termDuration": "${variables.termDuration}",
        "effectiveDate": "${variables.effectiveDate}"
      },
      "issuer": "${variables.partyAEthAddress.value}"
    },
    "partyBData": {
      "type": "VerifiedCredentialEIP712",
      "schema": "verified-credential-eip712.schema.json",
      "displayName": "Party B Signature",
      "description": "EIP712 signature from Party B accepting the MOU terms",
      "data": {
        "partyBName": "${variables.partyBName}",
        "partyBSignature": "${variables.partyBSignature}"
      },
      "issuer": "${variables.partyBEthAddress.value}"
    },
    "accepted": {
      "type": "VerifiedCredentialEIP712",
      "schema": "verified-credential-eip712.schema.json",
      "displayName": "Party A Accepted Party B's Data",
      "description": "EIP712 signature from Party A accepting Party B's data",
      "data": {
        "partyASignature": "${variables.partyASignature}"
      },
      "issuer": "${variables.partyAEthAddress.value}"
    },
    "rejected": {
      "type": "VerifiedCredentialEIP712",
      "schema": "verified-credential-eip712.schema.json",
      "displayName": "Party A Rejected Party B's Data",
      "description": "EIP712 signature from Party A rejecting Party B's data",
      "data": {
        "partyARejectionSignature": {
          "type": "string",
          "subtype": "signature",
          "name": "Party A Rejection Signature",
          "validation": {
            "required": true
          }
        }
      },
      "issuer": "${variables.partyAEthAddress.value}"
    }
  },
  "transitions": [
    {
      "from": "PENDING_PARTY_A_SIGNATURE",
      "to": "PENDING_PARTY_B_SIGNATURE",
      "conditions": [
        {
          "type": "isValid",
          "input": "partyAData"
        }
      ]
    },
    {
      "from": "PENDING_PARTY_B_SIGNATURE",
      "to": "PENDING_ACCEPTANCE",
      "conditions": [
        {
          "type": "isValid",
          "input": "partyBData"
        }
      ]
    },
    {
      "from": "PENDING_ACCEPTANCE",
      "to": "ACCEPTED",
      "conditions": [
        {
          "type": "isValid",
          "input": "accepted"
        }
      ]
    },
    {
      "from": "PENDING_ACCEPTANCE",
      "to": "REJECTED",
      "conditions": [
        {
          "type": "isValid",
          "input": "rejected"
        }
      ]
    }
  ]
}
}}
/>

## Canonical agreement JSON

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

```json title="simple-agreement.json" theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "metadata": {
    "id": "did:example:mou-v1",
    "templateId": "did:template:mou-v1",
    "version": "1.0.0",
    "createdAt": "2024-03-20T12:00:00Z",
    "name": "Memorandum of Understanding",
    "author": "Agreements Protocol",
    "description": "Template for non-binding memorandum of understanding between two parties"
  },
  "variables": {
    "partyAEthAddress": {
      "type": "address",
      "subtype": "participant",
      "name": "Party A Address",
      "description": "Ethereum address of the first party",
      "validation": {
        "required": true
      }
    },
    "partyAName": {
      "type": "string",
      "name": "Party A Name",
      "description": "Legal name of the first party",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "partyASignature": {
      "type": "string",
      "subtype": "signature",
      "name": "Party A Signature",
      "description": "Digital signature of the first party",
      "validation": {
        "required": true
      }
    },
    "partyBEthAddress": {
      "type": "address",
      "subtype": "participant",
      "name": "Party B Address",
      "description": "Ethereum address of the second party",
      "validation": {
        "required": true
      }
    },
    "partyBName": {
      "type": "string",
      "name": "Party B Name",
      "description": "Legal name of the second party",
      "validation": {
        "required": true,
        "minLength": 1
      }
    },
    "partyBSignature": {
      "type": "string",
      "subtype": "signature",
      "name": "Party B Signature",
      "description": "Digital signature of the second party",
      "validation": {
        "required": true
      }
    },
    "effectiveDate": {
      "type": "dateTime",
      "name": "Effective Date",
      "description": "The date when this MOU becomes effective",
      "validation": {
        "required": true
      }
    },
    "scope": {
      "type": "string",
      "name": "Scope of Cooperation",
      "description": "The scope of cooperation between the parties",
      "validation": {
        "required": true
      }
    },
    "termDuration": {
      "type": "string",
      "name": "Term Duration",
      "description": "The duration of the agreement",
      "validation": {
        "required": true
      }
    }
  },
  "content": {
    "type": "md",
    "data": "# MEMORANDUM OF UNDERSTANDING\n\n**BETWEEN PARTY A:**\n\n<u>${variables.partyAName}</u> (Party A Name)\n\n<u>${variables.partyAEthAddress}</u> (Party A Address)\n\n**AND PARTY B:**\n\n<u>${variables.partyBName}</u> (Party B Name)\n\n<u>${variables.partyBEthAddress}</u> (Party B Address)\n\n**EFFECTIVE DATE:**\n\n<u>${variables.effectiveDate}</u> (Effective Date)\n\n## 1. INTRODUCTION\n\nThis Memorandum of Understanding (\"MOU\") is entered into by and between Party A and Party B (collectively referred to as the \"Parties\").\n\nThe purpose of this MOU is to identify the roles and responsibilities of each Party.\n\n## 2. SCOPE OF COOPERATION\n\n<u>${variables.scope}</u>\n(Scope)\n\n## 3. RESPONSIBILITIES\n   - Maintain regular communication regarding the progress of collaborative activities.\n   - Designate representatives to coordinate the implementation of this MOU.\n   - Share relevant information and resources necessary for the successful implementation of this MOU.\n   - Acknowledge the contribution of the other Party in all public communications related to activities conducted under this MOU.\n\n## 4. TERM AND TERMINATION\n\n4.1 This MOU shall become effective on the date of the last signature below and shall remain in effect for a period of ${variables.termDuration} unless terminated earlier.\n\n4.2 Either Party may terminate this MOU by providing written notice to the other Party.\n\n4.3 Termination of this MOU shall not affect the completion of any activities already in progress, unless otherwise agreed by the Parties.\n\n## 5. CONFIDENTIALITY\n\n5.1 During the course of this MOU, the Parties may share confidential and proprietary information with each other. Each Party agrees to maintain the confidentiality of all information designated as confidential by the disclosing Party and shall not disclose such information to any third party without the prior written consent of the disclosing Party.\n\n## 6. INTELLECTUAL PROPERTY\n\n6.1 This MOU does not transfer any intellectual property rights between the Parties.\n\n6.2 Each Party shall retain all rights, title, and interest in its own intellectual property.\n\n6.3 Any intellectual property created jointly by the Parties during the course of activities under this MOU shall be owned jointly by the Parties, with specific terms to be negotiated in good faith and documented in a separate written agreement.\n\n## 7. SIGNATURES\n\nIN WITNESS WHEREOF, the Parties have executed this Memorandum of Understanding as of the Effective Date.\n\n<u>${variables.partyASignature}</u>\n(Party A Signature)\n\n<u>${variables.partyBSignature}</u>\n(Party B Signature)\n\nBy signing, I confirm that I have read, understood, and agree to be legally bound by all terms of this agreement."
  },
  "execution": {
    "states": {
      "PENDING_PARTY_A_SIGNATURE": {
        "name": "Pending Signature From A",
        "description": "This state awaits until Party A supplies Party B's address, effective date, scope, duration, and their own name."
      },
      "PENDING_PARTY_B_SIGNATURE": {
        "name": "Pending Signature From B",
        "description": "This state awaits until Party B confirms their identity by supplying their name."
      },
      "PENDING_ACCEPTANCE": {
        "name": "Pending Final Acceptance",
        "description": "This state awaits Party A's final acceptance of Party B's data."
      },
      "ACCEPTED": {
        "name": "Agreement Accepted",
        "description": "The agreement has been accepted by both parties and is now in force."
      },
      "REJECTED": {
        "name": "Agreement Rejected",
        "description": "The agreement has been rejected by Party A and will not proceed."
      }
    },
    "initialize": {
      "name": "Initialize",
      "description": "Initialize the agreement",
      "initialState": "PENDING_PARTY_A_SIGNATURE",
      "data": {
        "partyAEthAddress": "${variables.partyAEthAddress}",
        "partyBEthAddress": "${variables.partyBEthAddress}"
      }
    },
    "inputs": {
      "partyAData": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Party A Signature",
        "description": "EIP712 signature from Party A proposing the MOU terms including scope, duration, and effective date",
        "data": {
          "partyAName": "${variables.partyAName}",
          "scope": "${variables.scope}",
          "termDuration": "${variables.termDuration}",
          "effectiveDate": "${variables.effectiveDate}"
        },
        "issuer": "${variables.partyAEthAddress.value}"
      },
      "partyBData": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Party B Signature",
        "description": "EIP712 signature from Party B accepting the MOU terms",
        "data": {
          "partyBName": "${variables.partyBName}",
          "partyBSignature": "${variables.partyBSignature}"
        },
        "issuer": "${variables.partyBEthAddress.value}"
      },
      "accepted": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Party A Accepted Party B's Data",
        "description": "EIP712 signature from Party A accepting Party B's data",
        "data": {
          "partyASignature": "${variables.partyASignature}"
        },
        "issuer": "${variables.partyAEthAddress.value}"
      },
      "rejected": {
        "type": "VerifiedCredentialEIP712",
        "schema": "verified-credential-eip712.schema.json",
        "displayName": "Party A Rejected Party B's Data",
        "description": "EIP712 signature from Party A rejecting Party B's data",
        "data": {
          "partyARejectionSignature": {
            "type": "string",
            "subtype": "signature",
            "name": "Party A Rejection Signature",
            "validation": {
              "required": true
            }
          }
        },
        "issuer": "${variables.partyAEthAddress.value}"
      }
    },
    "transitions": [
      {
        "from": "PENDING_PARTY_A_SIGNATURE",
        "to": "PENDING_PARTY_B_SIGNATURE",
        "conditions": [
          {
            "type": "isValid",
            "input": "partyAData"
          }
        ]
      },
      {
        "from": "PENDING_PARTY_B_SIGNATURE",
        "to": "PENDING_ACCEPTANCE",
        "conditions": [
          {
            "type": "isValid",
            "input": "partyBData"
          }
        ]
      },
      {
        "from": "PENDING_ACCEPTANCE",
        "to": "ACCEPTED",
        "conditions": [
          {
            "type": "isValid",
            "input": "accepted"
          }
        ]
      },
      {
        "from": "PENDING_ACCEPTANCE",
        "to": "REJECTED",
        "conditions": [
          {
            "type": "isValid",
            "input": "rejected"
          }
        ]
      }
    ]
  }
}
```

## Related pages

* [Agreement data standard](/system-architecture/data-standard)
* [Author Agreement JSON](/workflow/author-agreement-json)
* [Complex Agreement](/examples/complex)
