Skip to main content

App spec reference — .works/works.yml, kind app

Normative. This file is the field-by-field contract for the App spec. The outline in CONTRACTS.md §1 fixes the top-level key names; this file fixes every field, type, default, bound, cross-field rule and issue code beneath them. Behaviour lives in spec.md; implementation in plan.md.

Epic: APW-03-app-spec-and-catalog · Status: Draft · Created: 2026-09-17 Schema: appSpecVersion: 1 inside .works/works.yml envelope version: 2 Published as: the app branch of https://api.ever.works/api/schema/works.yml.schema.json and the stand-alone https://api.ever.works/api/schema/app-spec.schema.json


0. How to read this file​

NotationMeaning
requiredAbsent ⇒ error required.
defaultThe value the platform uses when the key is absent. Defaults are never written back into the file.
NameDNS label: ^[a-z]([-a-z0-9]{0,30}[a-z0-9])?$ (1–32 chars).
EnvName^[A-Z_][A-Z0-9_]{0,127}$.
RelPathRepository-relative path, 1–255 chars; no leading /, no \, no .. segment, not under .git/.
GlobA RelPath that may contain *, **, ?; 1–200 chars.
HttpPath^/[^\s]{0,511}$.
CronFive-field cron (minute hour day-of-month month day-of-week), UTC.
CpuQuantityMillicores 250m (10m–64000m) or cores 2, 0.5 (0.01–64).
MemQuantityInteger followed by Mi or Gi, range 64Mi–256Gi.
StorageQuantityInteger followed by Mi or Gi, range 100Mi–500Gi.
ImageRefregistry/path[:tag][@sha256:<64 hex>], ≤ 512 chars.
ReferenceSee §21.
error / warningAn error stops the spec from being applied; a warning is shown and the spec still applies.

Every issue carries a stable code (§23). The UI translates codes; the App Provisioner agent reads them to self-correct.

