For agents: Skip to article. Documentation index. Read this page as Markdown.
Scull
Reference

Project configuration

Configure project defaults, providers, model allowlists, and signal mappings.

Read as Markdown

scull.yaml is the recommended policy surface for a Scull project. It keeps source metadata mappings, OpenAPI normalization, and extraction-model rules consistent across local runs and CI. The file is auto-discovered at the project root; the same resolved configuration is also available through the Python API.

Scull can run with built-in defaults when the file is absent, which is useful for a first trial. For a maintained schemaset, commit the file so build decisions are explicit and reviewable.

Quick Start

Declarative (scull.yaml)

metadata_mappings:
  source_url: [source-of-truth, custom_edit_url, canonical_url]
  last_verified: [last-reviewed, verified_date]
  owner: [owner, maintainer, author]

defaults:
  model_api_base: https://inference.baseten.co/v1

models:
  schemafier:
    allowed:
      - baseten/thinkingmachines/inkling-small
      - baseten/deepseek-ai/DeepSeek-V4-Flash-0731
  eval:
    allowed:
      - baseten/zai-org/GLM-5.3-Flash
      - openai/gpt-4o

oas_normalize:
  rules:
    - match: "(.*)DetailRecord$"
      rename: "\\1.Detail"

Programmatic API

from pathlib import Path
from scull.config import load, ScullConfig

# Auto-discover from cwd (or pass explicit path)
config = load()

# Or load from a specific file
config = load(path=Path("/custom/path/scull.yaml"))

# Access fields
print(config.metadata_mappings["source_url"])
# → ["source-of-truth", "custom_edit_url", "canonical_url"]

print(config.defaults.get("model"))
# → "anthropic/claude-haiku-4-5"

# Check if a model is allowed for an activity
try:
    config.check_model_allowed("schemafier", "baseten/zai-org/GLM-5.3-Flash")
except ValueError as e:
    print(e)
    # Model 'baseten/zai-org/GLM-5.3-Flash' is not in the allowed list
    # for 'schemafier' in scull.yaml (models.schemafier.allowed): ...

API Reference

load(path=None, cwd=None) → ScullConfig

Load scull.yaml. Returns built-in defaults if the file doesn't exist — safe to call without a config file.

Parameters:

  • path (Path, optional) — Explicit config file path to load
  • cwd (Path, optional) — Working directory to search from (default: Path.cwd())

Returns: A ScullConfig instance with all fields populated (defaults if no file found).

Search order:

  1. path argument if provided
  2. <cwd>/scull.yaml (default: ./scull.yaml)

class ScullConfig

Resolved project configuration. All fields have safe defaults.

Fields

Field Type Default Purpose
metadata_mappings Dict[str, List[str]] Built-in signal mappings Maps frontmatter keys → internal signals (source_url, last_verified, etc.)
defaults Dict[str, str] {} Raw defaults block; supported commands read documented keys such as model_api_base
models Dict[str, Dict] {} Activity-scoped model allowlists ({"schemafier": {"allowed": [...]}})
oas_normalize Dict[str, Any] {} Ordered regex rules for normalizing OAS schema names before field IDs are emitted
source_path Optional[Path] None The path the config was loaded from (None = using built-in defaults)

Methods

allowed_models(activity: str) → Optional[List[str]]

Get the allowed model list for an activity, or None if unrestricted.

# Returns None if no allowlist configured (unrestricted)
allowed = config.allowed_models("unknown_activity")

# Returns list of strings if allowlist exists
allowed = config.allowed_models("schemafier")
# → ["baseten/thinkingmachines/inkling-small", "baseten/deepseek-ai/DeepSeek-V4-Flash-0731"]
check_model_allowed(activity: str, model: str) → None

Raise ValueError if model isn't in the configured allowlist for activity. No-op if no allowlist is configured.

config.check_model_allowed("eval", "openai/gpt-4o")   # OK — in allowlist
config.check_model_allowed("eval", "gpt-5-chaos")     # Raises ValueError

