Skip to content
Built by Postindustria. We help teams build agentic production systems.

Static Linting (lint)

lint() walks every node in a Construct and verifies that each FromInput/FromConfig parameter has a matching key in the provided config dict. It never raises — it returns a list of LintIssue instances describing every binding problem it finds.

from neograph import lint, LintIssue
issues = lint(pipeline, config={"node_id": "test", "project_root": "/tmp"})
for issue in issues:
print(f"[{issue.kind}] {issue.message}")
ParameterTypeDescription
constructConstructThe pipeline to check
configdict[str, Any] | NoneConfig dict to validate DI bindings against. When None, only structural checks run (required params are flagged as missing since no config is available).

A list of LintIssue instances. An empty list means all bindings are satisfied.

@dataclass
class LintIssue:
node_name: str # which node has the problem
param: str # which parameter is unresolved
kind: str # "from_input", "from_config", "from_input_model", "from_config_model"
message: str # human-readable description
required: bool # True if the parameter has no default

The kind field tells you where the parameter expected to be resolved from:

KindMeaning
from_inputAnnotated[T, FromInput] — resolved from config["configurable"], originally from run(input={...})
from_configAnnotated[T, FromConfig] — resolved from config["configurable"], passed directly in config=
from_input_modelBundled BaseModel via FromInput — each model field must exist in config
from_config_modelBundled BaseModel via FromConfig — each model field must exist in config

For every node with FromInput or FromConfig parameters, lint() checks that the parameter name exists as a key in the config dict:

@node(output=Result)
def process(
upstream: Claims,
topic: Annotated[str, FromInput], # lint checks: "topic" in config
limiter: Annotated[RateLimiter, FromConfig], # lint checks: "limiter" in config
) -> Result: ...

When a DI parameter uses a BaseModel subclass, the resolver bundles — it constructs an instance by pulling each model field from config. lint() checks every field individually:

class RunCtx(BaseModel):
node_id: str
project_root: str
@node(output=Result)
def process(
upstream: Claims,
ctx: Annotated[RunCtx, FromInput], # lint checks: "node_id" AND "project_root" in config
) -> Result: ...

For nodes with an Oracle modifier whose merge_fn is a @merge_fn-decorated function, lint() also checks the merge function’s DI parameters:

@merge_fn
def weighted_merge(
candidates: list[Result],
weights: Annotated[list[float], FromConfig], # lint checks: "weights" in config
) -> Result: ...
from typing import Annotated
from pydantic import BaseModel
from neograph import node, compile, lint, FromInput, FromConfig
class Claims(BaseModel):
items: list[str]
class Result(BaseModel):
summary: str
@node(output=Result)
def summarize(
claims: Claims,
topic: Annotated[str, FromInput],
max_len: Annotated[int, FromConfig],
) -> Result:
return Result(summary=f"{topic}: {len(claims.items)} claims (max {max_len})")
# Check with a complete config -- no issues
issues = lint(pipeline, config={"topic": "AI safety", "max_len": 500})
assert issues == []
# Check with a missing key -- lint reports the gap
issues = lint(pipeline, config={"topic": "AI safety"})
assert len(issues) == 1
assert issues[0].param == "max_len"
assert issues[0].kind == "from_config"

lint() runs automatically as part of the neograph check CLI. See Pipeline Validation for details on the --config and --setup flags that supply config to the lint pass.


Documentation © 2025-2026 Constantine Mirin, mirin.pro. Licensed under CC BY-ND 4.0.