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

# Eligibility and approval workflows

> Build rules that determine qualification for loans, services, benefits, or access.

export const DecisionGraphViz = ({nodes = [], edges = [], highlighted = null, title = null}) => {
  const nodeColors = {
    input: {
      bg: '#f8fafc',
      border: '#94a3b8',
      text: '#475569',
      icon: '→'
    },
    output: {
      bg: '#f8fafc',
      border: '#94a3b8',
      text: '#475569',
      icon: '←'
    },
    table: {
      bg: '#fefce8',
      border: '#d9c860',
      text: '#7c6f2a',
      icon: '▤'
    },
    expression: {
      bg: '#f5f3ff',
      border: '#a78bda',
      text: '#5b4a8c',
      icon: 'ƒ'
    },
    function: {
      bg: '#fdf2f8',
      border: '#e293b7',
      text: '#8c4a6d',
      icon: '{ }'
    },
    switch: {
      bg: '#f0fdfa',
      border: '#6bc4b8',
      text: '#3d7a72',
      icon: '◇'
    }
  };
  const nodeWidth = 140;
  const nodeHeight = 64;
  const horizontalGap = 80;
  const verticalGap = 24;
  const inDegree = {};
  const outEdges = {};
  nodes.forEach(n => {
    inDegree[n.id] = 0;
    outEdges[n.id] = [];
  });
  edges.forEach(e => {
    inDegree[e.to] = (inDegree[e.to] || 0) + 1;
    outEdges[e.from] = outEdges[e.from] || [];
    outEdges[e.from].push(e.to);
  });
  const queue = nodes.filter(n => inDegree[n.id] === 0).map(n => n.id);
  const nodeLevel = {};
  queue.forEach(id => nodeLevel[id] = 0);
  const tempInDegree = {
    ...inDegree
  };
  while (queue.length > 0) {
    const current = queue.shift();
    const currentLevel = nodeLevel[current];
    (outEdges[current] || []).forEach(next => {
      nodeLevel[next] = Math.max(nodeLevel[next] || 0, currentLevel + 1);
      tempInDegree[next]--;
      if (tempInDegree[next] === 0) queue.push(next);
    });
  }
  const levels = {};
  nodes.forEach(n => {
    const level = nodeLevel[n.id] || 0;
    levels[level] = levels[level] || [];
    levels[level].push(n);
  });
  const nodePositions = {};
  const numLevels = Object.keys(levels).length;
  const maxNodesInLevel = Math.max(...Object.values(levels).map(l => l.length), 1);
  const canvasHeight = Math.max(maxNodesInLevel * (nodeHeight + verticalGap) + 60, 180);
  const canvasWidth = numLevels * (nodeWidth + horizontalGap) + 80;
  Object.entries(levels).forEach(([level, levelNodes]) => {
    const levelNum = parseInt(level);
    const x = 40 + levelNum * (nodeWidth + horizontalGap);
    const totalHeight = levelNodes.length * nodeHeight + (levelNodes.length - 1) * verticalGap;
    const startY = (canvasHeight - totalHeight) / 2;
    levelNodes.forEach((node, index) => {
      nodePositions[node.id] = {
        x,
        y: startY + index * (nodeHeight + verticalGap)
      };
    });
  });
  const edgePaths = edges.map((edge, i) => {
    const from = nodePositions[edge.from];
    const to = nodePositions[edge.to];
    if (!from || !to) return null;
    const startX = from.x + nodeWidth;
    const startY = from.y + nodeHeight / 2;
    const endX = to.x - 6;
    const endY = to.y + nodeHeight / 2;
    const dx = endX - startX;
    const controlOffset = Math.min(dx * 0.4, 40);
    return {
      key: `edge-${i}`,
      path: `M ${startX} ${startY} C ${startX + controlOffset} ${startY}, ${endX - controlOffset} ${endY}, ${endX} ${endY}`
    };
  }).filter(Boolean);
  const uniqueId = `graph-${Math.random().toString(36).substring(2, 11)}`;
  const containerRef = useRef(null);
  const [scale, setScale] = useState(1);
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStart, setDragStart] = useState({
    x: 0,
    y: 0
  });
  const handleWheel = e => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -0.1 : 0.1;
    setScale(s => Math.max(0.4, Math.min(2, s + delta)));
  };
  const handleMouseDown = e => {
    if (e.target.closest('button')) return;
    setIsDragging(true);
    setDragStart({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };
  const handleMouseMove = e => {
    if (!isDragging) return;
    setPosition({
      x: e.clientX - dragStart.x,
      y: e.clientY - dragStart.y
    });
  };
  const handleMouseUp = () => setIsDragging(false);
  const handleFit = () => {
    const container = containerRef.current;
    if (container) {
      const containerWidth = container.clientWidth - 40;
      const containerHeight = container.clientHeight - 80;
      const scaleX = containerWidth / canvasWidth;
      const scaleY = containerHeight / canvasHeight;
      const fitScale = Math.min(scaleX, scaleY, 1);
      setScale(Math.max(0.4, fitScale));
    } else {
      setScale(1);
    }
    setPosition({
      x: 0,
      y: 0
    });
  };
  const handleZoomIn = () => setScale(s => Math.min(s + 0.15, 2));
  const handleZoomOut = () => setScale(s => Math.max(s - 0.15, 0.4));
  useEffect(() => {
    const container = containerRef.current;
    if (container) {
      container.addEventListener('wheel', handleWheel, {
        passive: false
      });
      return () => container.removeEventListener('wheel', handleWheel);
    }
  }, []);
  useEffect(() => {
    const timer = setTimeout(() => {
      const container = containerRef.current;
      if (container) {
        const containerWidth = container.clientWidth - 40;
        const containerHeight = container.clientHeight - 80;
        const scaleX = containerWidth / canvasWidth;
        const scaleY = containerHeight / canvasHeight;
        const fitScale = Math.min(scaleX, scaleY, 1);
        setScale(Math.max(0.4, fitScale));
      }
    }, 0);
    return () => clearTimeout(timer);
  }, [canvasWidth, canvasHeight]);
  const containerHeight = Math.max(300, Math.min(canvasHeight + 80, 420));
  return <div style={{
    marginTop: '16px',
    marginBottom: '24px'
  }}>
      {title && <div style={{
    fontSize: '13px',
    fontWeight: '500',
    color: '#64748b',
    marginBottom: '8px',
    fontFamily: 'system-ui, -apple-system, sans-serif'
  }}>
          {title}
        </div>}
      <div ref={containerRef} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} style={{
    position: 'relative',
    border: '1px solid #e5e7eb',
    borderRadius: '12px',
    backgroundColor: '#f9fafb',
    overflow: 'hidden',
    height: containerHeight,
    cursor: isDragging ? 'grabbing' : 'grab',
    userSelect: 'none'
  }}>
        {}
        <div style={{
    position: 'absolute',
    top: '12px',
    right: '12px',
    display: 'flex',
    gap: '6px',
    zIndex: 10
  }}>
          <button onClick={handleZoomIn} style={{
    width: '32px',
    height: '32px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '18px',
    color: '#6b7280',
    fontWeight: '400',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>+</button>
          <button onClick={handleZoomOut} style={{
    width: '32px',
    height: '32px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '18px',
    color: '#6b7280',
    fontWeight: '400',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>−</button>
          <button onClick={handleFit} style={{
    height: '32px',
    padding: '0 12px',
    border: '1px solid #d1d5db',
    borderRadius: '8px',
    backgroundColor: 'white',
    cursor: 'pointer',
    fontSize: '12px',
    fontWeight: '500',
    color: '#6b7280',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
    transition: 'background-color 0.15s'
  }} onMouseOver={e => e.target.style.backgroundColor = '#f3f4f6'} onMouseOut={e => e.target.style.backgroundColor = 'white'}>Reset</button>
        </div>

        {}
        <div style={{
    transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`,
    transformOrigin: 'center center',
    width: canvasWidth,
    height: canvasHeight,
    position: 'absolute',
    left: '50%',
    top: '50%'
  }}>
          {}
          <svg style={{
    position: 'absolute',
    top: 0,
    left: 0,
    width: canvasWidth,
    height: canvasHeight,
    pointerEvents: 'none',
    overflow: 'visible'
  }}>
            <defs>
              <marker id={`${uniqueId}-arrow`} markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto" markerUnits="strokeWidth">
                <path d="M0,1 L6,4 L0,7 L2,4 Z" fill="#9ca3af" />
              </marker>
            </defs>
            {edgePaths.map(edge => <path key={edge.key} d={edge.path} fill="none" stroke="#d1d5db" strokeWidth="2" strokeLinecap="round" markerEnd={`url(#${uniqueId}-arrow)`} />)}
          </svg>

          {}
          {nodes.map(node => {
    const pos = nodePositions[node.id];
    if (!pos) return null;
    const colors = nodeColors[node.type] || nodeColors.expression;
    const isHighlighted = highlighted === node.id;
    return <div key={node.id} style={{
      position: 'absolute',
      left: pos.x,
      top: pos.y,
      width: nodeWidth,
      height: nodeHeight,
      backgroundColor: isHighlighted ? '#fef2f2' : colors.bg,
      border: `1.5px solid ${isHighlighted ? '#f87171' : colors.border}`,
      borderRadius: '10px',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      boxShadow: isHighlighted ? '0 0 0 2px rgba(248, 113, 113, 0.25)' : '0 1px 2px rgba(0,0,0,0.04)',
      cursor: 'default'
    }}>
                <div style={{
      fontSize: '14px',
      marginBottom: '3px',
      color: isHighlighted ? '#ef4444' : colors.text,
      opacity: 0.85
    }}>
                  {colors.icon}
                </div>
                <div style={{
      fontSize: '11px',
      fontWeight: '500',
      color: isHighlighted ? '#dc2626' : colors.text,
      textAlign: 'center',
      padding: '0 8px',
      fontFamily: 'system-ui, -apple-system, sans-serif',
      lineHeight: '1.35',
      maxWidth: '100%'
    }}>
                  {node.label}
                </div>
              </div>;
  })}
        </div>

        {}
        <div style={{
    position: 'absolute',
    bottom: '12px',
    left: '12px',
    display: 'flex',
    gap: '14px',
    padding: '8px 12px',
    fontSize: '11px',
    color: '#6b7280',
    fontFamily: 'system-ui, -apple-system, sans-serif',
    backgroundColor: 'white',
    borderRadius: '8px',
    border: '1px solid #e5e7eb',
    boxShadow: '0 1px 2px rgba(0,0,0,0.05)'
  }}>
          {Object.entries(nodeColors).map(([type, colors]) => <div key={type} style={{
    display: 'flex',
    alignItems: 'center',
    gap: '5px'
  }}>
              <div style={{
    width: '16px',
    height: '16px',
    backgroundColor: colors.bg,
    border: `1px solid ${colors.border}`,
    borderRadius: '4px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    fontSize: '9px',
    color: colors.text
  }}>
                {colors.icon}
              </div>
              <span style={{
    textTransform: 'capitalize'
  }}>{type}</span>
            </div>)}
        </div>
      </div>
    </div>;
};

export const ExpressionViz = ({entries = []}) => {
  const allCells = entries.flatMap((entry, i) => [<div key={`key-${i}`} style={{
    backgroundColor: '#dbeafe',
    color: '#1e40af',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    fontWeight: '500'
  }}>
      {entry.key}
    </div>, <div key={`val-${i}`} style={{
    backgroundColor: '#ffffff',
    color: '#334155',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
      {entry.value}
    </div>]);
  return <div style={{
    backgroundColor: '#f1f5f9',
    borderRadius: '12px',
    padding: '20px',
    margin: '16px 0'
  }}>
      <div style={{
    display: 'grid',
    gridTemplateColumns: 'auto 1fr',
    gap: '8px'
  }}>
        {allCells}
      </div>
    </div>;
};

export const DecisionTableViz = ({inputs = [], outputs = [], rows = []}) => {
  const colCount = inputs.length + outputs.length;
  const getFieldDisplay = field => !field || field === '-' ? '-' : field;
  const allCells = rows.flatMap((row, ri) => [...inputs.map((input, i) => <div key={`c-${ri}-in-${i}`} style={{
    backgroundColor: '#ffffff',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
        {row[input.field] ?? ''}
      </div>), ...outputs.map((output, i) => <div key={`c-${ri}-out-${i}`} style={{
    backgroundColor: '#ffffff',
    padding: '12px 16px',
    borderRadius: '8px',
    fontFamily: 'ui-monospace, monospace',
    fontSize: '14px',
    border: '1px solid #e2e8f0'
  }}>
        {row[output.field] ?? ''}
      </div>)]);
  return <div style={{
    backgroundColor: '#f1f5f9',
    borderRadius: '12px',
    padding: '20px',
    overflowX: 'auto',
    margin: '16px 0'
  }}>
      <div style={{
    display: 'grid',
    gridTemplateColumns: `repeat(${colCount}, 1fr)`,
    gap: '8px'
  }}>
        {}
        <div key="group-inputs" style={{
    gridColumn: `span ${inputs.length}`,
    padding: '8px 16px',
    fontWeight: '600',
    fontSize: '13px',
    color: '#374151',
    borderBottom: '2px solid #e2e8f0'
  }}>
          Inputs
        </div>
        <div key="group-outputs" style={{
    gridColumn: `span ${outputs.length}`,
    padding: '8px 16px',
    fontWeight: '600',
    fontSize: '13px',
    color: '#374151',
    borderBottom: '2px solid #e2e8f0'
  }}>
          Outputs
        </div>

        {}
        {inputs.map((input, i) => <div key={`h-in-${i}`} style={{
    backgroundColor: '#f0abfc',
    color: '#581c87',
    padding: '12px 16px',
    borderRadius: '8px',
    fontWeight: '500'
  }}>
            <div>{input.name}</div>
            <div style={{
    display: 'inline-block',
    marginTop: '6px',
    padding: '2px 8px',
    fontSize: '12px',
    fontWeight: '400',
    backgroundColor: 'rgba(88, 28, 135, 0.15)',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, monospace'
  }}>
              {getFieldDisplay(input.field)}
            </div>
          </div>)}
        {outputs.map((output, i) => <div key={`h-out-${i}`} style={{
    backgroundColor: '#a5b4fc',
    color: '#312e81',
    padding: '12px 16px',
    borderRadius: '8px',
    fontWeight: '500'
  }}>
            <div>{output.name}</div>
            <div style={{
    display: 'inline-block',
    marginTop: '6px',
    padding: '2px 8px',
    fontSize: '12px',
    fontWeight: '400',
    backgroundColor: 'rgba(49, 46, 129, 0.15)',
    borderRadius: '4px',
    fontFamily: 'ui-monospace, monospace'
  }}>
              {getFieldDisplay(output.field)}
            </div>
          </div>)}

        {}
        {allCells}
      </div>
    </div>;
};

This guide shows how to build eligibility rules that evaluate applicants against multiple criteria and route them through approval workflows.

## What you'll build

An eligibility decision that:

* Evaluates applicants against qualification criteria
* Handles multiple approval paths (auto-approve, manual review, deny)
* Provides clear reasons for decisions
* Supports tiered approval thresholds

<Note>
  Want to skip ahead? <a href="/downloads/eligibility.json" download>Download the completed decision</a> and import it directly.
</Note>

### Decision flow

<DecisionGraphViz
  title="Loan Pre-Qualification"
  nodes={[
{ id: "input", type: "input", label: "Applicant Data" },
{ id: "basic", type: "table", label: "Basic Eligibility" },
{ id: "credit", type: "table", label: "Credit Check" },
{ id: "capacity", type: "expression", label: "Loan Capacity" },
{ id: "router", type: "table", label: "Approval Router" }
]}
  edges={[
{ from: "input", to: "basic" },
{ from: "input", to: "credit" },
{ from: "basic", to: "capacity" },
{ from: "credit", to: "capacity" },
{ from: "capacity", to: "router" }
]}
/>

## Example: Loan pre-qualification

### Input data

```json theme={null}
{
  "applicant": {
    "age": 32,
    "income": 75000,
    "employmentYears": 4,
    "creditScore": 720
  },
  "loan": {
    "amount": 25000,
    "purpose": "home_improvement"
  }
}
```

## Step 1: Basic eligibility checks

First, check hard requirements that must be met:

<DecisionTableViz
  inputs={[
{ name: "Age", field: "applicant.age" },
{ name: "Income", field: "applicant.income" },
{ name: "Employment Years", field: "applicant.employmentYears" }
]}
  outputs={[
{ name: "Eligible", field: "eligible" },
{ name: "Reason", field: "reason" }
]}
  rows={[
{ "applicant.age": "< 18", "applicant.income": "", "applicant.employmentYears": "", eligible: "false", reason: '"Must be 18 or older"' },
{ "applicant.age": ">= 65", "applicant.income": "", "applicant.employmentYears": "", eligible: "false", reason: '"Age exceeds limit"' },
{ "applicant.age": "", "applicant.income": "< 25000", "applicant.employmentYears": "", eligible: "false", reason: '"Income below minimum"' },
{ "applicant.age": "", "applicant.income": "", "applicant.employmentYears": "< 1", eligible: "false", reason: '"Insufficient employment history"' },
{ "applicant.age": "", "applicant.income": "", "applicant.employmentYears": "", eligible: "true", reason: '"Meets basic requirements"' }
]}
/>

## Step 2: Credit assessment

Evaluate credit score bands:

<DecisionTableViz
  inputs={[
{ name: "Credit Score", field: "applicant.creditScore" }
]}
  outputs={[
{ name: "Credit Tier", field: "tier" },
{ name: "Max Loan Multiple", field: "multiple" }
]}
  rows={[
{ "applicant.creditScore": ">= 750", tier: '"excellent"', multiple: "5" },
{ "applicant.creditScore": "[700..749]", tier: '"good"', multiple: "4" },
{ "applicant.creditScore": "[650..699]", tier: '"fair"', multiple: "3" },
{ "applicant.creditScore": "[600..649]", tier: '"poor"', multiple: "2" },
{ "applicant.creditScore": "< 600", tier: '"denied"', multiple: "0" }
]}
/>

## Step 3: Calculate loan capacity

Use an expression to determine maximum loan amount:

<ExpressionViz
  entries={[
{ key: "annualIncome", value: "applicant.income" },
{ key: "debtToIncomeLimit", value: "0.40" },
{ key: "maxMonthlyPayment", value: "annualIncome / 12 * debtToIncomeLimit" },
{ key: "maxLoanByIncome", value: "maxMonthlyPayment * 48" },
{ key: "maxLoanByCredit", value: "annualIncome * CreditCheck.multiple" },
{ key: "approvedAmount", value: "min([loan.amount, maxLoanByIncome, maxLoanByCredit])" }
]}
/>

## Step 4: Determine approval path

Route to different outcomes based on risk:

<DecisionTableViz
  inputs={[
{ name: "Basic Eligible", field: "eligible" },
{ name: "Credit Tier", field: "tier" },
{ name: "Loan vs Approved", field: "ratio" }
]}
  outputs={[
{ name: "Decision", field: "decision" },
{ name: "Next Step", field: "nextStep" }
]}
  rows={[
{ eligible: "false", tier: "", ratio: "", decision: '"denied"', nextStep: '"none"' },
{ eligible: "", tier: '"denied"', ratio: "", decision: '"denied"', nextStep: '"none"' },
{ eligible: "true", tier: '"excellent"', ratio: "<= 1", decision: '"approved"', nextStep: '"auto_fund"' },
{ eligible: "true", tier: '"good"', ratio: "<= 1", decision: '"approved"', nextStep: '"verification"' },
{ eligible: "true", tier: '"fair"', ratio: "<= 1", decision: '"review"', nextStep: '"manual_review"' },
{ eligible: "true", tier: "", ratio: "> 1", decision: '"partial"', nextStep: '"counter_offer"' }
]}
/>

## Output structure

```json theme={null}
{
  "decision": "approved",
  "nextStep": "verification",
  "approvedAmount": 25000,
  "creditTier": "good",
  "reasons": ["Meets basic requirements", "Good credit standing"]
}
```

## Example: Benefits eligibility

### Household income check

<DecisionTableViz
  inputs={[
{ name: "Household Size", field: "size" },
{ name: "Annual Income", field: "income" }
]}
  outputs={[
{ name: "Eligible", field: "eligible" },
{ name: "Benefit Level", field: "level" }
]}
  rows={[
{ size: "1", income: "< 15000", eligible: "true", level: '"full"' },
{ size: "1", income: "[15000..25000]", eligible: "true", level: '"partial"' },
{ size: "2", income: "< 20000", eligible: "true", level: '"full"' },
{ size: "2", income: "[20000..35000]", eligible: "true", level: '"partial"' },
{ size: ">= 3", income: "< 25000", eligible: "true", level: '"full"' },
{ size: ">= 3", income: "[25000..45000]", eligible: "true", level: '"partial"' },
{ size: "", income: "", eligible: "false", level: '"none"' }
]}
/>

## Example: Access control

### Feature access by subscription

<DecisionTableViz
  inputs={[
{ name: "Plan", field: "plan" },
{ name: "Feature", field: "feature" }
]}
  outputs={[
{ name: "Allowed", field: "allowed" },
{ name: "Limit", field: "limit" }
]}
  rows={[
{ plan: '"enterprise"', feature: "", allowed: "true", limit: "null" },
{ plan: '"pro"', feature: '"api_access"', allowed: "true", limit: "10000" },
{ plan: '"pro"', feature: '"exports"', allowed: "true", limit: "100" },
{ plan: '"basic"', feature: '"api_access"', allowed: "false", limit: "0" },
{ plan: '"basic"', feature: '"exports"', allowed: "true", limit: "10" },
{ plan: "", feature: "", allowed: "false", limit: "0" }
]}
/>

## Best practices

**Fail fast** — Put disqualifying checks first to exit early.

**Provide reasons** — Always output why a decision was made.

**Handle edge cases** — Include catch-all rows for unexpected inputs.

**Version your rules** — Track changes when eligibility criteria update.
