Skip to content

Standard Library

Status: Draft · Spec version 0.6 · Normative · Companion to the Workfile specification

This document defines the standard library: the canonical connectors and the canonical filter packages of the specification’s canonical namespaces. For these namespaces, portability is a guarantee of the standard rather than a property of a shared deployment catalog.

The library is three kinds of artifact together:

  • this document, which states the behavior that a manifest cannot state;
  • the canonical manifests: data files in the standard’s own formats, a connector manifest or a filter package manifest, under conformance/library/;
  • the fixtures and samples that ship beside each manifest.

All three are normative. A manifest defines a surface: names, argument schemas, error classes, and promises. Fixtures verify behavior. Where the three disagree, the standard has a defect, and the maintainers resolve it explicitly, as the conformance rules provide for the suite.

The library defines no vendor connector, no address, no wire protocol, and no model provider. Vendor catalogs are published and versioned independently. Where a library connector performs network or model I/O, the deployment supplies the destination through a connection.

Each library package has its own version, independent of the spec version. This spec version pins the minimum version of each package:

Package Kind Minimum version
time, store, http, data, llm connector 1.0.0
text, html, money, rank, workweek, human, url, markup, match, phone filters 1.0.0

Three rules keep the pin meaningful:

  • Within a major version, a library package MUST NOT remove a name, change the type of an argument or a result, or weaken a promise. A minor version MAY add an action, a filter, or an optional argument.
  • Implementations MUST resolve each library namespace to the canonical package, at the pinned minimum version or above. Resolution and recording follow the pinning rules.
  • A resolved catalog MUST NOT bind any other package to a canonical or reserved namespace. The diagnostic is catalog.reserved_namespace.

Data-carrying packages. workweek holds calendar data, phone holds numbering-plan data, human holds locale wording, and money and url hold small symbol and parameter tables. That data ships inside the package. A data update is a new package version, and resolution pins it per run. A package that reads its data at evaluation time performs I/O and violates purity. Implementations MUST reject such a package.

Each library package has a capability name of the form lib-<namespace>: lib-time, lib-store, lib-http, lib-data, lib-llm, lib-text, lib-html, lib-money, lib-rank, lib-workweek, lib-human, lib-url, lib-markup, lib-match, lib-phone.

Obligations divide by conformance level:

  • Validation obligations bind every level. Implementations MUST resolve the canonical manifests, whatever capabilities they declare.
  • Execution of a library action, trigger, or filter binds only an implementation that declares that package’s capability. A file that requires an undeclared capability is rejected with capability.required.

An implementation conforms to the portable profile when it conforms at Level 2 or higher and declares every library capability. A Workfile whose connector and filter references stay within the library runs on every implementation of the portable profile, with identical validation results.

The manifests are under conformance/library/connectors/. Below are the semantics that they cannot declare.

time holds the canonical schedule trigger. It declares no actions.

Trigger Kind Configuration Binds
schedule schedule cron (string, required), timezone (string, required) tick{ scheduled_at, fired_at }

timezone is an IANA zone identifier. It is required because an implicit default timezone is a silent divergence between deployments.

Cron grammar. cron holds five fields, separated by whitespace: minute, hour, day-of-month, month, day-of-week. Each field is *, an integer, a range a-b, a step */n or a-b/n, or a comma-separated list of those. Minute is 0–59, hour is 0–23, day-of-month is 1–31, month is 1–12, and day-of-week is 0–6 with 0 as Sunday. There are no name forms, no 7 for Sunday, no seconds field, and no @ keywords.

Matching. An instant matches when every field matches its local value in the declared zone. One exception is inherited from historical cron: when day-of-month and day-of-week are both restricted (neither is *), the instant matches when either matches.

Zone transitions. A local time that does not occur, because a zone transition skips it, fires once at the first instant after the gap. A local time that occurs twice fires on the first occurrence only.

scheduled_at is the matched instant, with the zone’s offset as its display zone. fired_at is the instant the implementation observed. Implementations MUST start exactly one run per matched instant while they are running. Behavior across implementation downtime is implementation-defined, and MUST be documented.

store is canonical cross-run state: single keys, no enumeration, and no query. The design rationale states why it is small.

Action Input Output Idempotent
get key value: json, found: bool yes
put key, value: json yes
compare_and_set key, expected: json, value: json swapped: bool, value: json no
increment key, by: int = 1 value: int no

Atomicity. Implementations MUST apply each action atomically. Operations on one key are totally ordered, and each operation observes the effect of every operation ordered before it, across runs and across workflows that share the connection.

Null is absence. A stored value is never null. A put whose value is null or absent removes the key. get of an absent key yields found: false and value: null. A compare_and_set whose expected is null or absent matches only an absent key.

