script

package
v1.125.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package script is the managed-script domain: the live script record, its immutable version history, the typed parameter contract, and the single edit funnel every mutation surface crosses.

A managed script is agent-authored Starlark that the platform stores, versions, and governs so a solved process (a KPI report, a daily export) can be re-run without re-deriving it through a model. The package holds the model and the rules; it executes nothing and knows nothing about Starlark. The engine lives in internal/platform/scriptrun and the MCP surface in internal/platform/scriptlayer, so the domain stays free of both.

The governance shape deliberately copies pkg/prompt — live row plus immutable per-mutation snapshots, one ApplyEdit funnel — rather than genericizing it. The rules are domain-tuned, and abstracting across two domains would fix the wrong shape.

Index

Constants

View Source
const (
	// OutputKindAsset is a portal asset the platform versions and serves.
	OutputKindAsset = "portal_asset"
	// OutputKindObject is an object delivered to a configured bucket
	// destination, which the platform wrote and does not hold.
	OutputKindObject = "object"
)

Output kinds. An output has two shapes since external delivery (#1288), and the payload names which one it is rather than leaving a caller to infer it from which fields happen to be populated.

View Source
const (
	// DestinationKindPortal versions a portal asset.
	DestinationKindPortal = "portal"

	// DestinationKindS3 writes an object to a bucket over a named platform S3
	// connection. It is the only way an output leaves the platform.
	DestinationKindS3 = "s3"
)

Destination kinds. The kind decides what the platform does with the bytes, and the set is closed: a destination is a place the platform implements a write for, not an open transport.

View Source
const (
	// MaxDisplayNameLen bounds a script's display name, in runes, at the limit
	// its siblings use (resource.MaxDisplayNameLen). Unlike the description
	// there is no reading on which a label wants to be long: it is what the
	// listing, the page header and a search result print, and every one of them
	// truncates. An empty display name is allowed and falls back to the name an
	// agent calls the script by (Title).
	MaxDisplayNameLen = 200

	// MaxDescriptionBytes is the ceiling a description is refused above, and it
	// is a structural limit rather than an editorial one: it exists to protect
	// the row, the version history, and the search vector.
	//
	// The vector is the binding constraint. script_fts is composed from the
	// description together with the title, the category, the tags and the
	// parameter contract, and it is built into a GIN index expression
	// (migration 000102), so to_tsvector runs on every write. PostgreSQL
	// refuses a tsvector input over 1 MiB, and an index expression that raises
	// makes the row unwritable rather than merely unfindable — the description
	// would take the script down with it. 64 KiB leaves the composed document
	// an order of magnitude inside that limit even with every other indexed
	// field at its own maximum, and it is roughly twenty pages of markdown,
	// which is far past any honest documentation of one script.
	MaxDescriptionBytes = 64 * 1024
)

The rules for the three fields that explain a script to a person: its display name, its description, and the category it is filed under (#1369).

A managed script is complex logic that outlives the conversation that produced it, so its description is a DOCUMENT rather than a caption: markdown an author writes at whatever length the automation actually needs explaining at. That is why the bounds here are not the ones an asset or a resource puts on its own description. Those are caption bounds — 2000 characters, about half a page — and capping a script description there would cap the thing this documentation exists to hold.

View Source
const (
	ParamTypeString = "string"
	ParamTypeInt    = "int"
	ParamTypeFloat  = "float"
	ParamTypeBool   = "bool"
	ParamTypeDate   = "date"
	ParamTypeEnum   = "enum"

	// ParamTypeConnection is a connection name (#1361). It binds as a string,
	// and it is a type of its own because the platform knows the whole set of
	// values it can take. That is the difference from a string: a surface
	// asking for one can OFFER the connections the caller's persona reaches
	// instead of asking somebody to remember the spelling, and a value outside
	// that set is refused by the middleware at the query it names.
	ParamTypeConnection = "connection"
)

Parameter type names. The set is deliberately closed and stricter than prompt.Argument's untyped strings: a script's parameters are bound once at run creation and then frozen into run.params, so a value that reaches the interpreter has already been checked. Anything richer than a scalar belongs in the script's own logic, not in its signature.

View Source
const (
	RunStatusPending        = "pending"
	RunStatusRunning        = "running"
	RunStatusSucceeded      = "succeeded"
	RunStatusFailed         = "failed"
	RunStatusSkippedOverlap = "skipped_overlap"
)

Run lifecycle statuses.

  • pending: enqueued, waiting for a worker to claim it.
  • running: a worker holds the lease and the interpreter is executing.
  • succeeded / failed: terminal. A failed run carries the reason in Error and, when the script itself failed, the Starlark backtrace with it.
  • skipped_overlap: terminal, and never executed. A schedule came due while the previous run of the same schedule was still open, and the overlap policy is to skip. It is recorded as a run rather than logged because a report that stopped producing is precisely what a schedule's history has to show; a skip is not an outage, but it is not a run either.
View Source
const (
	// TriggerTool marks a run requested through the run_script tool.
	TriggerTool = "tool"
	// TriggerSchedule marks a run materialized by a script's schedule. The two
	// triggers produce identical rows and execute through the same worker
	// under the script's own principal; what differs is that nobody is waiting
	// on this one, which is why a failed scheduled run notifies and a failed
	// tool run answers its caller.
	TriggerSchedule = "schedule"
	// TriggerPortal marks a run an owner asked for on the script's own page
	// (#1363). It executes exactly as the other two do — same worker, same
	// principal, same audit — and it is a distinct label because the run history is
	// read by the person who clicked it, and recording their own click as an
	// agent's tool call is a false statement about who did what.
	TriggerPortal = "portal"
)

Run triggers: what produced the run row.

View Source
const (
	// MaxCronSpecLength bounds a cron expression. A standard five-field spec
	// and every descriptor form fit inside it many times over; the cap keeps a
	// pathological string out of the parser and the review surface.
	MaxCronSpecLength = 200

	// MinFireInterval is the closest together two fires of one schedule may
	// be. Standard cron cannot express anything finer than a minute, so this
	// only ever bites the @every descriptor — where "@every 5s" would turn a
	// governed automation into a load generator against the query engine it
	// reaches.
	MinFireInterval = time.Minute

	// DefaultTimezone is the zone a schedule is interpreted in when it names
	// none. UTC rather than the host's local zone: a schedule means the same
	// thing on every replica, and a deployment that moves regions does not
	// silently move its reports.
	DefaultTimezone = "UTC"
)

Schedule bounds.

View Source
const (
	StatusActive     = "active"
	StatusDeprecated = "deprecated"
	StatusSuperseded = "superseded"
)

Status constants define the script lifecycle.

  • active: in service. A saved script runs — run_script executes its latest saved version and a schedule fires it. Every script starts here.
  • deprecated: still readable and still explains past runs, but no longer executed.
  • superseded: replaced by another script, named in SupersededBy.
View Source
const (
	VersionStatusApplied    = "applied"
	VersionStatusSuperseded = "superseded"
)

Version status constants. A version row is the immutable snapshot of a script's substance at one mutation:

  • applied: the snapshot was applied to the live row. Every save produces one, and the newest applied version is the version a run executes.
  • superseded: a historical row that was never applied — a proposed edit from the era in which some edits waited on a review that no longer exists. Nothing writes it today; it is kept so old history reads true. Rows from that era may also carry "rejected", a proposal a reviewer declined, left as the decision it records.
View Source
const ConnectionParamKind = "trino"

ConnectionParamKind is the toolkit kind a connection-typed parameter's value names.

A connection is identified by kind and name together, and a deployment may legitimately carry one name across several kinds (#1384). The kind is a property of the binding the value is passed to. The one host binding that takes a connection is platform.query, which runs its statement through the Trino toolkit (internal/platform/scriptrun/host.go calls trino_query), so the connection a run reaches under a given name is the Trino one, whatever else carries that name. platform.export names a configured destination rather than a connection, so it does not widen this.

Adding a binding that takes a connection of another kind means this stops being one constant, and the picker route that reads it is where that would show first.

View Source
const DataRegionSelector = "#data"

DataRegionSelector is the CSS selector the marked data region must answer to: exactly one element carrying id="data". By convention that element is

<script type="application/json" id="data">...</script>

so the island never renders as visible content, but the contract is the id, not the tag: the platform replaces the interior of whatever single element carries it. A document with no match, or more than one, refuses the publish rather than writing anywhere else.

View Source
const DateLayout = "2006-01-02"

DateLayout is the one accepted wire form for a date parameter and the form every date-module function reads and returns. One layout, everywhere: a script that computes a report date and a schedule that binds one are then talking about the same strings.

View Source
const (
	DefaultSearchLimit = 20
)

Search result limits. DefaultSearchLimit is the top-K returned when the caller names no limit; maxSearchLimit bounds an explicit request so one ranked query cannot ask for an unbounded result set. They match the prompt library's, so a federated search cannot be skewed by one source quietly returning more candidates than another.

View Source
const DestinationPortal = "portal"

DestinationPortal is the name of the portal destination: an asset owned by the platform, versioned by the platform, and reachable only through it. It is where an output lands when a script names no destination, which keeps the common case — a scheduled report refreshing the asset people already read — the shortest thing to write.

View Source
const FireDateToken = "${fire_date}" // #nosec G101 -- a parameter token, not a credential

FireDateToken is the one token a schedule's bound parameters may carry. It expands, at materialization, to the date of the fire in the schedule's own timezone.

The vocabulary is deliberately one entry long. Its whole job is to pin time-dependence into the run record: a script that computed today's date itself would produce a different answer every time it ran, and a run nobody can reproduce is not a governed run. Everything else a date needs — a previous day, a month boundary — is arithmetic the script does on this value through the date module, where it is visible in the source a reviewer read.

View Source
const (

	// MaxSourceBytes bounds the Starlark source of one script. Scripts are glue
	// — a few hundred lines that call the platform and shape the result — and
	// heavy computation belongs in SQL, not in the interpreter. The cap keeps a
	// pathological body out of the parser, the version history, and the review
	// surface a human is expected to actually read.
	MaxSourceBytes = 256 * 1024
)

Bounds on the free-text fields of a script record, matched to the equivalent limits on prompts and assets so tag and name input is uniformly bounded.

View Source
const PrincipalPrefix = "script:"

PrincipalPrefix marks a user id belonging to a managed script rather than a person, following the apikey:<name> service-principal convention.

Variables

View Source
var (
	// ErrNoWork reports that no run was due for a claiming worker. It is the
	// normal idle outcome, not a failure.
	ErrNoWork = errors.New("no script run is due")

	// ErrLeaseLost reports that a worker tried to write to a run it no longer
	// holds, because its lease expired and another worker reclaimed the run.
	// The write is refused rather than applied: a process that lost its lease
	// is, by definition, no longer the one whose result counts.
	ErrLeaseLost = errors.New("the lease on this script run was lost")

	// ErrRunNotFound reports a lookup for a run id that does not exist.
	ErrRunNotFound = errors.New("script run not found")
)

Run queue and lifecycle errors.

View Source
var (
	// ErrScheduleNotFound reports a lookup for a schedule that does not exist.
	ErrScheduleNotFound = errors.New("this script has no schedule")

	// ErrUnknownToken marks a parameter binding that carries a token the
	// vocabulary does not define.
	ErrUnknownToken = errors.New("unknown schedule token")

	// ErrUnknownTimezone marks a zone the runtime could not load. It is a
	// separate sentinel from a bad cron expression because the two have
	// opposite causes: an expression that will not parse is a property of the
	// schedule, while a zone that will not load is a property of the BINARY —
	// the database is compiled in (time/tzdata) and a build that omits it fails
	// every named zone at once. A caller must not treat the second as a reason
	// to change the schedule.
	ErrUnknownTimezone = errors.New("unknown timezone")
)

Schedule errors.

DestinationKinds is the full set of destination kinds.

View Source
var ErrNameTaken = errors.New("the new owner already has a script with this name")

ErrNameTaken marks a transfer refused because the receiving owner already keeps a script under that name. Names are unique within an owner, so a transfer can fail on the receiving side alone; surfaces map it to a conflict rather than to an internal failure.

View Source
var ErrUnknownParam = errors.New("unknown parameter")

ErrUnknownParam marks a bind rejected because the caller supplied a name the script does not declare. It is a distinct error because the corrective action differs from a bad value: the caller is passing something the script will never read, which is nearly always a typo.

View Source
var ErrVersionConflict = errors.New("script version conflict")

ErrVersionConflict marks a version write rejected because the script or version state changed underneath the caller. REST handlers map it to 409; any other store error is an internal failure.

Functions

func ApplyEdit

func ApplyEdit(ctx context.Context, store Store, e Edit) error

ApplyEdit lands a script edit through the one shared gate every mutation surface crosses: the manage_script tool and the portal editor.

Every edit is applied to the live row. A store with versioning records a new applied version when a versioned field changed, so the saved script and the script a run executes are always the same code; a store without it degrades to a plain unversioned update.

func BindParams

func BindParams(defs []Param, values map[string]any) (map[string]any, error)

BindParams checks caller-supplied values against the declared contract and returns the bound set: every declared parameter present exactly once, with defaults applied and each value coerced to its declared type. Undeclared names are refused rather than passed through, so a typo never reaches the script as a silently ignored argument.

The result is what becomes the frozen run.params dict, which is why binding happens here — in the domain, once, before any interpreter is involved — rather than inside the engine.

func BindScheduleParams

func BindScheduleParams(defs []Param, raw map[string]any, fire time.Time, loc *time.Location) (map[string]any, error)

BindScheduleParams expands the schedule's tokens against a fire time and binds the result to the script's parameter contract.

Expansion happens here, at materialization, and the expanded values are what the run row stores. That is what makes a scheduled run reproducible: the run records the date it was computing for, so re-running it later with the same parameters asks the same question, rather than asking about whatever day the re-run happens on.

func DescriptionNotice added in v1.122.0

func DescriptionNotice(desc string) string

DescriptionNotice is the non-blocking signal that a description has outgrown the script it documents, and empty when it has not.

It is advisory by construction, which is the whole point of it. The platform wants long descriptions — that is what makes a stored automation understandable months later by somebody who did not write it — so the only hard refusal is the structural one (MaxDescriptionBytes), and everything short of it succeeds. This says what the author might do instead; it never decides for them.

The signal is size alone, unlike the knowledge page's, which also counts headings. A page with twelve top-level sections is covering several topics and wants splitting; a script description with twelve sections is still about one script, and refusing to distinguish the two would put a nudge in front of an author who is documenting thoroughly and correctly.

func DraftSource added in v1.123.1

func DraftSource(sent string, sc *Script) string

DraftSource resolves the code a draft acts on: the edit when one was sent, and the stored version otherwise.

It is the domain's rule rather than each surface's because the two surfaces that ask for a draft — manage_script and the portal editor — disagreed about it: the tool arm passed the stored version and ignored the source it was given, so an author iterating over MCP read a log produced by code they had not submitted (#1413). Executing an unsaved edit is the whole purpose of a draft, and sending no source is how a script nobody has edited is dry-run.

func ExecutionNote added in v1.122.0

func ExecutionNote(s *Script) string

ExecutionNote states a script's execution state in one sentence.

func IndexText added in v1.122.0

func IndexText(s *Script) string

IndexText composes the text a script is embedded on and shown as in a search result: its title (display name, falling back to the name an agent calls it by), its description, the names of the parameters a run binds, the category it is filed under and its tags, and one line stating whether anything will execute it. Empty parts are skipped so a sparse script does not pad the text with blank lines.

The execution note is part of the document rather than decoration: it changes what the script IS FOR. A script in service is something to run; a disabled or retired one is not, and reading a result should not leave that ambiguous.

The source code is deliberately absent. docs/scripts/security.md admits the contract to anyone the scope rules admit and the source only to the owner and to administrators; one vector per row cannot be split along that line, so a vector built partly from source would let code a caller may not read decide how their results rank. The source also churns on every code edit while a description changes rarely, so indexing it would re-embed the corpus for changes that do not alter what the script is for.

The indexjobs scripts consumer and the discovery source MUST agree on this composition — a stored embedding has to live in the same space as the text a caller is shown — so it is defined once here for both.

func ParamSummary

func ParamSummary(params []Param) string

ParamSummary renders a parameter contract as a comma-separated name list, marking the required ones, so a caller learns what a script needs without reading the typed contract field by field. Empty for a script that takes no parameters.

func ParamsEqual

func ParamsEqual(a, b []Param) bool

ParamsEqual reports whether two parameter contracts are identical. Param carries an untyped Default and a value list, so it is not a comparable type and slices.Equal cannot be used on a []Param; this is the one definition of parameter-contract equality, shared by the edit funnel and the diff surface.

func RefuseDraftRun added in v1.122.0

func RefuseDraftRun(sc *Script) error

RefuseDraftRun reports why a draft run of this script would be refused, or nil when one would be admitted.

It is deliberately not RefuseRun: a draft executes as its author, inline, while they iterate — so a deprecated script may still be draft-run by the person fixing it, and the refusals speak to an author rather than a caller.

func RefuseRun

func RefuseRun(sc *Script) error

RefuseRun reports why the platform must not execute this script, or nil when a run is admitted.

It lives in the domain because it is the one rule every path into execution answers to — run_script, the scheduler, and the worker at claim time — so a second producer of runs cannot arrive with a second, slightly different idea of what is executable. It is checked when a run is executed, not only when it is queued, because between the two a script can be disabled or retired.

func SnapshotChanged

func SnapshotChanged(before, after *Script) bool

SnapshotChanged reports whether any versioned snapshot field (source, params, display name, description, category, tags) differs between the two states.

func SourceDigest added in v1.122.0

func SourceDigest(source string) []byte

SourceDigest is the digest a dry-run account is keyed by. One definition, used by the writer that records a run and by the reader that matches a version against it, so the two cannot disagree about what "the same source" means.

func Title added in v1.122.0

func Title(s *Script) string

Title renders a script's human label: its display name, falling back to the name an agent would call it by.

func ValidateDeclaredDestinations added in v1.123.0

func ValidateDeclaredDestinations(destinations []Destination) error

ValidateDeclaredDestinations checks the destination set a deployment declares in configuration: each must be a complete bucket address, one name is one place, and the built-in portal cannot be redeclared.

func ValidateName

func ValidateName(name string) error

ValidateName checks that a script name is well-formed.

func ValidateObjectKey

func ValidateObjectKey(key string) error

ValidateObjectKey checks a relative object key: the configured prefix of a destination, or the key a script writes beneath it.

The rules refuse rather than rewrite. A key is part of the contract between a script and whatever consumes its output, so silently rewriting one would mean the object landing somewhere the source does not say — and a traversal that was cleaned away rather than reported is a refusal nobody was told about.

func ValidateParams

func ValidateParams(params []Param) error

ValidateParams checks a parameter list is a well-formed contract: known types, unique identifier-shaped names, enums that enumerate something, and defaults that satisfy their own declared type.

func ValidateSource

func ValidateSource(src string) error

ValidateSource checks that Starlark source is present and within bounds. It is a size and presence check only; parsing is the engine's job.

func ValidateStatus

func ValidateStatus(status string) error

ValidateStatus checks that a status value is recognized.

func ValidateStatusTransition

func ValidateStatusTransition(from, to string) error

ValidateStatusTransition checks whether a status transition is allowed.

func ValidateTags

func ValidateTags(tags []string) error

ValidateTags checks that a script's tag list is within bounds.

Types

type Author

type Author struct {
	// Email identifies the person recorded on the version.
	Email string
	// Roles is the authority they held when they wrote it.
	Roles []string
}

Author is who produced a version and the authority they held while doing it.

The roles half is what makes an unattended run honest. The middleware resolves a caller's persona from their roles, and a platform-executed script runs with nobody present — no token, no session, no live identity to resolve — so the authority it presents has to have been captured at some earlier moment. Capturing it from the AUTHOR, at the moment they saved the version, means a run can only ever do what the person who wrote that code could already do.

type Contract

type Contract struct {
	ID          string   `json:"id" example:"script_a1b2c3d4"`
	Name        string   `json:"name" example:"daily-sales-report"`
	DisplayName string   `json:"display_name,omitempty" example:"Daily Sales Report"`
	Description string   `json:"description,omitempty" example:"Summarize yesterday's sales by region"`
	OwnerEmail  string   `json:"owner_email,omitempty" example:"jane@example.com"`
	Category    string   `json:"category,omitempty" example:"reporting"`
	Tags        []string `json:"tags,omitempty" example:"sales,reporting"`
	Status      string   `json:"status" example:"active"`
	Enabled     bool     `json:"enabled" example:"true"`

	// Params is the typed parameter contract a run binds against: the live
	// record's, which is the latest saved version's.
	Params []Param `json:"params"`

	// Version is the version a run executes: the latest saved one.
	Version int `json:"version" example:"3"`

	// Refusal states why a run requested now would be refused, and is empty
	// when one would be admitted.
	Refusal string `json:"refusal,omitempty" example:"the script is disabled"`

	// Schedule is the cadence this script fires on, nil when it has none.
	Schedule *ContractSchedule `json:"schedule,omitempty"`

	// LastRun is the most recent successful run, nil when the script has never
	// completed one. It answers "what does this produce" with evidence rather
	// than with a promise.
	LastRun *ContractRun `json:"last_successful_run,omitempty"`
}

Contract is what a reference to a script resolves to: everything a caller needs to answer "what is this, may I use it, and what does it produce" without reading a line of Starlark.

It exists as one type because a script is reachable from more than one surface — fetch on an mcp:script:<id> reference, and a prompt that attaches one (#1302, #1289) — and each surface answering the question in its own shape would be two contracts to keep in step. The source is deliberately not part of it: reading the code is what manage_script's get is for, and what a reviewer does.

One field deserves its reasoning stated. Refusal is the run gate's own answer (RefuseRun), not a second reading of it, so a caller is never told a script is runnable that run_script would then decline — a disabled or deprecated script refuses a run whatever its history says.

func BuildContract

func BuildContract(sc *Script, sched *Schedule, lastRun *Run) Contract

BuildContract renders the contract for one script from the records that define it: the live row, the schedule (nil when it has none), and the last successful run (nil when it has never had one).

It is a pure function over records the caller has already read, so every surface that resolves a script reference produces the identical document and none of them needs its own composition rule.

func (Contract) OwnedBy added in v1.123.0

func (c Contract) OwnedBy(email string) bool

OwnedBy reports whether the named caller owns the script this contract describes. It answers through the same rule the record and the store predicate answer through, so a surface holding only the contract — the fetch path, which composes it in one read — enforces the identical visibility without a second read of the script row.

func (Contract) Text

func (c Contract) Text() string

Text renders the contract as prose: what the script is, what it takes, whether anything will execute it, on what cadence, and what it last produced.

It lives here rather than in a consumer because every surface that resolves a script reference shows the same document — fetch returns it as a document body, and a prompt that references a script serves it inline — and two renderers would be two answers to one question, drifting apart the first time either is edited.

func (Contract) Title

func (c Contract) Title() string

Title is the script's human label: its display name, falling back to the name an agent would call it by.

type ContractOutput

type ContractOutput struct {
	Name string `json:"name" example:"sales_by_region"`
	Kind string `json:"kind" example:"portal_asset"`
	// Destination is the named destination the output went to; "portal" for an
	// output that named none.
	Destination string `json:"destination" example:"portal"`
	Format      string `json:"format,omitempty" example:"csv"`
	RowCount    int    `json:"row_count,omitempty" example:"1420"`
	// Refresh marks a data-region refresh of an existing asset: the run
	// replaced one marked region rather than writing a whole document, and
	// Bytes is the spliced payload.
	Refresh bool `json:"refresh,omitempty" example:"false"`
	Bytes   int  `json:"bytes,omitempty" example:"98304"`

	// AssetID and AssetVersion locate an OutputKindAsset output.
	AssetID      string `json:"asset_id,omitempty" example:"asset_a1b2c3d4"`
	AssetVersion int    `json:"asset_version,omitempty" example:"7"`

	// Bucket and Key locate an OutputKindObject output.
	Bucket string `json:"bucket,omitempty" example:"acme-exports"`
	Key    string `json:"key,omitempty" example:"weekly/2026/08/sales.csv"`
}

ContractOutput is one thing a run produced. Kind decides which locator is meaningful: an asset carries the id and version a caller can fetch, an object carries the bucket and key it was written to, which is all the platform knows about it — the bytes left the platform and nothing here will serve them back.

type ContractRun

type ContractRun struct {
	RunID      string           `json:"run_id" example:"run_a1b2c3d4"`
	Version    int              `json:"version" example:"3"`
	FinishedAt *time.Time       `json:"finished_at,omitempty"`
	Outputs    []ContractOutput `json:"outputs,omitempty"`
}

ContractRun summarizes one successful run and what it wrote.

type ContractSchedule

type ContractSchedule struct {
	CronSpec string `json:"cron_spec" example:"0 7 * * 1-5"`
	Timezone string `json:"timezone" example:"America/Los_Angeles"`
	Enabled  bool   `json:"enabled" example:"true"`
	// NextRunAt is the next due fire, nil when the schedule is disabled or its
	// expression has no further fire.
	NextRunAt *time.Time `json:"next_run_at,omitempty"`
}

ContractSchedule is a script's cadence, reported rather than offered: a caller learns the script refreshes itself and when it next will, which is what decides whether to run it again or read what it already produced.

type Cron

type Cron struct {
	// contains filtered or unexported fields
}

Cron is a parsed cron expression bound to its timezone.

Parsing is all this domain takes from the cron library: the schedule's timing is computed from the row on demand, and no goroutine anywhere waits on a cron ticker. What fires a run is the run queue's own due predicate, so there is one scheduler in the platform and it is the queue.

func ParseCron

func ParseCron(spec, timezone string) (Cron, error)

ParseCron parses a cron expression in a timezone. An empty timezone is UTC.

func (Cron) Location

func (c Cron) Location() *time.Location

Location returns the zone the expression is read in.

func (Cron) Next

func (c Cron) Next(t time.Time) time.Time

Next returns the first fire strictly after t.

type Destination

type Destination struct {
	// Name is what a script writes as destination="...", unique within the
	// configured set.
	Name string `json:"name" yaml:"name" example:"acme-drop"`

	// Kind is one of DestinationKinds. Configuration declares only bucket
	// destinations, so it defaults to s3 there.
	Kind string `json:"kind" yaml:"kind" example:"s3"`

	// Connection is the named platform S3 connection the object is written
	// over, empty for the portal. It is the name the authorization middleware
	// checks when the write is issued, so a destination whose connection the
	// run's persona cannot reach is refused by the authority of record.
	Connection string `json:"connection,omitempty" yaml:"connection" example:"acme-s3"`

	// Bucket is the bucket objects land in, empty for the portal.
	Bucket string `json:"bucket,omitempty" yaml:"bucket" example:"acme-exports"`

	// Prefix is the key prefix every object written here sits under, empty for
	// the portal and optional for a bucket. It is the destination's boundary:
	// the script chooses a key beneath it and can never write outside it.
	Prefix string `json:"prefix,omitempty" yaml:"prefix" example:"weekly"`
}

Destination is one place a script may write: named by the script, resolved by the platform against the deployment's configuration at run time. The portal is built in; every other destination is declared in the scripts configuration (scripts.destinations), so repointing one — changing its connection, bucket, or prefix — takes effect on the next run.

A script supplies no endpoint, no credential, and no bucket. It names a destination and everything below comes from configuration, which is why there is no arbitrary egress to have: the only network a script reaches is the operator-configured connection set, and the write is authorized against the run's persona by the middleware like any other call.

func PortalDestination

func PortalDestination() Destination

PortalDestination returns the canonical portal destination.

func (Destination) IsPortal

func (d Destination) IsPortal() bool

IsPortal reports whether the destination is the platform's own asset store.

func (Destination) Label

func (d Destination) Label() string

Label renders a destination for an error message or a log line: the name a script writes, and the address it resolves to.

func (Destination) Normalized

func (d Destination) Normalized() Destination

Normalized returns the destination with its fields trimmed and its prefix in one canonical form, so two declarations that meant the same place read as the same place.

func (Destination) Validate

func (d Destination) Validate() error

Validate checks that one destination names a place the platform can write.

type DryRun added in v1.122.0

type DryRun struct {
	// ID is the run id the draft executed under, which is also its session id,
	// so the audit rows the run produced are reachable from this account.
	ID       string `json:"id" example:"run_a1b2c3d4"`
	ScriptID string `json:"script_id"`
	// SourceSHA256 is the digest of the source that executed. It is what links
	// this account to a version, and it is a raw digest rather than its hex
	// spelling because that is what the column stores.
	SourceSHA256 []byte `json:"-"`
	RequestedBy  string `json:"requested_by,omitempty" example:"jane@example.com"`
	// Status is RunStatusSucceeded or RunStatusFailed. A draft run has no
	// pending state: it is executed inline, and the caller waits for it.
	Status string `json:"status" example:"succeeded"`
	// Error is why a failed draft failed, including the interpreter traceback.
	Error string `json:"error,omitempty"`
	// Log is what the run printed, bounded when it was captured.
	Log          string         `json:"log,omitempty"`
	LogTruncated bool           `json:"log_truncated,omitempty"`
	Metrics      RunMetrics     `json:"metrics"`
	Outputs      []DryRunOutput `json:"outputs,omitempty"`
	CreatedAt    time.Time      `json:"created_at"`
}

DryRun is one recorded draft execution.

func (*DryRun) Succeeded added in v1.122.0

func (d *DryRun) Succeeded() bool

Succeeded reports whether the recorded draft run finished without failing.

type DryRunOutput added in v1.122.0

type DryRunOutput struct {
	Name        string `json:"name" example:"daily_sales"`
	Destination string `json:"destination,omitempty" example:"portal"`
	Format      string `json:"format" example:"csv"`
	RowCount    int    `json:"row_count" example:"1200"`
	// Document marks an output written verbatim from a string body, whose
	// RowCount is therefore not a fact about it.
	Document bool `json:"document,omitempty" example:"false"`
	// Refresh marks a platform.publish_data call: the run would have replaced
	// the data region of an existing asset, and Bytes is the payload it would
	// have spliced in.
	Refresh bool `json:"refresh,omitempty" example:"false"`
	// Bytes is the serialized length in the declared format. A preview
	// serializes to measure rather than estimating, so it is the size a real
	// run of the same rows would write.
	Bytes int `json:"bytes" example:"48213"`
}

DryRunOutput is one output a draft run would have written. It carries the shape and nothing else: a preview has no asset id and no object key, because it wrote neither.

type DryRunStore added in v1.122.0

type DryRunStore interface {
	// RecordDryRun stores one account, trimming the author's older accounts of
	// the same script so the table is bounded by the authoring loop's working
	// set rather than by how many times somebody pressed the button.
	RecordDryRun(ctx context.Context, d *DryRun) error

	// LatestDryRun returns the newest account of one script's exact source, or
	// nil when nobody has run it. Nil is the ordinary answer — most versions
	// were never dry-run — so it is not an error.
	LatestDryRun(ctx context.Context, scriptID string, sourceSHA256 []byte) (*DryRun, error)
}

DryRunStore records and resolves accounts of draft executions.

type Edit added in v1.122.0

type Edit struct {
	// Before is the persisted pre-edit state and After the fully mutated copy.
	Before *Script
	After  *Script
	// Author is recorded on any version produced, together with the authority
	// they held, which is what a run of that version presents (see Author).
	Author Author
}

Edit is one edit crossing the funnel: the persisted pre-edit state, the fully mutated copy, and who wrote it with the authority they held.

type ListFilter

type ListFilter struct {
	// OwnerEmail narrows the listing to one person's scripts, which is the
	// whole of visibility: a caller lists their own, and an administrator
	// leaves it empty to list every script on the platform.
	OwnerEmail string
	Enabled    *bool  // filter by enabled state
	Status     string // filter by lifecycle status; "" for all
	// Category narrows to one category slug; "" for all. Tags narrows to the
	// scripts carrying ANY of the named tags: a reader filtering by two tags is
	// asking for the union of two shelves, not for the scripts on both.
	Category string
	Tags     []string
	Search   string // free-text search on name, display_name, description
	Limit    int    // cap the number of rows returned; 0 means the store default
}

ListFilter controls which scripts are returned by List.

type Materialization

type Materialization string

Materialization is what one attempt to create a scheduled run produced.

const (
	// MaterializedRun means this caller inserted the run.
	MaterializedRun Materialization = "run"
	// MaterializedSkippedOverlap means the previous run of this schedule was
	// still open, so a skipped_overlap row was recorded instead. The skip is a
	// row rather than a log line because a report that quietly stopped
	// producing is exactly the failure a schedule is supposed to make visible.
	MaterializedSkippedOverlap Materialization = "skipped_overlap"
	// MaterializedDuplicate means another replica materialized this fire
	// first. It is the normal outcome of racing materializers, not a fault.
	MaterializedDuplicate Materialization = "duplicate"
)

Materialization outcomes.

type Param

type Param struct {
	Name        string `json:"name" example:"report_date"`
	Type        string `json:"type" example:"date"`
	Description string `json:"description,omitempty" example:"The business date to report on"`
	Required    bool   `json:"required" example:"true"`
	// Default supplies the value when the caller omits an optional parameter.
	// It is bound through exactly the same coercion and checking as a
	// caller-supplied value, so a default cannot smuggle in a type the
	// parameter does not accept.
	Default any `json:"default,omitempty"`
	// Values enumerates the allowed values of an enum parameter, and is
	// meaningless (and refused) on every other type.
	Values []string `json:"values,omitempty" example:"daily,weekly"`
}

Param is one typed parameter of a script: the contract a caller, a schedule, or a draft run binds values against.

type Run

type Run struct {
	ID string `json:"id" example:"run_a1b2c3d4"`

	ScriptID  string `json:"script_id"`
	VersionID string `json:"version_id"`
	// Version is the version NUMBER executed, carried alongside the id so a
	// run reads as "daily-sales v3" without a second lookup.
	Version int `json:"version" example:"3"`

	Trigger string `json:"trigger" example:"tool"`
	Status  string `json:"status" example:"succeeded"`

	// ScheduleID names the schedule that materialized this run, and is empty
	// for every other trigger. It is what the single-fire guarantee is written
	// against: the run's (schedule, fire time) pair is unique, so however many
	// replicas notice the same fire, exactly one run exists for it.
	ScheduleID string `json:"schedule_id,omitempty"`

	// Params are the bound, type-checked parameter values the run executes
	// with. They are bound once, when the run is created, so a re-read of the
	// row explains the run exactly.
	Params map[string]any `json:"params,omitempty"`

	// RequestedBy is the email of whoever asked for this run.
	RequestedBy string `json:"requested_by,omitempty" example:"jane@example.com"`

	// FireTime is the instant the run computes against, handed to the script as
	// run.fire_time. It is pinned when the run is created and never moves,
	// which is what ScheduledFor cannot promise: an infrastructure retry pushes
	// the due time out, and a run delayed that way must still produce the report
	// it was asked for rather than one shifted by the delay.
	FireTime     time.Time  `json:"fire_time"`
	ScheduledFor time.Time  `json:"scheduled_for"`
	StartedAt    *time.Time `json:"started_at,omitempty"`
	FinishedAt   *time.Time `json:"finished_at,omitempty"`

	// Attempt counts claims of this run, and LockedUntil / LockedBy carry the
	// current worker's lease. Together they are the fencing token: a write
	// from a worker whose lease expired and was reclaimed matches no row.
	Attempt     int        `json:"attempt"`
	LockedUntil *time.Time `json:"locked_until,omitempty"`
	LockedBy    string     `json:"locked_by,omitempty"`

	Error        string      `json:"error,omitempty"`
	Log          string      `json:"log,omitempty"`
	LogTruncated bool        `json:"log_truncated,omitempty"`
	Metrics      RunMetrics  `json:"metrics"`
	Outputs      []RunOutput `json:"outputs,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Run is one execution of one script version: a queue row while it is pending or running, and the durable history of that execution afterwards.

The two roles are deliberately one table. A run's history IS its queue record — what was executed, with which parameters, by whose request, how long it took, what it wrote — and splitting them would mean copying every field to keep the history readable.

func (*Run) Lease

func (r *Run) Lease() RunLease

Lease is one worker's claim on one run, and the fencing token every write against that run carries.

func (*Run) Output

func (r *Run) Output(name, destination string) *RunOutput

Output returns the recorded output this run wrote under one name to one destination, or nil. A worker consults it before writing: an output this run already persisted must not be written twice when the run is reclaimed after a crash.

The lookup is by the pair because the name alone is not the identity of a write. One result may be both versioned as a portal asset and delivered to a bucket, and matching on the name would report the second write as already done and silently skip it.

func (*Run) Terminal

func (r *Run) Terminal() bool

Terminal reports whether the run has finished, however it ended. A skipped overlap is terminal on arrival: it names a fire that will not be executed, so nothing is ever going to move it.

type RunFilter

type RunFilter struct {
	// ScriptID scopes the listing to one script; empty lists across scripts.
	ScriptID string
	// ScriptIDs scopes the listing to a set of scripts, which is how a caller
	// reads the runs of everything they own in one query rather than one query
	// per script. An EMPTY, non-nil slice means "no scripts" and matches
	// nothing — the distinction matters, because a caller who owns nothing must
	// not fall through to a listing across every script on the platform.
	ScriptIDs []string
	// Status scopes the listing to one lifecycle status.
	Status string
	// Limit caps the rows returned; zero means the store default.
	Limit int
}

RunFilter selects runs for a history listing.

type RunLease

type RunLease struct {
	RunID   string
	Worker  string
	Attempt int
}

RunLease identifies the holder of a claim. Every mutating store call takes one, and a call whose lease no longer matches the row is refused with ErrLeaseLost rather than overwriting the work of the worker that took over.

type RunMetrics

type RunMetrics struct {
	Steps      uint64 `json:"steps"`
	DurationMS int64  `json:"duration_ms"`
	Queries    int    `json:"queries"`
	Exports    int    `json:"exports"`
}

RunMetrics is what one execution cost, recorded on the run for capacity review and for sizing a script's limits against what it actually uses.

type RunOutput

type RunOutput struct {
	Name string `json:"name"`
	// Destination is the named destination's name. It is empty on rows
	// written before destinations existed, which Destination() reads as the
	// portal.
	Destination string `json:"destination,omitempty"`

	// AssetID and AssetVersion identify a portal output.
	AssetID      string `json:"asset_id,omitempty"`
	AssetVersion int    `json:"asset_version,omitempty"`

	// Bucket and Key locate a delivered object.
	Bucket string `json:"bucket,omitempty"`
	Key    string `json:"key,omitempty"`

	Format   string `json:"format"`
	RowCount int    `json:"row_count"`
	// Document marks an output written verbatim from a string body rather than
	// serialized from rows, so a surface reporting the output does not describe
	// a dashboard as a zero-row table.
	Document bool `json:"document,omitempty"`
	// Refresh marks a platform.publish_data write: the run replaced the data
	// region of an existing asset rather than writing a whole output, so Bytes
	// is the payload spliced in, not the document.
	Refresh bool `json:"refresh,omitempty"`
	Bytes   int  `json:"bytes"`
}

RunOutput is one persisted output of a run: where it went, and what landed there. An output written to the portal names the stable asset the output name maps to and the version this run created of it; one delivered to a bucket names the object it wrote.

One run may write the same output name to more than one destination — the dashboard keeps its versioned asset while an external system receives its file — so a recorded output is identified by the PAIR, never by the name alone.

type RunResult

type RunResult struct {
	// Status is RunStatusSucceeded or RunStatusFailed.
	Status string
	// Error is the failure message, carrying the Starlark backtrace when the
	// script itself failed.
	Error        string
	Log          string
	LogTruncated bool
	Metrics      RunMetrics
}

RunResult is the terminal outcome of one attempt, as the worker reports it.

type RunStore

type RunStore interface {
	// Enqueue inserts a pending run, assigning ID when empty.
	Enqueue(ctx context.Context, r *Run) error

	// GetRun returns one run by id, or ErrRunNotFound.
	GetRun(ctx context.Context, id string) (*Run, error)

	// ListRuns returns runs matching the filter, newest first.
	ListRuns(ctx context.Context, filter RunFilter) ([]Run, error)

	// Claim takes the next due run for worker, holding it for lease. It
	// returns ErrNoWork when nothing is due.
	Claim(ctx context.Context, worker string, lease time.Duration) (*Run, error)

	// RecordOutput appends one persisted output to the run. It is written as
	// soon as the output exists, not at the end of the run, so a reclaimed run
	// can tell what it already wrote.
	RecordOutput(ctx context.Context, lease RunLease, out RunOutput) error

	// Finish records a terminal result for the claimed run.
	Finish(ctx context.Context, lease RunLease, res RunResult) error

	// Retry returns the claimed run to pending, due after backoff, recording
	// the cause. Reserved for infrastructure failures: a script error is
	// deterministic and retrying it changes nothing.
	Retry(ctx context.Context, lease RunLease, cause string, backoff time.Duration) error

	// PurgeRuns deletes terminal runs older than retention, returning the
	// number removed.
	PurgeRuns(ctx context.Context, retention time.Duration) (int64, error)
}

RunStore is the queue and the history of script runs.

The queue half follows the shape the platform's other durable queues use: a claim that folds crashed-worker recovery into its own predicate (a lease that expired makes the row claimable again), so there is no reaper process and no leader election, and any number of replicas can run a worker.

type Schedule

type Schedule struct {
	ID       string `json:"id"`
	ScriptID string `json:"script_id"`

	// CronSpec is a standard five-field cron expression or one of the
	// descriptors (@daily, @hourly, @every 30m).
	CronSpec string `json:"cron_spec" example:"0 7 * * 1-5"`
	// Timezone is the IANA zone the spec is read in.
	Timezone string `json:"timezone" example:"America/Los_Angeles"`

	// Params are the values every fire binds, with tokens unexpanded. They are
	// stored as written so the schedule reads as what it means ("report on the
	// day it fires") rather than as whatever date happened to be current when
	// somebody set it.
	Params map[string]any `json:"params,omitempty"`

	Enabled bool `json:"enabled"`

	// NextRunAt is when the next fire is due, and zero when the expression has
	// no further fire at all. It is the materializer's efficiency index, not
	// its correctness guarantee: two replicas may read the same due schedule,
	// and what stops them producing two runs is the unique index on the run
	// they insert.
	//
	// While the schedule is disabled the field still holds the fire it was
	// paused on, because resuming picks up from there. What a READER is told is
	// DueAt, which is empty for a paused schedule; see MarshalJSON.
	NextRunAt time.Time `json:"next_run_at,omitzero"`
	// LastFireAt is the fire time of the most recent run this schedule
	// produced, empty until it has produced one.
	LastFireAt *time.Time `json:"last_fire_at,omitempty"`
	// MissedFires counts fires that came due while the platform was not
	// materializing them, cumulatively. Catching up on them is deliberately
	// not attempted (see NextFire), so the count is the visible record of the
	// gap.
	MissedFires int `json:"missed_fires"`

	CreatedBy string    `json:"created_by,omitempty" example:"jane@example.com"`
	UpdatedBy string    `json:"updated_by,omitempty" example:"jane@example.com"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Schedule is the cadence one script runs on: the cron expression, the zone it is read in, and the parameter values every fire binds.

A schedule is not an authority. It names when the script's latest saved version runs and with which parameters, and nothing else; the roles a run presents and the connections it may reach are decided at run time by the persona filter, which a schedule cannot touch.

func BuildSchedule

func BuildSchedule(sc *Script, prev *Schedule, req ScheduleRequest, now time.Time) (*Schedule, error)

BuildSchedule turns a request into the schedule to store, validated against the live record's parameter contract — which is what its fires will bind against, because a run executes the latest saved version — and with its first fire computed.

prev, when non-nil, is the schedule being replaced: its identity and its creator survive an edit of the cadence, because the runs that point at it point at the same automation.

func (Schedule) DueAt added in v1.122.0

func (s Schedule) DueAt() time.Time

DueAt reports when this schedule's next fire is due, and the zero time when no fire is due at all.

A disabled schedule has no next fire. Its stored NextRunAt is the fire it was paused on, kept so that resuming picks up where the pause began, and stating it to a reader announces a fire that will not happen: the materializer's own predicate requires the schedule to be enabled. Every surface that tells somebody when a schedule fires next asks this rather than reading the field.

func (Schedule) MarshalJSON added in v1.122.0

func (s Schedule) MarshalJSON() ([]byte, error)

MarshalJSON renders the schedule as a reader may act on it, which differs from the stored row in exactly one field: next_run_at is DueAt.

It lives here rather than in each of the surfaces that serve a Schedule because the rule is a property of the schedule, not of any one payload — the admin API, the portal API, and the manage_script response all serve this struct, and a rule applied in three places is a rule that drifts in one.

func (*Schedule) NextFire

func (s *Schedule) NextFire(c Cron, now time.Time) ScheduleFire

NextFire returns the fire a due schedule should materialize now, the number of fires that were missed before it, and the next due time to store.

The misfire policy is fire-once-latest. After a gap — a stopped worker, a restored database — the schedule materializes ONE run, for the most recent fire that has come due, and counts the rest as missed. Catching up would mean a burst of identical reports against the warehouse the moment the platform came back, each computing a date nobody is waiting on any more, which is a worse failure than a visible gap. A backfill somebody actually wants is a run_script call with the parameters they want.

Due is false when nothing is due yet, and when the schedule is further behind than one pass will walk.

func (*Schedule) Validate

func (s *Schedule) Validate(params []Param) error

Validate checks that a schedule is well-formed against the parameter contract of the version it will execute.

The parameter check is done here, when the schedule is set, rather than at the fire — a schedule whose bindings do not satisfy the contract would otherwise fail silently every night, with nobody watching.

type ScheduleAdvance

type ScheduleAdvance struct {
	// ID names the schedule and From is where the caller found NextRunAt. The
	// update applies only if the row still carries From, which is what makes
	// the step idempotent across replicas.
	ID   string
	From time.Time
	// Next is the fire time to store — zero when the expression has no further
	// fire, which parks the schedule rather than leaving it perpetually due —
	// and Fired, when non-zero, is the fire this pass materialized.
	Next  time.Time
	Fired time.Time
	// Missed is added to the schedule's cumulative missed-fire count.
	Missed int
}

ScheduleAdvance is one conditional step forward of a schedule.

type ScheduleFilter

type ScheduleFilter struct {
	// ScriptID scopes the listing to one script.
	ScriptID string
	// ScriptIDs scopes the listing to a set of scripts, which is how a caller
	// lists the schedules it may see without reading the rest and discarding
	// them. An EMPTY, non-nil slice means "no scripts" and matches nothing —
	// the distinction matters, because a caller who can see nothing must not
	// fall through to an unfiltered listing.
	ScriptIDs []string
	// Enabled, when set, scopes the listing to enabled or disabled schedules.
	Enabled *bool
	// Limit caps the rows returned; zero means the store default.
	Limit int
}

ScheduleFilter selects schedules for a listing.

type ScheduleFire

type ScheduleFire struct {
	// At is the fire to materialize, meaningful only when Due is true.
	At time.Time
	// Missed counts the fires stepped over without materializing.
	Missed int
	// Next is the due time to store, zero when the expression has no further
	// fire.
	Next time.Time
	// Due reports whether At should be materialized at all.
	Due bool
}

ScheduleFire is what one materialization pass concluded about a schedule: which fire to materialize, how many were stepped over to reach it, and where the schedule moves to.

type ScheduleRequest

type ScheduleRequest struct {
	CronSpec string
	Timezone string
	// Params are the bindings, with tokens as written.
	Params map[string]any
	// Enabled is a pointer so a request that does not mention it leaves an
	// existing schedule's state alone; a new schedule with nothing said is on,
	// because setting a schedule is asking for it to run.
	Enabled *bool
	// Actor is who is making the change, recorded on the row.
	Actor string
}

ScheduleRequest is a surface's request to set a script's schedule.

type ScheduleStore

type ScheduleStore interface {
	// SetSchedule creates or replaces the schedule of one script, assigning ID
	// when empty. A script has at most one schedule: a second cadence is a
	// second script, which keeps the run history of "the 07:00 report" from
	// interleaving with another cadence's.
	SetSchedule(ctx context.Context, s *Schedule) error

	// GetSchedule returns one script's schedule, or ErrScheduleNotFound.
	GetSchedule(ctx context.Context, scriptID string) (*Schedule, error)

	// ListSchedules returns schedules matching the filter.
	ListSchedules(ctx context.Context, filter ScheduleFilter) ([]Schedule, error)

	// SetScheduleEnabled turns one script's schedule on or off, recording who
	// did it. Disabling is how a schedule is retired: the row stays, because a
	// schedule that produced runs is part of the explanation of those runs.
	SetScheduleEnabled(ctx context.Context, scriptID string, enabled bool, actor string) error

	// DueSchedules returns enabled schedules whose next fire has arrived.
	DueSchedules(ctx context.Context, now time.Time, limit int) ([]Schedule, error)

	// MaterializeRun inserts one scheduled run, reporting what happened: the
	// run was created, it was skipped because the previous one is still open,
	// or another replica got there first.
	MaterializeRun(ctx context.Context, r *Run) (Materialization, error)

	// AdvanceSchedule moves a schedule forward after a materialization pass.
	// It is conditional on the schedule still being where the caller found it,
	// so two replicas that walked the same fire do not double-count the misses
	// or move the schedule twice. It reports whether the row moved.
	AdvanceSchedule(ctx context.Context, adv ScheduleAdvance) (bool, error)
}

ScheduleStore persists schedules and materializes the runs they produce.

Materialization lives here rather than on RunStore because its correctness is a property of the schedule, not of the queue: what makes a fire happen once across every replica is the unique index this store's insert conflicts against.

type ScoredScript

type ScoredScript struct {
	Script Script  `json:"script"`
	Score  float64 `json:"score"`
}

ScoredScript pairs a script with its relevance score in [0,1].

type Script

type Script struct {
	ID          string  `json:"id" example:"script_a1b2c3d4"`
	Name        string  `json:"name" example:"daily-sales-report"`
	DisplayName string  `json:"display_name" example:"Daily Sales Report"`
	Description string  `json:"description" example:"Summarize yesterday's sales by region"`
	Source      string  `json:"source" example:"rows = platform.query(connection='primary', sql='SELECT 1')"`
	Params      []Param `json:"params"`
	// OwnerEmail is the one person a script belongs to: the only caller who
	// sees, edits, runs, and schedules it, administrators aside. An
	// administrator can move it to another owner (Transfer).
	OwnerEmail string `json:"owner_email" example:"jane@example.com"`
	// Category files the script under one lowercase slug, the axis a listing
	// filters on and a reader scans. It is the same axis a resource and an
	// insight carry, written the same way (#1369).
	Category string   `json:"category,omitempty" example:"reporting"`
	Tags     []string `json:"tags" example:"sales,reporting"`
	Enabled  bool     `json:"enabled" example:"true"`

	// Lifecycle.
	Status       string     `json:"status" example:"active"`
	SupersededBy string     `json:"superseded_by,omitempty" example:"daily-sales-report-v2"`
	DeprecatedAt *time.Time `json:"deprecated_at,omitempty"`

	// Version is the number of the snapshot the live row currently carries,
	// which is the version a run executes: saving a version makes it the
	// version that runs.
	Version int `json:"version" example:"3"`

	CreatedAt time.Time `json:"created_at" example:"2026-08-13T14:30:00Z"`
	UpdatedAt time.Time `json:"updated_at" example:"2026-08-13T14:30:00Z"`
}

Script is the live record of one managed script: its identity, and the currently served source and parameter contract, which are what a run executes.

func (*Script) ApplyStatusTransition

func (s *Script) ApplyStatusTransition(newStatus, supersededBy string, now time.Time) error

ApplyStatusTransition validates and applies a status change, stamping the lifecycle metadata. A no-op when newStatus is empty or unchanged. now is passed in for testability. Returns an error on an invalid transition.

func (*Script) OwnedBy added in v1.123.0

func (s *Script) OwnedBy(email string) bool

OwnedBy reports whether the named caller owns this script, which is the whole of script visibility: a script is one person's, and only that person (and an administrator, an authority the caller applies, not this method) sees it, edits it, runs it, or schedules it.

Both sides must be identified. A script whose owner is empty — one authored by a principal carrying no email, such as an API key, or one whose owner predates this rule — would otherwise belong to every caller the platform cannot name either. Such a script is nobody's until an administrator transfers it. The store's list and search predicates require the same, so a caller can never fetch what a listing would have hidden.

func (*Script) Principal

func (s *Script) Principal() string

Principal is the identity a platform-executed run of this script authenticates as.

A script gets its own principal rather than borrowing its owner's so every gate, rate limiter, and audit row can tell the two apart: a row reading script:daily-sales is a governed automation, and one reading the owner's address is that person at a keyboard. The owner stays accountable through the script's owner_email, which the run also carries.

func (*Script) Transfer added in v1.123.0

func (s *Script) Transfer(newOwnerEmail string) error

Transfer moves this script to a new owner, normalizing the address the way every other identity in the platform is normalized so a transfer to "Jane@Example.com" and a login as "jane@example.com" name one person.

Ownership is the whole of script visibility, so a transfer hands over everything at once: what the new owner sees, edits, runs, and schedules. The named use is moving a script to an administrator, which is how a script comes to run under an administrator's authority — the run presents the roles captured on the version it executes, and the store records the transfer as a version authored by the administrator making it.

It refuses a transfer to the current owner rather than treating it as a no-op: the caller asked for a change, and silently recording a version that changes nothing would put a hand-over in the history that never happened.

func (*Script) Validate

func (s *Script) Validate() error

Validate checks the whole record: name, source, parameter contract, tags, the fields that document the script (display name, description, category), and status. Mutation surfaces call it on the FINAL state rather than field by field as arguments arrive, so a record that was valid before an edit and invalid after it is refused — which is the case a per-argument check misses, since the offending combination may involve a field the caller never sent.

type SearchQuery

type SearchQuery struct {
	// Embedding is the query vector. A nil Embedding selects lexical-only
	// ranking, which is exactly the behavior a deployment with no embedding
	// provider has always had; a non-nil one selects hybrid ranking over the
	// vectors the indexjobs scripts consumer writes.
	Embedding []float32
	// QueryText is the raw intent text the lexical ranking matches.
	QueryText string
	// OwnerEmail is the caller identity, which is the whole visibility
	// predicate. Empty matches no script at all.
	OwnerEmail string
	// Limit caps the candidates returned; see EffectiveLimit.
	Limit int
}

SearchQuery describes a relevance ranking request over the script library.

Visibility is applied before ranking, as a predicate rather than a filter over the answer: a script the caller cannot see must cost neither a row nor a decision. The rule is Script.OwnedBy expressed in SQL — the caller's own scripts and nothing else. An unidentified caller therefore matches nothing, which is the fail-closed answer.

func (SearchQuery) EffectiveLimit

func (q SearchQuery) EffectiveLimit() int

EffectiveLimit clamps the requested limit into [1, maxSearchLimit], defaulting an unset or out-of-range value to DefaultSearchLimit.

type Searcher

type Searcher interface {
	Search(ctx context.Context, q SearchQuery) ([]ScoredScript, error)

	// Contract composes the contract document for one script: the script's own
	// record and parameter contract, its cadence when it has one, and its last
	// successful run. It applies no
	// visibility rule of its own — the caller has already established that this
	// script may be seen.
	Contract(ctx context.Context, id string) (*Contract, error)
}

Searcher ranks scripts by relevance within the caller's visibility, and resolves one script's whole contract by id. The two halves are the two halves of discovery: search says a script exists and what it takes, and the contract read says everything a caller needs to decide whether to use it.

It is a capability separate from Store, so only a backing store that can rank (the PostgreSQL one) implements it and the feature degrades to absent rather than forcing every Store to carry a ranking query.

type Store

type Store interface {
	// Create persists a new script and its first version, assigning ID when
	// empty. The author is recorded on that version along with the authority
	// they held, which is what a run of that version presents.
	Create(ctx context.Context, s *Script, author Author) error

	// GetByName retrieves one owner's script by name. Returns nil, nil if not
	// found.
	GetByName(ctx context.Context, ownerEmail, name string) (*Script, error)

	// GetByID retrieves a script by ID. Returns nil, nil if not found.
	GetByID(ctx context.Context, id string) (*Script, error)

	// Update modifies an existing script.
	Update(ctx context.Context, s *Script) error

	// Delete removes a script by ID.
	Delete(ctx context.Context, id string) error

	// List returns scripts matching the filter, newest first.
	List(ctx context.Context, filter ListFilter) ([]Script, error)

	// Transfer moves a script to a new owner and records the move as a version
	// authored by the administrator making it, whose roles the new version
	// carries: a run presents the authority captured on the version it
	// executes, so a transfer that left the old authority in place would keep
	// running the script as the person who no longer owns it.
	Transfer(ctx context.Context, id, newOwnerEmail string, author Author) error
}

Store defines the interface for script persistence. A script name is unique within its owner and nowhere else, so every lookup by name names an owner too: two analysts may each keep their own "daily-sales".

type Version

type Version struct {
	ID          string   `json:"id" example:"sver_a1b2c3d4"`
	ScriptID    string   `json:"script_id" example:"script_a1b2c3d4"`
	Version     int      `json:"version" example:"3"`
	DisplayName string   `json:"display_name" example:"Daily Sales Report"`
	Description string   `json:"description" example:"Summarize yesterday's sales by region"`
	Category    string   `json:"category,omitempty" example:"reporting"`
	Source      string   `json:"source"`
	Params      []Param  `json:"params"`
	Tags        []string `json:"tags" example:"sales,reporting"`
	Author      string   `json:"author" example:"jane@example.com"`
	// AuthorRoles is the authority the author held when this snapshot was
	// written, and the roles a run of this version presents. See Author.
	AuthorRoles []string  `json:"author_roles,omitempty" example:"analyst"`
	Status      string    `json:"status" example:"applied"`
	CreatedAt   time.Time `json:"created_at" example:"2026-08-13T14:30:00Z"`
}

Version is one immutable snapshot of a script's versioned fields (source, params, display name, description, category, tags), with the author who produced it and the authority they held.

The snapshot is what makes a run explainable months later — a run record names the version it executed, and the code of that version is still here.

type VersionStore

type VersionStore interface {
	// UpdateWithVersion persists s like Store.Update and, when any versioned
	// snapshot field differs from the stored row, records a new applied version
	// authored by author and advances s.Version to it.
	UpdateWithVersion(ctx context.Context, s *Script, author Author) error

	// ListVersions returns every version of the script, newest first.
	ListVersions(ctx context.Context, scriptID string) ([]Version, error)

	// GetVersion returns one version with its full source, or nil, nil when the
	// script has no such version.
	GetVersion(ctx context.Context, scriptID string, version int) (*Version, error)

	// GetVersionByID returns one version by its id, or nil, nil when no such
	// version exists. It is how a run loads the code it executes: a run is
	// queued against an id, which names one immutable snapshot for the life of
	// the script, where a version number could be renumbered.
	GetVersionByID(ctx context.Context, id string) (*Version, error)
}

VersionStore is the versioning capability of a script store. The PostgreSQL store implements it; a store without it (no-database deployments, plain test stores) degrades to unversioned updates through ApplyEdit's fallback. Every write method is transactional with the scripts row it touches.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL