Script Metadata
A script can declare what it needs to run — a minimum scriptling version, the libraries it imports, the plugins it expects to be connected — in a TOML block carried in comments before its first statement. The CLI checks the block before the script executes, so a missing requirement is a clear error instead of an import failure three layers in. The format follows the shape of PEP 723 (inline script metadata) with scriptling’s own keys.
# /// script
# requires-scriptling = ">=0.24"
#
# dependencies = [
# "requests",
# "scriptling.sql via sql >= 0.23",
# ]
#
# plugins = [
# "knot >= 1.2.3",
# ]
# ///
import requests
import scriptling.sql as sqlA script without a block runs exactly as before; the block is opt-in.
Keys
| Key | Meaning |
|---|---|
requires-scriptling |
Version constraint matched against the running scriptling version, e.g. ">=0.24". Operators: >=, <=, >, <, ==, !=. |
dependencies |
Libraries the script imports. Each entry is a library name, optionally followed by via and the plugin that provides it — "scriptling.sql via sql >= 0.23" — using the same optional version constraint as the plugins list. |
plugins |
External plugin processes that must be loaded, each "name" with an optional version constraint: "knot >= 1.2.3". Constraints are matched against the version each plugin declared in its handshake. |
[tool.<name>] |
Reserved for tool and host configuration. Accepted and ignored by scriptling. |
Unknown keys are errors, not warnings — a typo like dependencys should fail loudly. Versions and constraints are dotted numeric (0.24, 1.2.3).
Plugin names match the name a plugin declared in its handshake, and a bare name additionally matches the same name under scriptling’s host-owned namespaces: sql finds the first-party plugin that declares scriptling.sql, and hello finds a plugin that declared the bare name hello (registered as plugin.hello). knot matches only knot — bare names never match third-party dotted namespaces like knot.space. The first-party database plugins declare scriptling’s build version, so a constraint like sql >= 0.23.0 pins the scriptling release the plugin was built with.
Dependencies or plugins?
The distinction is where the library can come from:
| Declaration | Satisfied by | Missing error says |
|---|---|---|
"requests" |
Any provider: built-in, registered library, package module | library is not available |
"scriptling.sql via sql" |
The library resolving — compiled into the default build, or the sql plugin loaded | load the plugin, or use a build with it compiled in |
"knot >= 1.2.3" (in plugins) |
The knot plugin process, at that version | plugin not loaded, or version too old |
Rules of thumb: if it can be compiled in, declare it as a dependency and name its plugin — the entry passes on the default build without the plugin, and on scriptling-slim with the plugin loaded. If it only exists as a connected plugin, declare it in plugins — that is also where version constraints matter, because a plugin like knot versions independently, while the database plugins version with scriptling itself (pin them with requires-scriptling).
The via clause takes the plugin name with an optional constraint, exactly like a plugins entry: "knot.space via knot >= 1.2". One nuance: when the library resolves, the via clause is never consulted — the constraint only bites when the plugin is loaded but the library still did not resolve.
How checking works
Dependencies are checked first, by resolution — can this environment actually import the name, taking into account registered libraries, built-in modules, and --package bundles? A dependency that resolves is satisfied however the environment provides it. Only an unresolved dependency promotes its declared plugin into the required set; plugins declared directly are always required. One aggregated error then reports everything at once:
Error: script requirements not met:
- this script needs scriptling >=0.24, but this host is 0.23.1
- required library "requests" is not available in this environment
- required plugin "knot" is not loaded
load plugins with --plugin <path>, --plugin-dir, or SCRIPTLING_PLUGIN_DIR--lint validates the block itself — malformed TOML, unknown keys, bad constraints — with file and line, without checking requirements, which depend on the environment rather than the source.
Rules
- At most one block per script, located before the first statement.
- Every line inside the block is a comment; the block must be closed with
# ///before any code. - A malformed block is a hard error: a script that tried to declare requirements never runs as if it had none.
- Blocks are checked wherever a named script runs: one-shot script files, fetched
scheme://scripts,--code, server setup scripts (HTTP, JSON-RPC, and MCP stdio — verified once at startup, before anything binds), and package main entries — a.pyentry directly, amodule.functionentry through the module’s source. Interactive stdin has no check. - Imported modules, webroot and route handler scripts, and MCP tool handler scripts are not checked: the script you name is the unit of requirements.
Embedding hosts
The check lives in the embeddable metadata package; the CLI is just one consumer. Hosts embedding scriptling parse the script source and verify against their own environment — the full guide, including what to pass as the host version and how to feed the resolver from your loaders, is Checking Script Requirements. The shape of it:
m, ok, err := metadata.Parse(source)
if err != nil {
return err // malformed block
}
if ok {
err = m.Verify(metadata.Env{
HostVersion: appVersion, // the host's version, not scriptling's
Resolves: func(name string) bool { return interp.HasLibrary(name) },
PluginVersion: func(name string) (string, bool) {
for _, md := range manager.List() {
if md.Name == name {
return md.Version, true
}
}
return "", false
},
})
}Verify returns one aggregated error (*metadata.CheckError) whose failures carry kinds — version, library, plugin — so a host can attach its own remedy hints, the way the CLI appends how to load plugins.