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 loadcwd(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:
pathargument if provided<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