Machine-checkable mirror. §1–§22 are also committed as JSON Schema 2020-12 in docs/specs/features/app-works/contracts/app-spec.schema.json (created by this epic's T59), with a fixture corpus beside it — contracts/fixtures/app-spec/valid/ and contracts/fixtures/app-spec/invalid/<code>.yml, each invalid file headed by the code it must report. The JSON Schema is structural only — the cross-field rules of §22 stay in code — and it is checked in CI against the same corpus the validator runs, so a Blueprint draft, a profile draft and a catalog entry can be validated before this epic's code exists. This file remains the reference: where the two disagree, this prose wins and the JSON Schema is corrected (T59 owns the guard).


1. Envelope​

# yaml-language-server: $schema=https://api.ever.works/api/schema/works.yml.schema.json
version: 2
kind: app
name: Cal.diy (community build) # v1 root keys keep their meaning
spec:
kind: app # optional; repeats the root kind
appSpecVersion: 1 # optional; default 1
source: { relation: fork, upstream: { repo: calcom/cal.diy, defaultBranch: main } }
KeyTypeDefaultNotes
kind (root)string—app. When absent, spec.kind: app selects this schema.
spec.kindstringrootMust equal the root kind when both are present — else error kind_mismatch.
spec.appSpecVersioninteger11–1000. Newer than this build understands ⇒ unknown_field issues become warnings (§2).

2. Strictness, preservation and limits​

  1. Strict reporting. Inside spec, a key this schema does not define is error unknown_field, with a suggestion when a defined key is within edit distance 2 (replica → replicas).
  2. Preservation. The platform never deletes a key it does not know. Strictness changes the validation result only; every writer round-trips the raw document (the envelope rule in docs/agent-services/works-yml-schema.md is unchanged).
  3. Extension keys. Any key starting with x- is allowed at any depth inside spec, preserved, and ignored.
  4. Newer spec. When appSpecVersion is greater than the build's supported version, unknown_field becomes warning unknown_field_newer_version; every other rule still applies.
  5. Limits. File ≤ 256 KiB (error file_too_large); nesting depth ≤ 12; YAML alias expansions ≤ 100 (error yaml_alias_limit); duplicate mapping keys are error duplicate_key; at most 200 issues are reported, then truncated: true.

3. Where validation runs​

WhereModeRuns
Editor (yaml-language-server)—Structure only (JSON Schema, §25).
Platform, on a Work's Work Repositorydata-repositoryStructure, §21, §22 and server-only rules.
Platform, draft text (validate API)data-repositoryStructure, §21, §22; server-only rules when a Work id is given.
Apps catalog CI, on a Blueprint repositoryblueprintStructure, §21, §22. source and blueprint are allowed and expected — a Blueprint's file becomes the App Work's spec, where both are present (all three APW-13 Blueprints declare both, and catalog CI check C4 requires zero errors). Corrected 2026-09-17: this row used to forbid them with blueprint_mode_forbidden_key, which no APW-13 Blueprint could satisfy — the code is kept in §23's list but is no longer emitted for these two keys. The rule that does apply in this mode: blueprint.repo must name the repository the file lives in. license is optional.

The effective spec of an App Work is the most recent evaluation of its tracked branch with zero errors. A later file with errors is reported but never replaces it; nothing is built or deployed from a commit whose own spec has errors (spec.md FR-20).

4. Top-level keys​

KeyTypeRequiredOwner (consumer)Section
sourceobjectrequired (data-repository)APW-01 writes; on the Blueprint path APW-03's apply job writes it with the App spec (R-4)§5
blueprintobject—APW-03§6
licenseobject—APW-03§7
displayobject—APW-03 (08, 11)§8
buildobjectwhen components is non-emptyAPW-05§9
componentsarray—APW-06§10
dependenciesobject—APW-07§11
envarray—APW-07§12
jobsarray—APW-06§13
cronarray—APW-06§14
domainsobject—APW-06§15
smokearray—APW-06 (04)§16
checksarray—APW-08§17
agentsobject—APW-08§18
upstreamSyncobject—APW-02§19
upstreamPullRequestsobject—APW-09§20
provisioningobject—APW-04§20

"Owner" is the epic whose runtime consumes the block; APW-03 owns the schema of every block.

5. source​

FieldTypeDefaultRules
relationenumrequiredfork · private-copy · link.
upstream.repostringrequired unless link^[A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$. Forbidden when link (upstream_forbidden_for_link).
upstream.defaultBranchstringupstream's default at creationGit ref name, 1–255 chars.
branchstringWork Repository default branchGit ref name, 1–255 chars, not ending .lock, no .., no //. The branch built and deployed.

Server-only rule: relation must equal the relation recorded when the App Work was created (error source_relation_mismatch) — a hand edit cannot turn a fork into a link.

6. blueprint​

Written by the platform when an App Blueprint is applied; informational afterwards.

FieldTypeDefaultRules
idstringrequired^[a-z0-9][a-z0-9-]{0,63}$.
versionstringrequiredSemantic version MAJOR.MINOR.PATCH.
repostringrequired^ever-works/[a-z0-9-]+$ (error blueprint_repo_outside_org).
shastringrequired^[0-9a-f]{40}$.

Server-only: an id the Apps catalog does not list is warning blueprint_unknown (upgrade notices stop).

7. license​

Informational. The license gate classifies from detection, never from this block.

FieldTypeDefaultRules
spdxstring—SPDX expression ≤ 200 chars (MIT, AGPL-3.0-only, MIT OR Apache-2.0, LicenseRef-<id>).
classenum—green · amber · red · unknown.
sourceenum—detected · blueprint · user.
noticestring—≤ 500 chars. Trademark / attribution notice shown with the app.
sourceOfferUrlstring—https:// URL ≤ 500 chars. Where network users obtain the source when the Work Repository is private.

Server-only: a declared spdx or class that differs from detection is warning license_declared_mismatch.

8. display​

FieldTypeDefaultRules
namestringWork name1–80 chars.
protectedPathsGlob[][]≤ 50 entries. Agents may not change matching files (D13; enforced by APW-08).

9. build​

FieldTypeDefaultRules
strategyenumnone when components is emptydockerfile · image · auto · none. Required when components is non-empty.
dockerfileRelPathDockerfileOnly with dockerfile.
contextRelPath.Only with dockerfile / auto.
targetstring—^[A-Za-z0-9._-]{1,64}$. Only with dockerfile.
imageImageRefrequired with imageTag-only reference ⇒ warning image_not_pinned.
args[]array[]≤ 50. Each { name: EnvName, value?: string ≤ 1000, fromEnv?: EnvName }, exactly one of value / fromEnv.
services[]array[]≤ 5. Each { name: Name, image: ImageRef, port?: 1–65535, env?: [{ name, value }] ≤ 20 }. Ephemeral, build-only.
resources.cpunumber21–16.
resources.memoryMemQuantity7Gi1Gi–64Gi.
resources.timeoutMinutesinteger605–180.

Strategies (CONTRACTS.md Resolution R-13): dockerfile builds the named Dockerfile; image deploys a prebuilt image and runs no Build; none builds and runs nothing; auto is a zero-config build — the build plugin detects the language and framework from the repository and builds an image without a Dockerfile. Which builder implements auto is the build plugin's choice and is never named in the App spec. When no enabled build plugin lists auto among its supported strategies, the server-only warning build_strategy_unavailable is reported (§22) and APW-05 refuses the Build.

10. components​

At most 10. At least 1 when build.strategy ≠ none (error strategy_requires_components).

FieldTypeDefaultRules
nameNamerequiredUnique among components.
roleenumrequiredweb (Service + Ingress) · worker.
commandstring[]image entrypoint≤ 20 items, each ≤ 1000 chars.
argsstring[]image cmd≤ 50 items, each ≤ 1000 chars.
targetstringbuild.targetDockerfile stage override for this component.
portintegerrequired for web1–65535. Forbidden for worker (worker_port_forbidden).
replicasinteger10–10.
writableRootFilesystembooleanfalse
runAsUserinteger— (the image's own user)Added 2026-09-17 (APW06-G26). 1–4294967294. The numeric uid the container must run as. Needed because an image whose USER is a name (umami's nextjs) cannot satisfy runAsNonRoot — the kubelet refuses it with "image has non-numeric user" (APW-06 plan §4.4, §5.1 image_user_unverifiable), so the App would be undeployable on both targets with no field that could rescue it. The renderer passes it through verbatim and never derives one; on Ever Works Apps it is allowed but the tier's own restricted policy still applies. Omit it and the field is absent from the rendered pod spec, so every existing App spec renders byte-identically.
probes.{startup,readiness,liveness}objectreadiness: { tcp: true } for webSee below.
resources.cpuCpuQuantity250m
resources.memoryMemQuantity512MiRequest.
resources.cpuLimitCpuQuantity—≥ cpu.
resources.memoryLimitMemQuantity2 × memory≥ memory (limit_below_request).
volumes[]array[]≤ 5. { name: Name, path: absolute ≤ 255, size: StorageQuantity, backup: boolean = true }; unique names and paths.

Probe object: exactly one of http: HttpPath or tcp: true; periodSeconds 1–300 (default 10); timeoutSeconds 1–60 (default 5); initialDelaySeconds 0–600 (default 0); failureThreshold 1–120 (default 3, startup default 30). A worker probe must use tcp only with a declared port, so workers use no probe or an http probe on a port they open (warning worker_probe_without_port).

11. dependencies​

FieldTypeDefaultRules
postgres.versionenum"16""14" · "15" · "16" · "17".
postgres.directUrlbooleanfalseAlso provide a non-pooled URL.
postgres.extensionsstring[][]≤ 10, each ^[a-z0-9_]{1,63}$. Availability is provider-specific (warning extension_unavailable).
redis.versionenum"7""7".
redis.maxmemoryPolicyenumnoevictionnoeviction · allkeys-lru · volatile-lru · allkeys-lfu · volatile-lfu.
redis.persistencebooleanfalse
objectStorage.bucketsName[]required1–10, unique.
objectStorage.publicBucketsName[][]Subset of buckets (public_bucket_undeclared).
smtp.requiredbooleanfalsetrue ⇒ deploy is blocked until SMTP is configured (APW-07).

Outputs a from: may reference. Proposed here; the normative list is APW-07's APP_DEPENDENCY_OUTPUTS and this table must equal it before either epic merges.

DependencyOutputs (secret ones marked †)
postgresurl†, directUrl† (only with directUrl: true), host, port, database, user, password†
redisurl†, host, port, password†
objectStorageendpoint, region, accessKeyId†, secretAccessKey†, bucket.<name> (one per declared bucket)
smtphost, port, user, password†, from, secure

12. env[]​

At most 200 entries; name unique. Each entry has exactly one value source (error env_source_count): value, from, template, generate or prompt.

FieldTypeDefaultRules
nameEnvNamerequired
secretbooleanfalseStored encrypted, never logged or returned (Constitution VII).
phaseenumruntimeruntime · build · both.
descriptionstring—≤ 300 chars.
valuestring—≤ 4096 chars. Forbidden when secret: true (error literal_secret_value).
fromReference—§21.
templatestring—≤ 2048 chars, §21.
generateobject—See below. Implies secret: true; secret: false is error generated_not_secret.
validateobject—length 1–65536, minLength, maxLength (≤ 65536, min ≤ max), pattern ≤ 500 chars, RE2 syntax (no back-references or look-around: pattern_unsupported).
promptobject—description required 1–300; required boolean (default true); example ≤ 200 (secret-scanned: prompt_example_secret); group ≤ 40.

generate

FieldTypeDefaultRules
kindenumrequiredbase64 · hex · chars · uuid · keypair.
bytesinteger3216–128. base64 and hex only.
lengthinteger3216–256. chars only.
alphabetenumalnumalnum · alnum-symbols · hex-lower · base64url. chars only.
keypairobject{ type: ed25519 }keypair only. See below. Public half exposed as <NAME>_PUBLIC only.
rotateenumnevernever is the only value in appSpecVersion: 1.

generate.keypair (CONTRACTS.md Resolution R-11)

FieldTypeDefaultRules
typeenumed25519ed25519 · ec-p256 · rsa-2048 · rsa-4096.
formatenumpempem · base64url-raw · pkcs12. base64url-raw only with ed25519 or ec-p256 (error keypair_format_unsupported, R25).
passwordEnvEnvName—Required with pkcs12, forbidden otherwise; names another env entry that is secret: true and generated with kind base64, hex or chars (error keypair_password_invalid, R26).

What each format stores — the private half in the entry itself, the public half in <NAME>_PUBLIC (a derived, non-secret value; nothing else about the key pair is exposed):

formatPrivate half (<NAME>)Public half (<NAME>_PUBLIC)
pemPKCS #8 PEM (-----BEGIN PRIVATE KEY-----).SubjectPublicKeyInfo PEM (-----BEGIN PUBLIC KEY-----).
base64url-rawThe raw private key bytes, base64url without padding: ed25519 32-byte seed and ec-p256 32-byte scalar are both 43 characters.The raw public key, base64url without padding: ed25519 32 bytes (43 characters); ec-p256 uncompressed 65-byte point (87 characters).
pkcs12A PKCS #12 archive (the private key plus a self-signed certificate whose subject names the entry), standard base64 with padding, encrypted with the value of passwordEnv.SubjectPublicKeyInfo PEM.
env:
# PEM, the default — a JWT signing key
- { name: JWT_SIGNING_KEY, secret: true, generate: { kind: keypair, keypair: { type: ed25519 } } }
# Raw base64url — web-push keys; the fixed 43-character length may be validated
- {
name: VAPID_PRIVATE_KEY,
secret: true,
generate: { kind: keypair, keypair: { type: ec-p256, format: base64url-raw } },
validate: { length: 43, pattern: '^[A-Za-z0-9_-]{43}$' }
}
# PKCS #12 — protected by a separately generated password
- { name: SAML_KEY_PASSWORD, secret: true, generate: { kind: chars, length: 32, alphabet: alnum } }
- {
name: SAML_SIGNING_KEY,
secret: true,
generate: { kind: keypair, keypair: { type: rsa-2048, format: pkcs12, passwordEnv: SAML_KEY_PASSWORD } }
}
# Invalid — rsa has no raw form (keypair_format_unsupported); pkcs12 without a password (keypair_password_invalid)
- { name: BAD_RAW, secret: true, generate: { kind: keypair, keypair: { type: rsa-4096, format: base64url-raw } } }
- { name: BAD_P12, secret: true, generate: { kind: keypair, keypair: { type: ec-p256, format: pkcs12 } } }

Generated length (used by rule R9): hex = 2 × bytes; base64 = 4 × ceil(bytes / 3); chars = length; uuid = 36; keypair with format: base64url-raw = 43; any other keypair has no fixed length (any validate.length is error generate_validate_conflict). An entry may not declare <NAME>_PUBLIC for a keypair entry <NAME> (error duplicate_name, R4).

13. jobs[]​

At most 10; name unique.

FieldTypeDefaultRules
nameNamerequired
whenenumrequiredpre-deploy · first-deploy · post-deploy. first-deploy jobs run before the app is exposed publicly.
componentNamedomains.primaryComponentMust name a component (component_ref_unknown); the job uses its image and env. An http job needs a web component (http_job_requires_web_component).
commandstring[]—Exactly one of command / http. ≤ 20 items × 1000 chars.
http.methodenumPOSTGET · POST · PUT · PATCH · DELETE.
http.pathHttpPathrequiredSent to the component's port inside the cluster, never through the public URL.
http.bodyJSON—≤ 16 KiB serialized. String leaves may contain {{…}} placeholders (§21).
http.authEnvEnvName—Must name a secret: true entry.
http.authSchemeenumbearerbearer sends Authorization: Bearer <value>; raw sends Authorization: <value> (CONTRACTS §1, APW-13 addition). Only with authEnv.
http.expect.statusint[][200, 201, 204]1–10 codes, 100–599.
timeoutSecondsinteger60010–3600.
retriesinteger00–3.

14. cron[]​

The app's own recurring calls, rendered as cluster CronJobs — not platform Schedules. At most 20.

FieldTypeDefaultRules
nameNamerequiredUnique.
scheduleCronrequiredcron_invalid when unparsable.
componentNamedomains.primaryComponent
command / http—exactly onehttp as in §13, including authEnv and authScheme.
timeoutSecondsinteger30010–3600.
concurrencyenumforbidforbid · allow.

15. domains​

FieldTypeDefaultRules
primaryComponentNamethe only web componentMust name a web component. Required with 2+ web components.
publicUrlEnvEnvName[][]≤ 10; each names an env entry.
onChangeenumrestartrestart · rebuild.
needsHairpinbooleanfalseThe app calls its own public URL from the server side.

16. smoke[]​

At most 20; name unique. Smoke requests never follow redirects, so expect.status judges the first response (CONTRACTS §1).

FieldTypeDefaultRules
http.methodenumGETGET · HEAD · POST.
http.pathHttpPathrequired
http.bodyJSON—≤ 16 KiB, POST only.
componentNamedomains.primaryComponentMust be web.
expect.statusint[][200]1–10 codes.
expect.bodyContainsstring[][]≤ 5 × 200 chars.
expect.bodyNotContainsstring[][]≤ 5 × 200 chars.
expect.maxLatencyMsinteger10000100–60000.
whenenumalwaysalways · first-deploy.

17. checks[]​

Quality gates for Tasks on this App Work; run sandboxed (README §7 rule 9). At most 20; name unique.

FieldTypeDefaultRules
nameNamerequired
commandstringrequired1–500 chars, at least one non-whitespace character, no control characters.
requiredbooleantruefalse is warning advisory_check — an advisory check verifies nothing.
timeoutSecondsinteger180060–7200.

18. agents​

FieldTypeDefaultRules
instructionFilesRelPath[][]≤ 10.
maxPullRequestChangedLinesinteger50050–5000.
maxPullRequestChangedFilesinteger501–500.
requireHumanMergePathsGlob[][]≤ 50 entries. Paths whose changes only a person may merge, whatever the merge policy says (semantics: APW-08; declared in CONTRACTS §1 "Additions (APW-08)". Removals from this list are reported by diffGuardedSpecBlocks — see CONTRACTS §2A).

19. upstreamSync​

Forbidden when source.relation is link (upstream_sync_requires_upstream).

FieldTypeDefaultRules
enabledbooleantrue
scheduleCron0 6 * * 1Consecutive fires at least 60 minutes apart (schedule_too_frequent).
modeenummergemerge only.
branchstringupstream.defaultBranchGit ref name.

20. upstreamPullRequests and provisioning​

FieldTypeDefaultRules
enabledbooleanfalsetrue requires source.relation: fork (upstream_prs_require_fork).
requireApprovalbooleantrueOnly true is valid (upstream_pr_approval_required).
maxOpeninteger31–10.

provisioning (CONTRACTS §1, APW-04 addition; the App Provisioner never writes it):

FieldTypeDefaultRules
autoReprovisionbooleanfalseThe owner's opt-in to automatic re-provisioning when an Upstream sync breaks the smoke tests.

21. Reference syntax​

Reference := DomainRef | DepRef | PlatformRef | BuildRef | ComponentRef
DomainRef := "domains.primary." ( "url" | "host" )
BuildRef := "build.commitSha" ; CONTRACTS §1, APW-13 addition
ComponentRef := "components." Name ".internalUrl" ; CONTRACTS §1, APW-13 addition
DepRef := "deps." DepKind "." Output
DepKind := "postgres" | "redis" | "objectStorage" | "smtp"
Output := Identifier | "bucket." Name ; bucket.<name> for objectStorage only
PlatformRef := "platform.smtp." ( "host" | "port" | "user" | "password" | "from" | "secure" )
Template := { Literal | "{{" Space* ( Reference | EnvRef ) Space* "}}" }
EnvRef := "env." EnvName
FromEnv := EnvName ; build.args[].fromEnv only
ConstructResolves whenOtherwise
domains.primary.*at least one web component existsreference_unresolved
deps.<kind>.<out>dependencies.<kind> is declared and <out> is one of its outputs (§11)reference_unresolved
platform.smtp.*dependencies.smtp is declaredreference_unresolved
build.commitShabuild.strategy is dockerfile or auto (a Build produces the image)reference_unresolved
components.<n>.internalUrla component named <n> exists and is webreference_unresolved
env.<NAME>an env entry named <NAME> exists and is not the entry itselfreference_unresolved / template_cycle
fromEnvan env entry exists with phase build or bothreference_unresolved / phase_mismatch
any placeholdermatches the grammarreference_syntax

Secrecy propagates. An entry whose from names a † output, or whose template references a † output or an env entry with secret: true, must itself be secret: true (error secret_reference_not_secret). Phase propagates. A runtime entry cannot template a build-only entry and vice versa (phase_mismatch). Cycles among template entries are error template_cycle naming every entry in the cycle. Resolution depth ≤ 10 (template_too_deep).

22. Cross-field rules​

#RuleCodeSeverity
R1A web component declares port.web_component_needs_porterror
R2build.strategy ≠ none ⇒ at least one component; components ⇒ strategy declared.strategy_requires_components / components_require_strategyerror
R3domains.primaryComponent names a web component; required with 2+ web components.primary_component_invaliderror
R4Names are unique within components, jobs, cron, smoke, checks; env names unique, counting the implicit <NAME>_PUBLIC of each keypair entry.duplicate_nameerror
R5Every from: / template: / fromEnv: resolves (§21).reference_unresolvederror
R6Secrecy and phase propagate (§21).secret_reference_not_secret / phase_mismatcherror
R7Each env entry has exactly one value source.env_source_counterror
R8A secret: true entry has no value.literal_secret_valueerror
R9generate and validate agree: validate.length equals the generated length (§12); minLength ≤ generated ≤ maxLength; pattern matches 3 sample values generated from a fixed seed.generate_validate_conflicterror
R10build.args[].value contains no secret: the platform secret scanner matches the value, or the arg name contains SECRET, TOKEN, PASSWORD, PASSWD, PRIVATE, CREDENTIAL, APIKEY or API_KEY and the literal is non-empty. The value is never echoed.literal_secret_in_build_argserror
R11build.args[].fromEnv naming a secret: true entry — the value is baked into image layers.secret_build_argwarning
R12upstreamPullRequests.requireApproval is true.upstream_pr_approval_requirederror
R13source.relation: link ⇒ no source.upstream, no upstreamSync, upstreamPullRequests.enabled not true.upstream_forbidden_for_link · upstream_sync_requires_upstream · upstream_prs_require_forkerror
R14jobs[].component, cron[].component, smoke[].component name existing components; smoke targets web.component_ref_unknownerror
R15http.authEnv names a secret: true entry.auth_env_not_secreterror
R16domains.publicUrlEnv[] names existing env entries.reference_unresolvederror
R17memoryLimit ≥ memory, cpuLimit ≥ cpu.limit_below_requesterror
R18A component with volumes and replicas > 1 — a volume cannot attach to two pods (aligned with APW-06).volume_replicaserror
R19build.strategy: image with a tag-only reference.image_not_pinnedwarning (error in blueprint mode for verified entries)
R20checks[].required: false.advisory_checkwarning
R21upstreamSync.schedule fires at most once per 60 minutes.schedule_too_frequenterror
R22display.protectedPaths and every RelPath are relative and stay inside the repository.path_outside_repositoryerror
R23No env entry is named EVER_WORKS_* — that prefix is reserved for platform-injected values.reserved_env_nameerror
R24license.class: green declared while license.spdx maps to another class in the registry.license_declared_mismatchwarning
R25generate.keypair.format: base64url-raw is used only with type ed25519 or ec-p256 (R-11).keypair_format_unsupportederror
R26generate.keypair.passwordEnv is present exactly when format: pkcs12, and names another secret: true entry generated with kind base64, hex or chars (R-11).keypair_password_invaliderror
R27license.sourceOfferUrl is required whenever the Work Repository is not public (private fork or private copy), because the Source link is the licence's network-source-offer condition; a public repository satisfies it by being public. Added 2026-09-17: ACC-03-36 already asserted sourceOfferMissing, but that code existed in no rule table and no code list, so the scenario could not pass.sourceOfferMissingerror

Server-only rules (need platform state, so editors cannot run them): source_relation_mismatch (§5), blueprint_unknown (§6), license_declared_mismatch against detection (§7), build_strategy_unavailable (no enabled build plugin lists the strategy — for example auto — among its supported strategies — warning), dependency_unavailable (no enabled app-dependency provider for the kind on the selected target — warning), tracked_branch_missing (error).

The context those rules read (RuleContext) — this table is the whole of it, and the validator reads nothing else:

FieldTypeWho fills itRule it serves
recordedRelationlink · fork · private-copythe Work's sourceRepository.typesource_relation_mismatch (§5)
catalogIdsstring[], or null when the catalog is unreachableAPW-03's catalog registryblueprint_unknown (§6)
catalogUnavailablebooleanthe same readsuppresses blueprint_unknown instead of reporting it
buildStrategiesstring[], or null when no build plugin is enabledIBuildPlugin.supportedStrategies over the enabled build pluginsbuild_strategy_unavailable
dependencyProvidersproviders by kind for the Work's deploy target, or null when unknownIAppDependencyProvider.supports(kind, target, ctx) over the configured providersdependency_unavailable
trackedBranchExistsboolean, or null when the branch could not be readGitFacadeService.getLatestCommit on the tracked branchtracked_branch_missing

A field left null (unknown) skips its rule — nothing is reported for state the platform could not read. That is what keeps a valid App spec from showing "valid, with warnings" just because APW-05 or APW-07 has not merged yet, and it is why build_strategy_unavailable applies only to strategies that need a builder (dockerfile and auto): image and none name no builder, so an empty plugin list never warns about them.

Which structural problems suppress the rules. The rule set (§21, §22) runs whenever the document parses. Only these suppress it completely: yaml_syntax, file_too_large, yaml_alias_limit and the depth limit. Every other structural problem (unknown_field, invalid_type, out_of_range, pattern, …) is reported together with the rule findings, which are computed over a best-effort copy of the document with the offending keys and invalid leaves removed — so §24.4 reports unknown_field and its five rule codes in one response, and a single invalid leaf never produces a duplicate report for the same path. When a subtree cannot be copied at all, the issue that names it records which rules were skipped in its params.

23. Issue object and codes​

{
"code": "web_component_needs_port",
"severity": "error",
"path": "spec.components[0].port",
"pointer": "/spec/components/0/port",
"displayPath": "components › web › port",
"line": 41,
"column": 7,
"message": "Web components must declare the port they listen on.",
"hint": "Add `port: <number>` under the `web` component.",
"params": { "component": "web" }
}
  • path uses array indexes; displayPath uses component/job/env names where they exist.
  • line/column are 1-based and point at the key when present, else at the nearest parent key.
  • message and hint are English and never contain a value from the file for secret entries, build.args, or prompt.example.
  • Codes are append-only. Removing or renaming a code is a breaking change (Constitution X).

Structural codes: yaml_syntax, file_too_large, yaml_alias_limit, duplicate_key, kind_mismatch, required, invalid_type, invalid_enum, out_of_range, pattern, unknown_field, unknown_field_newer_version, blueprint_mode_forbidden_key, pattern_unsupported, prompt_example_secret, generated_not_secret, worker_port_forbidden, worker_probe_without_port, http_job_requires_web_component, public_bucket_undeclared, extension_unavailable, cron_invalid, reference_syntax, template_cycle, template_too_deep, blueprint_repo_outside_org. Rule codes: every code in §22.

24. Examples​

24.1 Single container with Postgres — Cal.diy (the program's running example)​

version: 2
kind: app
name: Cal.diy (community build)
spec:
source: { relation: fork, upstream: { repo: calcom/cal.diy, defaultBranch: main }, branch: main }
blueprint: { id: cal, version: 1.0.0, repo: ever-works/cal-template, sha: 0123456789abcdef0123456789abcdef01234567 }
license: { spdx: MIT, class: green, source: blueprint, notice: 'Cal.diy® is a trademark of Cal.com, Inc.' }
display: { name: 'Cal.diy (community build)', protectedPaths: ['apps/web/public/brand/**'] }
build:
strategy: dockerfile
dockerfile: Dockerfile
target: runner
args:
- { name: NEXT_PUBLIC_WEBAPP_URL, value: 'http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER' }
- { name: CALENDSO_ENCRYPTION_KEY, fromEnv: CALENDSO_ENCRYPTION_KEY } # R11 warning, accepted
services:
[
{
name: postgres,
image: 'postgres:16',
port: 5432,
env: [{ name: POSTGRES_PASSWORD, value: build-only }]
}
]
resources: { cpu: 4, memory: 12Gi, timeoutMinutes: 60 }
components:
- name: web
role: web
command: ['/calcom/scripts/start.sh']
port: 3000
writableRootFilesystem: true
probes:
startup: { http: /api/version, periodSeconds: 10, failureThreshold: 60 }
readiness: { http: /auth/login }
liveness: { http: /api/version, periodSeconds: 30 }
resources: { cpu: 500m, memory: 1Gi, memoryLimit: 3Gi }
dependencies:
postgres: { version: '16', directUrl: true }
smtp: { required: true }
env:
- { name: NEXTAUTH_SECRET, secret: true, generate: { kind: base64, bytes: 32 } }
- {
name: CALENDSO_ENCRYPTION_KEY,
secret: true,
phase: both,
generate: { kind: chars, length: 32, alphabet: alnum },
validate: { length: 32 }
}
- { name: CRON_API_KEY, secret: true, generate: { kind: hex, bytes: 32 } }
- { name: DATABASE_URL, secret: true, from: deps.postgres.url }
- { name: DATABASE_DIRECT_URL, secret: true, from: deps.postgres.directUrl }
- { name: NEXT_PUBLIC_WEBAPP_URL, from: domains.primary.url }
- { name: NEXTAUTH_URL, template: '{{domains.primary.url}}/api/auth' }
- { name: EMAIL_SERVER_HOST, from: deps.smtp.host }
- { name: EMAIL_SERVER_PASSWORD, secret: true, from: deps.smtp.password }
- {
name: GOOGLE_API_CREDENTIALS,
secret: true,
prompt: { description: 'Google Calendar OAuth JSON', required: false }
}
- { name: CALCOM_TELEMETRY_DISABLED, value: '1' }
jobs:
- {
name: migrate,
when: pre-deploy,
component: web,
command: ['npx', 'prisma', 'migrate', 'deploy'],
timeoutSeconds: 900
}
cron:
- {
name: booking-reminder,
schedule: '*/15 * * * *',
http: { method: POST, path: /api/cron/bookingReminder, authEnv: CRON_API_KEY, authScheme: raw }
}
domains:
{
primaryComponent: web,
publicUrlEnv: [NEXT_PUBLIC_WEBAPP_URL, NEXTAUTH_URL],
onChange: restart,
needsHairpin: true
}
smoke:
- { name: version, http: { path: /api/version }, expect: { status: [200] } }
- { name: login, http: { path: /auth/login }, expect: { status: [200], bodyNotContains: ['localhost:3000'] } }
checks:
- { name: type-check, command: 'yarn type-check:ci --force', timeoutSeconds: 1800 }
agents: { instructionFiles: [AGENTS.md], maxPullRequestChangedLines: 500 }
upstreamSync: { schedule: '0 6 * * 1', mode: merge }
upstreamPullRequests: { enabled: false, requireApproval: true }

24.2 Prebuilt image — a small analytics app (illustrative)​

version: 2
kind: app
spec:
source: { relation: fork, upstream: { repo: example-org/analytics, defaultBranch: main } }
build:
{
strategy: image,
image: 'ghcr.io/example-org/analytics@sha256:9f2c1e0b7a4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6b5a49'
}
components:
- name: web
role: web
port: 3000
probes: { startup: { http: /api/heartbeat, failureThreshold: 30 }, readiness: { http: /api/heartbeat } }
resources: { cpu: 250m, memory: 512Mi, memoryLimit: 1Gi }
dependencies: { postgres: { version: '16' } }
env:
- { name: DATABASE_URL, secret: true, from: deps.postgres.url }
- { name: APP_SECRET, secret: true, generate: { kind: hex, bytes: 32 }, validate: { length: 64 } }
- { name: ADMIN_PASSWORD, secret: true, generate: { kind: chars, length: 24, alphabet: alnum-symbols } }
- { name: DISABLE_TELEMETRY, value: '1' }
jobs:
- name: set-admin-password
when: first-deploy
component: web
http:
{
method: POST,
path: /api/setup/admin,
body: { password: '{{env.ADMIN_PASSWORD}}' },
expect: { status: [200, 201] }
}
smoke:
- { name: heartbeat, http: { path: /api/heartbeat }, expect: { status: [200], maxLatencyMs: 2000 } }

build.strategy: image means no Build runs (APW-05); the checks, Upstream sync and evolve loop still work on the fork, but a code change only reaches production once the image is built by some other means.

24.3 Web + worker with Redis — a help-desk app (illustrative)​

version: 2
kind: app
spec:
source: { relation: private-copy, upstream: { repo: example-org/helpdesk, defaultBranch: main } }
build: { strategy: dockerfile, dockerfile: docker/Dockerfile, context: ., resources: { memory: 6Gi } }
components:
- {
name: web,
role: web,
command: ['node', 'dist/server.js'],
port: 8080,
probes: { readiness: { http: /healthz }, liveness: { http: /healthz, periodSeconds: 30 } },
resources: { cpu: 500m, memory: 768Mi, memoryLimit: 1536Mi }
}
- {
name: worker,
role: worker,
command: ['node', 'dist/worker.js'],
replicas: 2,
resources: { cpu: 250m, memory: 512Mi }
}
dependencies:
postgres: { version: '16' }
redis: { version: '7', maxmemoryPolicy: noeviction }
objectStorage: { buckets: [attachments] }
env:
- { name: DATABASE_URL, secret: true, from: deps.postgres.url }
- { name: REDIS_URL, secret: true, from: deps.redis.url }
- { name: S3_ENDPOINT, from: deps.objectStorage.endpoint }
- { name: S3_BUCKET, from: deps.objectStorage.bucket.attachments }
- { name: S3_ACCESS_KEY_ID, secret: true, from: deps.objectStorage.accessKeyId }
- { name: S3_SECRET_ACCESS_KEY, secret: true, from: deps.objectStorage.secretAccessKey }
- { name: SESSION_SECRET, secret: true, generate: { kind: base64, bytes: 48 }, validate: { length: 64 } }
- { name: PUBLIC_URL, from: domains.primary.url }
- { name: SIGNUP_ENABLED, value: 'false' }
jobs:
- { name: migrate, when: pre-deploy, component: web, command: ['node', 'dist/migrate.js'] }
cron:
- { name: purge-trash, schedule: '30 3 * * *', component: worker, command: ['node', 'dist/purge.js'] }
smoke:
- { name: health, http: { path: /healthz }, expect: { status: [200] } }
upstreamSync: { schedule: '0 5 * * *' }
upstreamPullRequests: { enabled: false } # a private copy cannot open upstream pull requests (D2)

24.4 What an invalid file reports​

spec:
source: { relation: fork, upstream: { repo: example-org/helpdesk } }
build: { strategy: dockerfile, args: [{ name: PAYMENTS_SECRET_KEY, value: '<a real key pasted here>' }] }
components: [{ name: web, role: web, replica: 2 }]
env: [{ name: DATABASE_URL, from: deps.postgres.url }]
upstreamPullRequests: { requireApproval: false }
CodedisplayPathMessage
literal_secret_in_build_argsbuild › args › PAYMENTS_SECRET_KEY › valueBuild arguments are stored in image layers. Reference an env entry with fromEnv.
unknown_fieldcomponents › web › replicaUnknown field replica. Did you mean replicas?
web_component_needs_portcomponents › web › portWeb components must declare the port they listen on.
reference_unresolvedenv › DATABASE_URL › fromdeps.postgres.url needs dependencies.postgres.
secret_reference_not_secretenv › DATABASE_URL › secretDATABASE_URL reads a secret output, so it must be secret: true.
upstream_pr_approval_requiredupstreamPullRequests › requireApprovalUpstream pull requests always need a person's approval.

25. Publication​

ArtifactLocation
Runtime validator (source of truth)packages/agent/src/works-config/schema/app-spec.schema.ts (plan §2.2)
Envelope JSON Schema with the app branchpackages/agent/src/works-config/schema/works.v2.schema.json (committed, drift-guarded)
Stand-alone App spec JSON Schemapackages/agent/src/works-config/schema/app-spec.v1.schema.json (committed, drift-guarded)
ServedGET /api/schema/works.yml.schema.json, GET /api/schema/app-spec.schema.json
Vendored copy for Blueprint CIever-works/apps → schema/app-spec.schema.json (catalog.md §6)

JSON Schema cannot express §21–§22; editors validate structure, the platform validates everything.