Typical usage: call before invoking scull-schemafier or eval agents to enforce policy.

def run_schemafier(model: str):
    config.check_model_allowed("schemafier", model)
    # Now safe to call the LLM extraction pipeline

Signal Mappings

When prose files have YAML frontmatter, signals are extracted by trying each aliased key in order (first match wins). These are stored in the schema's sources block for traceability.

Built-in Signals

The following signals are recognized even without a scull.yaml:

Signal Recognized Aliases Description
source_url source-of-truth, custom_edit_url, canonical_url, permalink Canonical URL for the doc page
last_verified last-reviewed, verified_date When the content was last reviewed
verified_against verified-against, api_version, version Version/API this doc covers
review_interval review-interval, review_cadence How often to re-verify (e.g., "monthly")
owner owner, maintainer, author Responsible team/person
related related, see-also, references, cross-references Links to related concepts/docs

Customizing Mappings

Project-declared mappings fully override built-in mappings per signal (not merged). For example, to replace the source_url aliases entirely:

metadata_mappings:
  source_url: [my-url-key, alternate-key]

This replaces all four built-in aliases with just two custom ones. To add to existing aliases instead of replacing them, keep all original keys plus any new ones.

Defaults Block

Any key-value pair in the defaults block is exposed through config.defaults.get(key). A key is not automatically a CLI default merely because it appears in this mapping. The extraction command currently consumes this documented key:

Key Usage Example
model_api_base Default API base URL https://inference.baseten.co/v1

It can be overridden by --model-api-base or SCULL_MODEL_API_BASE. The extraction model itself comes from --model or SCULL_MODEL; schema and index locations come from their command options and query-layer path discovery.

OpenAPI name normalization

oas_normalize.rules applies ordered regular-expression renames before OpenAPI schema names become field IDs and structure keys:

oas_normalize:
  rules:
    - match: "(.*)DetailRecord$"
      rename: "\\1.Detail"

Each name uses the first matching rule. With no rules, source schema names pass through unchanged.

Model Allowlists

Define which models are permitted for each activity. Use in CI to prevent accidental usage of expensive or experimental models.

models:
  schemafier:    # LLM extraction pipeline
    allowed:
      - baseten/thinkingmachines/inkling-small    # example extraction model
      - baseten/deepseek-ai/DeepSeek-V4-Flash-0731  # fallback
    fallback_chain:
      - baseten/deepseek-ai/DeepSeek-V4-Flash-0731
  eval:          # Eval agent runner
    allowed:
      - anthropic/claude-haiku-4-5                # cheap eval default
      - baseten/zai-org/GLM-5.3-Flash             # fast non-thinking
  benchmark:     # Multi-model comparison runs
    allowed:
      - baseten/zai-org/GLM-5.3-Flash
      - anthropic/claude-haiku-4-5
      - baseten/deepseek-ai/DeepSeek-V4-Flash-0731

Activities not listed here have no restrictions. The scull-schemafier validates every model in the effective primary plus fallback chain before making an extraction call. This means a fallback must also appear in models.schemafier.allowed; an allowed primary does not make an unlisted fallback allowed. Call config.check_model_allowed() in your own model-calling code to enforce the same policy.

Extraction requests use LiteLLM's request-scoped unsupported-parameter handling. This lets provider-specific models such as configured OpenAI GPT-5 variants omit parameters they reject (including temperature=0) while preserving the supported JSON response format used by extraction. Supported primary models still receive their supported parameters, and the setting is not applied globally to the LiteLLM process. Fallback model IDs remain the exact strings in fallback_chain. If an endpoint still explicitly rejects temperature, extraction retries once without that parameter; unrelated errors continue through the existing fallback chain. The initial prompt explicitly requests JSON, and candidate validation and the four response-format attempts remain in place.

import sys
from scull.config import load

config = load()

model = sys.argv[1]  # e.g., "gpt-5-ultra-expensive"
config.check_model_allowed("benchmark", model)  # Fails before calling LLM