Projects¶
A peg is two halves and a manifest that binds them, and one command writes all three.
Created my-peg/
workpeg.json namespace: my-peg
AGENTS.md how a coding agent should build it
CLAUDE.md imports AGENTS.md
ui/ the interface, a Scaffold app
function/ the backend
Next:
workpeg ui serve my-peg/ui/ --fake-cloud
workpeg function run --path my-peg/function/
When the folder already had an AGENTS.md or CLAUDE.md, its line reads
kept yours instead.
What a peg is¶
Two halves and a manifest.
ui/— the interface. Python that the compiler turns into browser JavaScript. Everything in Getting started applies to it.function/— the backend. A Python entrypoint packaged as a Docker image. Functions covers it, with one caveat about the generated signature noted below.workpeg.json— the manifest. It names the peg and points at the function's entrypoint.
The halves sit in subdirectories because every existing command already
expects to own its directory: workpeg ui build wants main.py where you
point it, the function commands want a Dockerfile where you point them.
Merging them would mean teaching each command a new layout; separating them
means each keeps working and only learns a path.
What you get¶
Sixteen files in four directories:
my-peg/
├── AGENTS.md ← how a coding agent should build this peg
├── CLAUDE.md ← one line, @AGENTS.md
├── workpeg.json ← the manifest: namespace, entrypoint, runtime
├── README.md ← per-peg readme
├── ui/ ← the Scaffold starter, the same one `ui new` writes
│ ├── main.py ← the Router: Landing, then the business shell
│ ├── app_shell.py ← AppShell(Shell), which returns the Scaffold
│ ├── components.py ← PageHeader and the loading, empty and error states
│ └── pages/
│ ├── __init__.py
│ ├── home.py
│ ├── customers.py ← a list over one table
│ ├── customer.py ← its detail page
│ └── not_found.py
└── function/
├── app/
│ ├── __init__.py ← empty, so app.main:main resolves
│ └── main.py ← the function half
├── requirements.txt ← empty; extra deps for the function image
├── wheels/ ← those deps' wheels; builds have no network
└── Dockerfile ← FROM the function base at your SDK's version
AGENTS.md and CLAUDE.md sit at the root, where an agent working in either
half finds them; Coding agents says what the guide covers. If
the folder already has either file, --force keeps it. README.md is
written and not mentioned in the success output.
workpeg.json is the only file both halves share. Your name goes in as
namespace; the rest is the same for every peg:
{
"namespace": "my-peg",
"function": {
"entrypoint": "app.main:main"
},
"runtime": {
"default": "docker",
"docker": {
"host": "127.0.0.1",
"port": 8000
}
}
}
ui/ is the same starter as
workpeg ui new: a Router whose pages sit
under the /b/[business_id] shell, an AppShell that returns the SDK
Scaffold, and a list page and a detail page that draw every state a data
page has. It compiles as written; build it with workpeg ui build my-peg/ui/.
function/app/main.py is what app.main:main in the manifest points at:
That is the contract in Functions: context says who and
what the call is about, with context["token"] to write back with, and
payload is the row's data or the action's arguments. An older SDK's
scaffold wrote main(event, context=None), with the two the wrong way
round; rename them in a peg made before then.
The name — lowercased and trimmed — is substituted throughout: the manifest's
namespace, the readme title, the Text(…) string, the returned peg
field. Nothing else is templated, and requirements.txt is left empty for
you to fill.
workpeg project create¶
Scaffold both halves and the manifest in one folder. Success exits 0. A
name the command refuses exits 2 and leaves nothing behind — validation
runs before the first file is written.
Failures it does not anticipate are less tidy. Pointing --path at an
existing regular file, or at a directory you cannot write to, raises a Python
traceback and exits 1. Copying is neither staged nor rolled back, so a
permission error partway through leaves a half-written tree. Both are worth
knowing before aiming --force at a directory you care about.
| Argument | Default | Description |
|---|---|---|
name |
required | The peg's namespace, and the directory name unless --path overrides it |
--path |
./<name> |
Where to write the peg; missing parent directories are created |
--force |
off | Write into a directory that already has files |
Two flags, and that is the whole surface. There is no --template: there is
one layout.
# The usual case — folder and namespace both my-peg
workpeg project create my-peg
# Namespace my-peg, folder checkout-widget
workpeg project create my-peg --path checkout-widget
# Parents are created for you
workpeg project create billing --path services/billing
# Write into a directory that already has files in it
workpeg project create my-peg --force
workpeg project on its own prints its help and exits 0. create is its
only subcommand.
--path — the folder and the namespace are separate¶
The namespace always comes from name. --path only moves the folder:
Created checkout-widget/
workpeg.json namespace: my-peg
AGENTS.md how a coding agent should build it
CLAUDE.md imports AGENTS.md
ui/ the interface, a Scaffold app
function/ the backend
Next:
workpeg ui serve checkout-widget/ui/ --fake-cloud
workpeg function run --path checkout-widget/function/
Every path in that output follows --path; only the namespace: line
follows the name.
There is no ./my-peg afterwards, and checkout-widget/workpeg.json reads
"namespace": "my-peg". The folder never names the peg. That is what
stops a git mv from quietly changing the address your peg answers to.
Absolute paths work, and so do paths outside the current directory. The
output is always printed relative to where you ran the command, so an
absolute --path is echoed back with ../ in it — the peg is where you
asked for it, the message is just describing the walk.
--force — a merge, not a reset¶
You need --force only when the target directory exists and has something
in it. An existing empty directory scaffolds without it, because the check
is emptiness rather than existence. A directory holding nothing but .git
or .DS_Store counts as non-empty and will ask for the flag.
Unrelated files survive. Re-running with --force over a directory
containing NOTES.txt leaves NOTES.txt exactly as it was.
--force overwrites the scaffold files without asking
Files the scaffold owns are rewritten from the template. An edited
ui/main.py comes back as the starter's, the edit gone, with nothing
on stdout or stderr to say so and exit 0. Use --force to drop a peg
into a directory that has other things in it, not to refresh one you
have started working in. AGENTS.md and CLAUDE.md are the exception:
one that is already there is never rewritten.
Naming a peg¶
The name becomes three things: the directory, the namespace field in
workpeg.json, and eventually the first label of <namespace>.workpeg.com.
That last one is why the rules are DNS rules and not taste — a string that
cannot be a hostname label cannot be an address, so the check runs at
creation rather than at release, when renaming would mean redoing all three.
- 3 to 63 characters
- lowercase letters, digits and hyphens only
- starts with a letter
- ends with a letter or a digit
The bounds are exact: abc is accepted and ab is not, 63 characters are
accepted and 64 are not. No underscores, no dots, no trailing hyphen.
Type the name in the shape you want it. Uppercase is not refused — it is
lowercased, but only on the way into the files. workpeg project create
MyPeg exits 0, creates a directory literally called MyPeg, prints
namespace: MyPeg, and writes "namespace": "mypeg" into the manifest.
Leading and trailing spaces do the same thing: the directory keeps them, the
namespace does not. The printed line follows what you typed, the manifest
follows the rules, and the two disagree.
Reserved names¶
Sixteen names are refused outright:
Each one names a Workpeg service rather than a peg. A peg called api would
want api.workpeg.com, which is not yours to take — so the collision is
refused at creation, where it costs you one keystroke, rather than at
release, where it costs you the name.
The pv- prefix is reserved too, though nothing uses it today. It was held
for a feature since removed, and the reservation outlived it deliberately:
holding a prefix nobody wants costs nothing, while reclaiming one after
somebody has taken pv-checkout costs them a rename and every link that
pointed at it. A true prefix match — pv-x is refused, pva-fine is fine.
When the name is refused¶
Too short, too long, a leading digit, a trailing hyphen, an underscore or a dot all produce the same message:
ERROR: 'my_peg' cannot be a hostname label. Use lowercase letters, digits and hyphens; start with a letter, end with a letter or digit, 3-63 characters.
A reserved name:
The pv- prefix:
ERROR: 'pv-something' starts with 'pv-', which Workpeg reserves for preview deploys. Pick another name.
An empty name, or one that is only whitespace:
Every message quotes the normalised value rather than what you typed, so
API is reported back as 'api' and PV-SOMETHING as 'pv-something'.
The checks run in a fixed order and the first failure is the one you see, so
a name can break two rules and only be told about one. mx is two
characters and reserved; it reports the reserved message, because the
reserved check runs before the hostname rules.
A directory that already has files is refused by its fully resolved path,
ending already exists and is not empty. Use --force to write into it
anyway. The path is the absolute one, not the one you typed.
A name starting with a hyphen never reaches any of this. Argparse claims it as a flag and answers on stderr:
usage: workpeg project create [-h] [--path PATH] [--force] name
workpeg project create: error: the following arguments are required: name
Working on each half¶
The UI half¶
Both compiler commands take the path:
Every flag documented for workpeg ui
serve and workpeg ui
build works here unchanged. The
commands did not learn a layout, only a path.
The function half¶
The local runtime reads one invocation from stdin and needs no Docker:
That is the fast loop, and it is the one to reach for while you are writing the function. Local runtime (no Docker) covers it in full, and Running with Docker covers the container path.
workpeg function test runs the function once with the envelope the
platform would send, offline, and prints its answer:
workpeg function run --path my-peg/function/ starts it in Docker. The
generated manifest sets "default": "docker", and function/ reads the
peg's workpeg.json one folder up, since it has none of its own. The other
backend, --with cracker, runs on Workpeg's engine, which is not available
yet, and says so:
See Runtime backends for which backends exist.
The function commands live under workpeg function¶
There is no top-level run. workpeg function run takes --path rather
than a positional directory, and workpeg function runtime reads the
directory you are standing in. The generated function/Dockerfile builds
FROM the function base image, whose entrypoint is
["workpeg", "function", "runtime", "--server"].
What project create does not do¶
- It does not release. Shipping both halves together is
workpeg project submit, which does not exist yet. Until it does, each half goes out through its own command. - It does not build. There is no
dist/and no Docker image until you runworkpeg ui buildorworkpeg function build. - It does not install anything.
function/requirements.txtis created empty and left alone. - It does not touch git. No repository is initialised and nothing is committed.
Which business account to build under¶
A peg runs as its business — same namespace, same API access, same data. So does every copy of it you build and run while developing, including one served from your own machine. Use a separate business account for development and keep production for the peg your customers actually use.
The cost of not doing this is not theoretical: a development build reaches whatever the business it runs as can reach, and a mistake in unfinished code lands on real rows.