compare_and_set writes value when the stored value equals expected by the equality rules. Its output value holds the stored value after the operation: the new value when swapped is true, and the unchanged value otherwise.

increment adds by to the stored value, and treats an absent key as 0. A stored value that is not an int fails with wrong_type (fatal). A result outside the int range fails with overflow (fatal).

Bounds. A key is at most 512 code points. Implementations MUST accept a value up to 64 KiB in canonical serialization; a larger bound is implementation-defined, and a value over the bound fails with too_large (fatal).

Scope. Keys are scoped to the connection. The deployment decides which workflows share a connection, and therefore share keys. Concurrent read-modify-write against one key requires queue_by on that key, or compare_and_set.

http is the bridge to a system that the catalog does not cover. Its promises are the weakest in the library, because a manifest cannot know the remote.

Action Input Output Idempotent
get url, headers, query, timeout = 30s status: int, headers, body: string yes
post / put / delete url, headers, query, body: json, timeout = 30s status: int, headers, body: string no
Trigger Kind Binds
webhook webhook request{ method, path, headers, query, body }

get is promised idempotent because RFC 9110 §9.2.1 defines it as safe. put and delete carry no promise, although RFC 9110 defines them idempotent, because the manifest promises only what an unknown remote honors.

Request. url MUST be an absolute http or https URL; any other value fails with invalid_url (fatal). Entries of query are appended to the URL’s query, percent-encoded. Headers are merged in order: the connection’s headers parameter, then the step’s headers, with the later value winning per name. A body that is a string is sent as given. Any other body is sent in canonical JSON serialization, as the json filter defines it, with Content-Type: application/json unless a header supplies one.

Redirects. For get, implementations MUST follow redirects, up to 10; more fail with too_many_redirects (fatal). For post, put, and delete, implementations MUST NOT follow a redirect: a 3xx response binds as a result, because re-sending a body across a redirect diverges between clients.

Response. A 2xx or 3xx response binds the output. A 4xx or 5xx response is an action error whose code is the status. The manifest classifies 408 and 429 and 5xx retryable, and 4xx fatal. A response that ends without a status — a timeout or a lost connection — is classified unknown, so on_unknown applies. body is the response body decoded as UTF-8; an undecodable byte becomes U+FFFD. http moves text, not files: file transfer belongs to a storage connector.

Address policy. The url argument addresses the request, so the host-pattern rule cannot apply. Implementations MUST apply the deployment’s host policy to every http request, as they do for a self-hosted connector. By default that policy SHOULD exclude private, loopback, and link-local ranges.

The webhook trigger declares no correlate_on, so wait_for cannot target it.

data holds content operations on files. Expressions cannot read file contents, so each is an action. Every data action is deterministic, idempotent, and validated.

Action Input Output
checksum file, algorithm: enum[md5, sha256] = sha256 checksum: string, size: int, content_type: string
parse_csv file, header: bool = true, delimiter = "," rows: list[json], count: int
parse_json file data: json

checksum yields lowercase hexadecimal, and size in bytes.

parse_csv reads RFC 4180: quoted fields, doubled quotes, CRLF or LF line ends, UTF-8 with an optional BOM. With header: true, each row is a map keyed by the header row; a repeated or empty header name fails with invalid_content (fatal). With header: false, each row is a list of strings. A row whose field count differs from the first row fails with invalid_content. Every field is a string; the author converts with core filters.

parse_json accepts one JSON document (RFC 8259). An input that is not valid JSON, holds trailing content, or repeats a member name fails with invalid_content. The rules match the core from_json filter.

There is no convert action.

llm holds portable model actions. The connection supplies the provider and the model: model is a required connection parameter, so a file never names one. Every llm action is nondeterministic; the manifest declares no deterministic promise, and recording is what makes a run replayable.

Action Input Output
categorize text, categories: list[string], instructions? category: string
extract text or file, schema: map[string, string], instructions? the extracted object
summarize text, instructions? summary: string
generate prompt text: string
transcribe file, language? text: string

categorize. The result MUST be a member of categories; the action declares validated_output, and implementations MUST enforce the membership before binding. categories MUST hold at least one member. When categories is a list literal, implementations MUST treat the output category as enum[...] of exactly those members. A route over it is then exhaustively checkable.

extract. schema maps each field name to a type expression. A call MUST supply exactly one of text and file; implementations MUST reject a call that supplies neither or both, with catalog.invalid_argument. The step binds the extracted object directly: one field per schema entry, so candidate.summary reads a field named summary. Implementations MUST validate the result against schema before binding. When schema is a map literal, implementations MUST type the binding by it, and paths into it are statically checkable. When schema is computed, the binding is an open map[string, json], as an unrefined extension region is.

