Your First Spec
Install Psy, write a module, and get the compiler to tell you things about it.
Nothing in this chapter is hard. The point is to get you to a working project fast enough that the next chapters have something to talk about.
Installing it
npm install -D @psy/cliI would install it as a dev dependency rather than globally. You can go global,
but npx psy exists and pinning a version per project has never once made me
sad.
There is also a standalone binary that carries the entire toolchain — no Node, no
node_modules, nothing installed in the project it compiles:
npm run compile:install # writes ~/.local/bin/psyThat is the one I actually use. Installation in full covers building it, cross-compiling it, and what wins when a project has its own copy of the packages.
Starting from a template
psy init my-projectIt asks which template you want. If you would rather not be asked:
psy init --list # see what's on offer
psy init my-project --template configminimal is the default and is roughly what we are about to write by hand. There
are five others, including one that scaffolds a whole Claude Code plugin — see
the CLI.
Or just write one
Make a file called services.psy:
const DefaultRegion = eu-west-1
export spec BaseService:
region: DefaultRegion
replicas: 2
resources:
cpu: 500m
memory: 512Mi
tags:
- managed
export spec Api extends BaseService:
name: api
replicas: 4
tags:
+ super
- publicCompile it to JSON and you can see what all of that resolved to:
{
"BaseService": {
"region": "eu-west-1",
"replicas": 2,
"resources": { "cpu": "500m", "memory": "512Mi" },
"tags": ["managed"]
},
"Api": {
"region": "eu-west-1",
"replicas": 4,
"resources": { "cpu": "500m", "memory": "512Mi" },
"tags": ["managed", "public"],
"name": "api"
}
}Four things are worth noticing, and three of them are probably not what you expect.
Strings do not need quotes. eu-west-1, 500m, api — all ordinary
strings. Psy is a configuration language, and configuration is mostly strings, so
strings got to be the cheap thing to write. Quotes are still there when you need
them ("true" is how you write the text "true" rather than the boolean).
Indentation is the structure. No braces, no dashes-then-braces, no significant commas. Four spaces per level. Tabs are an error, and I am not sorry.
extends does what you think. Api inherits region, resources and
tags from BaseService, and overrides replicas with 4.
+ super is the interesting one. In Api.tags, the + super line means
"take everything tags inherited, then keep going" — so Api.tags ends up as
[managed, public] rather than just [public]. That is composition, it is the
feature I care most about, and it gets its own chapter.
Check it
psy check services.psyThat runs the whole pipeline — lexer, parser, semantic analysis, IR — and prints any problems with a stable code and an exact location:
services.psy:14:5 error PSY3015: `Api` does not define abstract property `name`.
framework.psy:3:5: `name` is required hereEvery error has a PSYxxxx code that does not change between versions, so you
can search for one and find the same thing next year.
Diagnostics lists them.
Inspect it
This is the command I reach for most, and the reason I bothered building the provenance machinery at all.
psy inspect ApiApi (services.psy)
abstract: false
precedence: services.psy#Api > services.psy#BaseService
tags = [
"managed",
"public"
]
declared in Api at services.psy
provenance:
block list in Api.tags (services.psy:19:9)
super BaseService.tags in BaseService.tags (services.psy:8:5)
block list in BaseService.tags (services.psy:9:9)
literal in BaseService.tags (services.psy:9:11)
literal in Api.tags (services.psy:21:11)Read that from the outside in: the block in Api.tags produced the value; it was
made of the inherited BaseService.tags followed by one literal; the inherited
part came from line 9.
When a value in a deeply inherited config is not what you expected, this is the difference between "I know exactly which line did that" and forty minutes of grep. Every resolved value carries it. See Under The Hood.
You can also ask about one property:
psy inspect Api.tagsFormat it
psy format services.psy
psy format --checkThe formatter is canonical, deterministic and idempotent. It prints from the AST, so its output is by construction valid Psy with the same meaning, and formatting already-formatted output changes nothing at all.
There is one option worth setting and one that is a preference, and both live in
psy.config.ts — see Day To Day.
What you have now
A file, a checker that has opinions about it, and a tool that can explain any value in it back to you.
What you cannot yet do is anything interesting with inheritance beyond "overriding one key" — which is where the next chapter starts.