{
  "name": "Answer legal questions with grounded citations from CanLII and CourtListener",
  "nodes": [
    {
      "parameters": {
        "formTitle": "Legal research assistant",
        "formDescription": "Ask a legal question and get an answer grounded only in authorities retrieved from CanLII or CourtListener, with verified citations.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Your legal question or issue",
              "fieldType": "textarea",
              "requiredField": true,
              "placeholder": "e.g. What is the test for granting an interlocutory injunction?"
            },
            {
              "fieldLabel": "Source database",
              "fieldType": "dropdown",
              "requiredField": true,
              "fieldOptions": {
                "values": [
                  {
                    "option": "CourtListener (US case law, full-text search)"
                  },
                  {
                    "option": "CanLII (Canadian authority, case plus citators)"
                  }
                ]
              }
            },
            {
              "fieldLabel": "CanLII database ID",
              "fieldType": "text",
              "requiredField": false,
              "placeholder": "CanLII mode only, e.g. csc-scc"
            },
            {
              "fieldLabel": "CanLII case ID",
              "fieldType": "text",
              "requiredField": false,
              "placeholder": "CanLII mode only, e.g. 2024scc1"
            }
          ]
        }
      },
      "id": "form-trigger",
      "name": "Submit a legal question",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        16,
        336
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "resultCount",
              "value": 5,
              "type": "number"
            },
            {
              "id": "a2",
              "name": "canliiLanguage",
              "value": "en",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "citatorDirection",
              "value": "citingCases",
              "type": "string"
            },
            {
              "id": "a4",
              "name": "courtFilter",
              "value": "",
              "type": "string"
            },
            {
              "id": "a5",
              "name": "maxSnippetChars",
              "value": 500,
              "type": "number"
            },
            {
              "id": "a6",
              "name": "disclaimer",
              "value": "Research assistance only. This is not legal advice and does not create a lawyer-client relationship. Read and verify every cited source before relying on it.",
              "type": "string"
            }
          ]
        }
      },
      "id": "set-config",
      "name": "Set research configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        192,
        336
      ]
    },
    {
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "outputKey": "CourtListener",
              "renameOutput": true,
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $('Submit a legal question').item.json['Source database'] }}",
                    "rightValue": "CourtListener",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "outputKey": "CanLII",
              "renameOutput": true,
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $('Submit a legal question').item.json['Source database'] }}",
                    "rightValue": "CanLII",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    }
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        }
      },
      "id": "switch",
      "name": "Route by source database",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        448,
        336
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://www.courtlistener.com/api/rest/v4/search/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $('Submit a legal question').item.json['Your legal question or issue'] }}"
            },
            {
              "name": "type",
              "value": "o"
            },
            {
              "name": "order_by",
              "value": "score desc"
            }
          ]
        }
      },
      "id": "http-cl",
      "name": "Search CourtListener case law",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        672,
        320
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://www.courtlistener.com/api/rest/v4/opinions/{{ ($json.results && $json.results[0] && $json.results[0].opinions && $json.results[0].opinions[0] ? $json.results[0].opinions[0].id : '') }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "id": "http-cl-opinion",
      "name": "Fetch CourtListener opinion text",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        928,
        304
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.canlii.org/v1/caseBrowse/{{ $('Set research configuration').item.json.canliiLanguage }}/{{ $('Submit a legal question').item.json['CanLII database ID'] }}/{{ $('Submit a legal question').item.json['CanLII case ID'] }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth"
      },
      "id": "http-canlii-meta",
      "name": "Fetch CanLII case metadata",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        672,
        496
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.canlii.org/v1/caseCitator/en/{{ $('Submit a legal question').item.json['CanLII database ID'] }}/{{ $('Submit a legal question').item.json['CanLII case ID'] }}/{{ $('Set research configuration').item.json.citatorDirection }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth"
      },
      "id": "http-canlii-citator",
      "name": "Fetch CanLII citator cases",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        928,
        496
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Convergence point for both retrieval branches. Normalizes whichever source ran\n// into a single numbered block the model can cite from. For CourtListener it grounds\n// the top-ranked result on its full opinion text (fetched in the prior step), windowed\n// to the passage matching the question; remaining results fall back to the search\n// snippet. For CanLII it uses the case plus its citator network.\nconst cfg = $('Set research configuration').first().json;\nconst max = Number(cfg.resultCount) || 5;\nconst maxSnip = Number(cfg.maxSnippetChars) || 500;\nconst q = $('Submit a legal question').first().json['Your legal question or issue'] || '';\nconst dbChoice = $('Submit a legal question').first().json['Source database'] || '';\nconst isCourtListener = dbChoice.includes('CourtListener');\n\nlet sources = [];\nlet retrievedFrom = '';\n\nif (isCourtListener) {\n  retrievedFrom = 'CourtListener';\n  const results = ($('Search CourtListener case law').first().json.results || []).slice(0, max);\n  // Full text of the top-ranked opinion, fetched by id in the prior node.\n  const fetched = $json || {};\n  const topId = Number(fetched.id);\n  const topFull = fetched.plain_text || fetched.html || '';\n  // Keyword set from the question, used to window the relevant passage of the opinion.\n  const stop = new Set(['the','and','for','what','is','are','was','were','a','an','of','in','to','on','with','that','this','federal','court','courts','case','cases','law','laws','standard','question','issue','under']);\n  const keywords = q.toLowerCase().split(/[^a-z]+/).filter(w => w.length > 3 && !stop.has(w));\n  const windowText = (text) => {\n    const clean = String(text || '').replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim();\n    if (!clean) return '';\n    const lower = clean.toLowerCase();\n    let pos = -1;\n    for (const k of keywords) { const p = lower.indexOf(k); if (p !== -1 && (pos === -1 || p < pos)) pos = p; }\n    if (pos === -1) return clean.slice(0, 1500);\n    const start = Math.max(0, pos - 200);\n    const end = Math.min(clean.length, start + 1500);\n    return (start > 0 ? '...' : '') + clean.slice(start, end) + (end < clean.length ? '...' : '');\n  };\n  sources = results.map((r, i) => {\n    const op = Array.isArray(r.opinions) && r.opinions[0] ? r.opinions[0] : {};\n    const full = (Number(op.id) === topId) ? topFull : '';\n    const excerpt = full\n      ? windowText(full)\n      : String(op.snippet || r.snippet || '').replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim().slice(0, maxSnip);\n    return {\n      index: i + 1,\n      caseName: r.caseName || r.case_name || 'Unknown case',\n      citation: Array.isArray(r.citation) ? r.citation.join('; ') : (r.citation || ''),\n      court: r.court || r.court_id || '',\n      date: r.dateFiled || r.date_filed || '',\n      url: r.absolute_url ? ('https://www.courtlistener.com' + r.absolute_url) : '',\n      snippet: excerpt\n    };\n  });\n} else {\n  retrievedFrom = 'CanLII';\n  let caseMeta = {};\n  try { caseMeta = $('Fetch CanLII case metadata').first().json || {}; } catch (e) { caseMeta = {}; }\n  const citator = $json || {};\n  const citing = citator.citingCases || citator.citedCases || citator.citedLegislations || [];\n  sources.push({\n    index: 1,\n    caseName: caseMeta.title || 'Selected Canadian authority',\n    citation: caseMeta.citation || '',\n    court: caseMeta.databaseId || '',\n    date: caseMeta.decisionDate || '',\n    url: caseMeta.url || '',\n    snippet: 'Primary authority selected by the user.'\n  });\n  citing.slice(0, Math.max(0, max - 1)).forEach((c, i) => {\n    sources.push({\n      index: i + 2,\n      caseName: c.title || 'Related case',\n      citation: c.citation || '',\n      court: c.databaseId || '',\n      date: c.decisionDate || '',\n      url: c.url || '',\n      snippet: 'Case linked to the primary authority through the CanLII citator.'\n    });\n  });\n}\n\nconst lines = sources.map(s =>\n  '[' + s.index + '] ' + s.caseName +\n  ' | Citation: ' + (s.citation || 'n/a') +\n  ' | Court: ' + (s.court || 'n/a') +\n  ' | Date: ' + (s.date || 'n/a') +\n  ' | URL: ' + (s.url || 'n/a') +\n  '\\nExcerpt: ' + (s.snippet || 'n/a')\n).join('\\n\\n');\nconst groundingContext = sources.length ? lines : 'NO SOURCES WERE RETRIEVED.';\n\nreturn [{ json: {\n  question: q,\n  sources,\n  groundingContext,\n  sourceCount: sources.length,\n  retrievedFrom\n} }];\n"
      },
      "id": "build-context",
      "name": "Build grounding context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1184,
        432
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const err = ($json.error && ($json.error.message || $json.error)) || 'A retrieval step failed.';\nconst disclaimer = $('Set research configuration').first().json.disclaimer\n  || 'Research assistance only. This is not legal advice.';\nconst html = '<div style=\"font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:720px;line-height:1.55;\">' +\n  '<h3>The legal database could not be reached</h3>' +\n  '<p>The workflow could not retrieve sources, so no answer was generated. No case law is asserted.</p>' +\n  '<p style=\"color:#555;font-size:0.85em;\">Details: ' + String(err) + '</p>' +\n  '<p>Check that the API credential is set and valid, that the CanLII database ID and case ID are correct in CanLII mode, and that you are within the API rate limits, then submit again.</p>' +\n  '<hr><p style=\"color:#555;font-size:0.85em;\">' + disclaimer + '</p>' +\n  '</div>';\nreturn [{ json: { html } }];"
      },
      "id": "build-error",
      "name": "Build error result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1168,
        832
      ]
    },
    {
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "id": "model-answer",
      "name": "Groq Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        1456,
        528
      ],
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "id": "model-fixer",
      "name": "Citation parser fixer model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        1584,
        704
      ],
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\"type\": \"object\", \"properties\": {\"answer\": {\"type\": \"string\"}, \"supported\": {\"type\": \"boolean\"}, \"citations\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"index\": {\"type\": \"number\"}, \"caseName\": {\"type\": \"string\"}, \"citation\": {\"type\": \"string\"}, \"url\": {\"type\": \"string\"}, \"pinpoint\": {\"type\": \"string\"}}, \"required\": [\"index\", \"caseName\", \"citation\"]}}, \"caveats\": {\"type\": \"string\"}}, \"required\": [\"answer\", \"supported\", \"citations\"]}",
        "autoFix": true
      },
      "id": "parser",
      "name": "Structured citation parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1584,
        528
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a legal research assistant supporting a lawyer. Answer the QUESTION using ONLY the numbered SOURCES below.\n\nStrict rules:\n- Use only facts and holdings found in the SOURCES. Do not use any outside or prior knowledge.\n- Every legal proposition in your answer must reference a source by its number in square brackets, for example [1], and the exact citation string shown for that source.\n- Never output a citation, case name, or reporter reference that does not appear in the SOURCES list.\n- If the SOURCES do not contain enough to answer, set supported to false and state plainly that the retrieved sources do not support an answer. Do not guess, extrapolate, or invent authority.\n- Keep the answer concise and neutral. This is research assistance, not legal advice.\n\nIn the citations array, include only the sources you actually relied on. Set supported to true only when at least one retrieved source substantively supports the answer.\n\nQUESTION:\n{{ $json.question }}\n\nSOURCES:\n{{ $json.groundingContext }}",
        "hasOutputParser": true
      },
      "id": "chain",
      "name": "Answer grounded in retrieved sources",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        1456,
        336
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Layer 2 of the anti-hallucination guardrail: deterministically verify every\n// citation the model claimed against the retrieved sources, then render the result.\n// A citation is valid only if its index maps to a source we actually retrieved, and\n// we always render the source's own citation / name / url (never the model's strings),\n// so a fabricated reporter cite or case name cannot reach the reader. The verification\n// pass runs first and is kept separate from the grounding / prompt layer (layer 1).\nconst ai = $json.output || $json;\nconst ctx = $('Build grounding context').first().json;\nconst sources = ctx.sources || [];\nconst byIndex = new Map(sources.map(s => [Number(s.index), s]));\nconst claimed = Array.isArray(ai.citations) ? ai.citations : [];\nconst validated = [];\nconst dropped = [];\nfor (const c of claimed) {\n  const src = byIndex.get(Number(c.index));\n  if (!src) { dropped.push(c); continue; }\n  validated.push({\n    index: src.index,\n    caseName: src.caseName,\n    citation: src.citation,\n    url: src.url,\n    pinpoint: c.pinpoint || ''\n  });\n}\nconst supported = Boolean(ai.supported) && validated.length > 0;\nconst answer = supported\n  ? String(ai.answer || '')\n  : 'No supporting authority was found in the retrieved sources. The retrieved sources do not substantiate an answer to this question, so no case law is asserted. Try rephrasing the question, increasing the result count, or selecting a different source database.';\nconst caveats = String(ai.caveats || '');\nconst retrievedFrom = ctx.retrievedFrom;\nconst droppedCount = dropped.length;\n\n// Render the cited answer for display.\nconst disclaimer = $('Set research configuration').first().json.disclaimer\n  || 'Research assistance only. This is not legal advice. Read and verify every cited source before relying on it.';\nconst badge = supported\n  ? '<span style=\"background:#1a7f37;color:#fff;padding:3px 10px;border-radius:4px;font-size:0.85em;\">Supported by retrieved sources</span>'\n  : '<span style=\"background:#b35900;color:#fff;padding:3px 10px;border-radius:4px;font-size:0.85em;\">Not supported by retrieved sources</span>';\nconst cites = validated.map(c =>\n  '<li>[' + c.index + '] <strong>' + c.caseName + '</strong>' +\n  (c.citation ? ', ' + c.citation : '') +\n  (c.pinpoint ? ' (' + c.pinpoint + ')' : '') +\n  (c.url ? ' <a href=\"' + c.url + '\" target=\"_blank\" rel=\"noopener\">view source</a>' : '') +\n  '</li>'\n).join('');\nconst citeBlock = cites\n  ? '<h3>Verified citations</h3><ul>' + cites + '</ul>'\n  : '<p><em>No verified citations were returned.</em></p>';\nconst note = droppedCount\n  ? '<p style=\"color:#b35900;font-size:0.85em;\">' + droppedCount + ' proposed citation(s) were dropped because they did not match any retrieved source.</p>'\n  : '';\nconst answerHtml = answer.replace(/\\n/g, '<br>');\nconst html = '<div style=\"font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:720px;line-height:1.55;\">' +\n  '<p>' + badge + ' <span style=\"color:#555;font-size:0.85em;\">Source: ' + (retrievedFrom || 'n/a') + '</span></p>' +\n  '<h3>Answer</h3><p>' + answerHtml + '</p>' +\n  citeBlock + note +\n  (caveats ? '<h3>Caveats</h3><p>' + caveats + '</p>' : '') +\n  '<hr><p style=\"color:#555;font-size:0.85em;\">' + disclaimer + '</p>' +\n  '</div>';\n\nreturn [{ json: { html, supported, answer, validatedCitations: validated, droppedCount } }];\n"
      },
      "id": "verify",
      "name": "Verify citations and render result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        336
      ]
    },
    {
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ $json.html }}"
      },
      "id": "form-done",
      "name": "Return result",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        2176,
        496
      ]
    },
    {
      "parameters": {
        "content": "## Answer legal questions with grounded citations from CanLII and CourtListener\n\nResearch assistance for lawyers and legal researchers. It answers a natural-language legal question using only authorities retrieved live from a real legal database, then shows the citation behind every point. If the sources do not support an answer, it says so instead of inventing case law.\n\n### How it works\n1. You submit a question and pick a source: CourtListener (US case law, full-text search) or CanLII (a Canadian authority plus its citator network).\n2. The workflow retrieves authorities over HTTP from the chosen database.\n3. A language model answers using only those retrieved sources and returns a structured answer, a supported flag, and a citation list.\n4. A code step verifies every citation against the retrieved set and drops anything that does not match, so a hallucinated citation cannot reach the reader.\n5. The result page shows the answer, a supported or not-supported badge, the verified citations, and a disclaimer.\n\n### Setup\nAdd a CourtListener token (Header Auth) and a CanLII API key (Query Auth) as credentials, and connect a Groq chat model. Open \"Set research configuration\" to set result count, language, and citator direction.\n\n### Customization\nSwap the chat model, change the result count or CanLII citator direction, or add a Slack step to post results.\n\nThis is research assistance, not legal advice. The lawyer must read and verify every cited source.",
        "height": 1100,
        "width": 400,
        "color": 1
      },
      "id": "overview",
      "name": "Sticky Note overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -464,
        160
      ]
    },
    {
      "parameters": {
        "content": "## 1. Intake\nCollect the legal question and source database, and centralize configurable settings.",
        "height": 380,
        "width": 430,
        "color": 7
      },
      "id": "s1",
      "name": "Sticky Note s1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -48,
        160
      ]
    },
    {
      "parameters": {
        "content": "## 2. Retrieve authorities\nCourtListener full-text search, then the full opinion text of the top hit so the answer grounds on the holding (not just the caption). Or a CanLII case plus its citator network. Credentialed HTTP, no hardcoded keys.",
        "height": 488,
        "width": 688,
        "color": 7
      },
      "id": "s2",
      "name": "Sticky Note s2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        400,
        160
      ]
    },
    {
      "parameters": {
        "content": "## 3. Build grounding context\nBoth retrieval branches converge here, shaping the retrieved sources into one numbered block the model can cite from.",
        "height": 438,
        "width": 256,
        "color": 7
      },
      "id": "s3",
      "name": "Sticky Note s3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1104,
        160
      ]
    },
    {
      "parameters": {
        "content": "## 4. Grounded answer\nA Groq chat model answers using only the retrieved sources and returns a structured answer with citations. A structured parser with auto-fix is layer 1 of the guardrail.",
        "height": 684,
        "width": 492,
        "color": 7
      },
      "id": "s4",
      "name": "Sticky Note s4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1376,
        160
      ]
    },
    {
      "parameters": {
        "content": "## 5. Citation guardrail and result\nLayer 2: every citation is checked against the retrieved sources. Unmatched citations are dropped and the answer is marked not supported, then the cited answer is rendered and returned.",
        "height": 534,
        "width": 508,
        "color": 7
      },
      "id": "s5",
      "name": "Sticky Note s5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1888,
        160
      ]
    },
    {
      "parameters": {
        "content": "## Error handling\nIf a retrieval call fails, return a clear message instead of a broken run.",
        "height": 328,
        "width": 232,
        "color": 7
      },
      "id": "s7",
      "name": "Sticky Note s7",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1104,
        640
      ]
    }
  ],
  "connections": {
    "Submit a legal question": {
      "main": [
        [
          {
            "node": "Set research configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set research configuration": {
      "main": [
        [
          {
            "node": "Route by source database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by source database": {
      "main": [
        [
          {
            "node": "Search CourtListener case law",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch CanLII case metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search CourtListener case law": {
      "main": [
        [
          {
            "node": "Fetch CourtListener opinion text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build error result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch CanLII case metadata": {
      "main": [
        [
          {
            "node": "Fetch CanLII citator cases",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build error result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch CanLII citator cases": {
      "main": [
        [
          {
            "node": "Build grounding context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build error result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build grounding context": {
      "main": [
        [
          {
            "node": "Answer grounded in retrieved sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer grounded in retrieved sources": {
      "main": [
        [
          {
            "node": "Verify citations and render result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify citations and render result": {
      "main": [
        [
          {
            "node": "Return result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build error result": {
      "main": [
        [
          {
            "node": "Return result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Answer grounded in retrieved sources",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured citation parser": {
      "ai_outputParser": [
        [
          {
            "node": "Answer grounded in retrieved sources",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Citation parser fixer model": {
      "ai_languageModel": [
        [
          {
            "node": "Structured citation parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch CourtListener opinion text": {
      "main": [
        [
          {
            "node": "Build grounding context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build error result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}