transcribe. language is a BCP 47 tag, as a hint. The result is the transcript text.

Errors. The manifest classifies by named code: rate_limited and provider_unavailable are retryable; invalid_request, content_filtered, and input_too_long are fatal. An interrupted request classifies unknown, as everywhere.

Provider-specific parameters do not enter the canonical surface. A provider-coupled connector can publish under its own namespace, and x- keys remain available to deployments.

wf stays reserved and undefined. A later library version defines it or retires the reservation.

The manifests and fixtures are under conformance/library/packages/. Every filter satisfies the extension contract: pure, deterministic, total, no I/O, declared signature. Fixture files use the expression-fixture form.

Signatures below follow the core filter annex: a bracketed argument is optional, and T? marks a nullable result. Strings compare and measure by code point, with no normalization, as the value rules state.

Literal arguments. Several filters take an argument from a defined domain: a calendar name, a locale, a selector, a currency code. Where such an argument is a literal outside the domain, the file is invalid with filter.signature. Where it is computed, an out-of-domain value is an evaluation fault, fault.filter_argument.

Input domains. An input value outside a filter’s defined domain — a non-member of a scale, an amount or a number that does not parse — is an evaluation fault, fault.conversion, matching the core conversion filters.

Filter Signature and behavior
slugify string → string — see below
initials string → string — the first code point of each word, uppercased and concatenated; a word is a maximal run of non-whitespace
truncate_words(n) string → string — the first n words, joined by one space; no marker is appended; n MUST be a positive integer

slugify applies five steps in order: normalize to NFKD; remove combining marks; lowercase; replace each maximal run of characters outside [a-z0-9] with one -; trim leading and trailing -. A character with no ASCII decomposition is removed by step four, so the result can be empty.

Filter Signature and behavior
select(css) string → string? — the text content of the first match in document order, whitespace-collapsed and trimmed as in core strip_html; null when nothing matches
select_all(css) string → list[string] — the same text content, per match, in document order
links string → list[object] — every a element that has an href, as { href: string, text: string }, in document order; href is as written, unresolved

Input parses by the WHATWG HTML parsing algorithm, so malformed markup selects rather than faults.

Selector subset. The css argument is a closed subset of CSS: a type name, .class, #id, [attr], [attr="value"], compounds of those, and the descendant (space) and child (>) combinators. There is no grouping and no pseudo-class. A selector outside the subset follows the literal-argument rule above.

Filter Signature and behavior
parse string → object{ amount: string, currency: string? }; see below
to_currency(code) number → string — formats by ISO 4217; see below

money formats and parses. Conversion needs rates, which is I/O, so it belongs to a connector.

parse accepts one amount with an optional currency marker: a leading or trailing symbol or ISO 4217 code from the package data. amount is the digits with grouping removed and . as the decimal separator; currency is the ISO code, or null when no marker is present. Separator rules: when both . and , appear, the last one is the decimal separator; a single separator followed by exactly three digits is grouping; followed by one or two digits, it is the decimal separator; anything else is an evaluation fault. An input that does not hold exactly one parsable amount is an evaluation fault: parse validates or faults, and never guesses.

to_currency(code) rounds to the currency’s minor-unit count (ISO 4217), half away from zero as in core round, groups thousands with ,, and uses . as the decimal separator. It prefixes the currency’s symbol where the package data defines one, and otherwise prefixes the ISO code and one space. Version 1.0.0 data defines symbols for USD ($), EUR (), GBP (£), and JPY (¥).

rank orders enum values against a scale: a list[string] from lowest to highest.

Filter Signature and behavior
in(scale) string → int — the zero-based position of the value in scale; a value that is not a member is an evaluation fault
top(path, scale) list[object] → object? — the element whose value at path ranks highest; the first such element on a tie; null for an empty list; a non-member value at path is an evaluation fault

The reverse mapping needs no filter: scale[i] indexes the list.

Calendar-aware day arithmetic, over a scale that exact milliseconds cannot express. Each filter takes an optional calendar name; the default is mon_fri, which has no holidays. Version 1.0.0 ships mon_fri only; holiday calendars arrive as package data versions.

Every filter operates on local dates in the timestamp’s display zone, as format_time does. A workday is a day that the calendar does not exclude.

Filter Signature and behavior
add_days(n, [calendar]) timestamp → timestamp — advances n workdays, preserving the local time of day; n MAY be negative; 0 yields the value unchanged
is_workday([calendar]) timestamp → bool
next_workday([calendar]) timestamp → timestamp — the value unchanged when its day is a workday, else the next workday at the same local time of day
days_between(ts, [calendar]) timestamp → int — the count of workdays d with receiver < d ≤ ts, by local date; negated when ts is earlier

