unclecode/crawl4ai · error · ValueError
Circular include {p}
Error message
Circular include {p} What it means
ValueError from the C4A-Script compiler's _parse_with_includes pass: an INCLUDE command resolves (relative to the compile root) to a file already present in the current include chain ('seen' set), which means the include graph is cyclic. Compilation stops immediately because inlining would recurse forever.
Source
Thrown at crawl4ai/script/c4ai_script.py:350
# Handle list input by joining with newlines
if isinstance(text, list):
text = '\n'.join(text)
ir = self._parse_with_includes(text)
ir = self._collect_procs(ir)
ir = self._inline_calls(ir)
ir = self._apply_set_vars(ir)
return [self._emit_js(c) for c in ir if isinstance(c,Cmd) and c.op!="NOP"]
# passes
def _parse_with_includes(self,txt,seen=None):
seen=seen or set()
cmds=ASTBuilder().transform(self.parser.parse(txt))
out=[]
for c in cmds:
if isinstance(c,Cmd) and c.op=="INCLUDE":
p=(self.root/c.args[0]).resolve()
if p in seen: raise ValueError(f"Circular include {p}")
seen.add(p); out+=self._parse_with_includes(p.read_text(),seen)
else: out.append(c)
return out
def _collect_procs(self,ir):
out=[]
for i in ir:
if isinstance(i,Proc): self.procs[i.name]=i
else: out.append(i)
return out
def _inline_calls(self,ir):
out=[]
for c in ir:
if isinstance(c,Cmd) and c.op=="CALL":
if c.args[0] not in self.procs:
raise ValueError(f"Unknown procedure {c.args[0]!r}")
out+=self._inline_calls(self.procs[c.args[0]].body)View on GitHub (pinned to 7e80152142)
Solutions
- Break the cycle: move shared commands into a third file both scripts include, with no back-edges.
- Remove self-includes — a file never needs to include itself.
- Check for accidental identical includes of the same file twice in one chain; hoist the duplicate include to the top-level script.
- Map the include graph quickly (grep INCLUDE lines) to find the loop before editing.
Example fix
# before # a.c4a: INCLUDE "b.c4a" # b.c4a: INCLUDE "a.c4a" → ValueError: Circular include .../a.c4a # after # common.c4a: CLICK "#login"; WAIT_FOR navigation # a.c4a: INCLUDE "common.c4a" # b.c4a: INCLUDE "common.c4a"
Defensive patterns
Strategy: validation
Validate before calling
def check_includes_acyclic(entry: Path, root: Path) -> bool:
import re
def walk(p, seen):
if p in seen:
return False
seen = seen | {p}
for m in re.finditer(r'INCLUDE\s+"([^"]+)"', p.read_text()):
if not walk((root / m.group(1)).resolve(), seen):
return False
return True
return walk(entry.resolve(), set()) Try / catch
try:
js = compile_string(script, root=root)
except ValueError as e:
if 'Circular include' in str(e):
raise ValueError(f"Fix include cycle: {e}") from e Prevention
- Keep includes strictly one-directional: entry → partials → shared snippets, never back.
- Never let a file include itself or a file that (transitively) includes it.
- Lint INCLUDE graphs in CI with the acyclicity walker above.
When it happens
Trigger: Script A includes B and B includes A (directly or through a longer chain), or a script includes itself. The check is per-chain: the resolved path appearing anywhere in the active 'seen' set raises.
Common situations: Shared snippet files that include each other's helpers, refactoring where two partial scripts were made mutually inclusive, a file including itself as a default-header habit, or symlinks/paths that resolve to the same file via different spellings.
Related errors
- Unknown procedure {c.args[0]!r}
- C4A Script compilation error (script {i+1}): Line {error.l
- LLM returned empty script.
- Unknown procedure {cmd.args[0]!r}
- Container not found: ${config.container_selector}
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/4f46f7c38af61702.
Report an issue: GitHub.