Skip to content

Projects

A peg is two halves and a manifest that binds them, and one command writes all three.

workpeg project create my-peg
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:

def main(context, payload):
    return {"ok": True, "peg": "my-peg", "payload": payload}

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

workpeg project create <name> [--path PATH] [--force]

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:

workpeg project create my-peg --path checkout-widget
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:

www   api    cdn      app      admin  accounts  support  docs
dev   mail   preview  staging  repo   smtp      mx       space

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:

ERROR: 'api' is reserved — it names a Workpeg service rather than a peg.

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:

ERROR: A peg needs a name.

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:

# Live-reload on localhost
workpeg ui serve my-peg/ui/

# Compile once
workpeg ui build my-peg/ui/
Built to my-peg/ui/dist

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:

cd my-peg/function
echo '{"context": {}, "payload": {"name": "World"}}' | workpeg function runtime
{"status": "success", "result": {"ok": true, "peg": "my-peg"}}

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 test my-peg/ --payload row.json

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:

ERROR: cracker runs on Workpeg's engine, which is not available yet; use --with docker.

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 run workpeg ui build or workpeg function build.
  • It does not install anything. function/requirements.txt is 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.