A shifted result whose local time does not occur, because a zone transition skips it, moves forward by the width of the gap, as in the time connector.

Locale-directed formatting. locale is a required BCP 47 tag on every filter: wording is a locale, and an implicit default is a silent divergence, as the core exclusions state. Version 1.0.0 ships en; further locales arrive as package data versions.

Filter Signature and behavior
ordinal(locale) int → string1st, 2nd, 11th, 21st
int_comma(locale) int → string — grouped digits: 1,234,567
int_word(locale) number → string1.2 million; magnitudes below one million format as int_comma; one decimal place, half away from zero, with a trailing .0 dropped; the scale words are million, billion, trillion
format_number(places, locale) number → string — fixed decimal places, grouped, half away from zero
relative_to(ts, locale) timestamp → string — the receiver described relative to ts; see below

relative_to selects the largest fitting unit from: year (365 days), month (30 days), week (7 days), day, hour, minute, with counts rounded down. A receiver before ts yields the “ago” form, and after it the “in” form. A difference under one minute yields the locale’s “just now” wording. The wording per locale is package data, verified by fixtures.

Core composes URLs (url_encode); url decomposes them. Inputs parse as URL references by the WHATWG URL standard. An input that cannot parse as a reference is an evaluation fault.

Filter Signature and behavior
domain string → string? — the host, lowercased; null when the reference has none
path string → string — the path component; the empty string when absent
query(param) string → string? — the first value of param, percent-decoded; null when absent
normalize string → string — absolute URLs only, else an evaluation fault; see below
strip_tracking string → string — removes tracking query parameters; see below

normalize lowercases the scheme and host, removes a default port (80 for http, 443 for https), makes an empty path /, percent-decodes unreserved characters, and uppercases remaining percent-escapes.

strip_tracking removes each query parameter whose name is in the package data: any utm_ prefix, and gclid, dclid, fbclid, msclkid, twclid, mc_cid, mc_eid in version 1.0.0. A query left empty is removed with its ?.

markup moves Markdown to each destination format. The input dialect is CommonMark 0.31.2 exactly, with no extensions; an extension arrives only as a package version.

Filter Signature and behavior
md_to_html string → string — CommonMark 0.31.2 rendering, with raw HTML escaped; see below
md_to_slack string → string — Slack mrkdwn; see below
md_strip string → string — the plain text; see below
nl2br string → string — inserts <br> before each newline, and changes nothing else

md_to_html produces the output that the CommonMark 0.31.2 reference tests define, with one deviation: a raw HTML block or inline is escaped as text, never passed through. A filter output that reaches an HTML mail body must not smuggle markup.

md_to_slack maps: strong to *bold*; emphasis to _italic_; a link to <url|text>; inline code and fenced code unchanged; a heading to a *bold* line; a bullet item to ; an ordered item to n. ; a block quote line to > . Constructs mrkdwn cannot express render as their plain text.

md_strip yields the rendered plain text: markers removed, a link as its text, an image as its alt text, code as its content, and blocks joined by one newline.

nl2br does not escape. Escape first: {{ note | html_escape | markup.nl2br }}.

Approximate string matching, for deduplication and reconciliation. Distances count code points with no normalization, so canonically equivalent strings can measure unequal.

Filter Signature and behavior
levenshtein(s) string → int — the edit distance to s: insertions, deletions, and substitutions, each at cost one
similarity(s) string → float1 - levenshtein / max(length_a, length_b); 1.0 when both strings are empty

Telephone number parsing and formatting. The numbering-plan metadata is package data, so a number’s validity is defined by the data version the run pinned. A region is an ISO 3166-1 alpha-2 code.

Filter Signature and behavior
parse([region]) string → object{ e164: string, region: string }; an input that is not a valid number is an evaluation fault
to_e164([region]) string → string — the E.164 form
region string → string — the number’s region
national_format([region]) string → string — the region’s national writing convention

The region argument supplies the country context for an input without a country code. An input without a country code and without the argument is an evaluation fault, as is an input that is not valid for its region. In the manifest, the argument’s default is the empty string, which means that no region context is supplied.

Exclusions the library states, beside the core exclusions:

  • data.convert. A file-format conversion matrix is open-ended, and every cell is a divergence. A conversion belongs to a vendor connector, or to a future package with a closed, fixture-pinned matrix.
  • match.soundex. An English-only heuristic, whose result is wording rather than distance.
  • Format-directed time parsing. parse_time(fmt, [locale]) joins the library when a package ships fixtures for its locale, century, and impossible-date rules.
  • Heuristics. Language detection, sentiment, and address parsing diverge across implementations. They belong behind llm.*, where nondeterminism is declared and recorded.
  • Actions in filter form. Translation, currency conversion, and geocoding are connectors. Signing is a connector’s executor concern.