Managing SaaS Pegasus projects
You are managing a SaaS Pegasus project on behalf of the user. Pegasus is a Django boilerplate; a "project" is a configuration of features (frontend framework, auth providers, billing, AI, etc.) that the Pegasus build pipeline later renders into a real Django codebase. Your job is to translate the user's intent into the right CLI calls.
Running the CLI
The Pegasus CLI is published on PyPI as pegasus-cli (binary name
pegasus) and also ships installed in every Pegasus project's venv,
pinned to that project's release version.
Pick the invocation form at session start:
- Run
command -v pegasus. - If it returns a path → use bare
pegasus .... This is the preferred form whenever it's available — the local install is version-matched to the project and may even be ahead of what's on PyPI (e.g. unreleased CLI changes that ship with the local repo). - If it returns nothing → use
uvx --from pegasus-cli pegasus ...for every command. The--fromis required because the binary name differs from the package name. Works from any cwd, no venv needed. First call caches the env (a few seconds); subsequent calls are fast.
Don't mix forms within a session. Pick one based on the
command -v result and stay consistent — including for the
auth login command the user runs in a separate terminal (tell them
to use the same form you're using).
For readability, the rest of this document writes commands as bare
pegasus .... Mentally prepend uvx --from pegasus-cli if you're in
the uvx case.
Setup (one time)
Authentication uses an API key from saaspegasus.com.
- Check auth status with
pegasus auth status— non-interactive, safe to run from a Bash tool call. It exits 0 when a key is configured (via~/.pegasus/credentialsor$PEGASUS_API_KEY) and non-zero otherwise. - If unauthenticated, do not try to log in from this session.
pegasus auth loginis interactive (it pauses waiting for the user to paste their key) and will hang if you run it via Bash or the!prefix. Instead, tell the user to open a separate terminal and runpegasus auth loginthemselves. They'll be prompted to paste a key from https://www.saaspegasus.com/. Once that finishes, re-runpegasus auth statushere to confirm and continue. - Alternative for advanced users: export
PEGASUS_API_KEY=<their-key>in the shell that launched Claude Code, then restart this session. - Never try to guess or generate a key yourself — only the user has it.
Commands
pegasus projects list # list all projects
pegasus projects fields --json # schema for a new project
pegasus projects fields --for <id> --json # schema for an existing project
pegasus projects show <id> --json # full config of one project
pegasus projects create --json [--set k=v ...] [--config-file path]
pegasus projects update <id> --json [--set k=v ...] [--config-file path]
pegasus projects push <id> # push to GitHub (separate flow)
ALWAYS pass --json when you (an agent) are inspecting output. The
default is a Rich table for humans that may truncate or scroll past your
visible viewport — a 60+ field schema looks like fields are missing when
they aren't. JSON output is always complete and parseable. Treat tables as
human-only.
The standard workflow
For any non-trivial create or update, work in this order:
Get the schema. The endpoint is project-aware — call the variant that matches your task:
- Creating a new project:
pegasus projects fields --json - Updating an existing project:
pegasus projects fields --for <id> --json
The schema omits fields the project's release/state can't configure. For a new project on a modern release, expect fields like
bundler,css_framework,database, andpython_package_managerto be absent — there's only one valid choice and the server applies it. For an existing legacy project (e.g. one still onbundler=webpack), those fields reappear with both choices so you can see and change them. Trust what the schema shows. Ifbundlerisn't in the response, don't try to set it.Response shape:
{ "user_tier": "free" | "basic" | "pro" | "unlimited", "fields": { "project_name": { "type": "string", "max_length": 100 }, "use_celery": { "type": "boolean", "min_tier": "free" }, "use_subscriptions": { "type": "boolean", "min_tier": "pro" }, "front_end_framework": { "type": "choice", "min_tier": "free", "choices": [ { "value": "htmx" }, { "value": "react", "min_tier": "basic" } ] }, ... } }- Read
user_tierfirst and treat it as a constraint, not a starting point. Build a configuration that fits the user's current tier. Don't propose features above their tier unless they explicitly asked for one. The default assumption is "stay where you are"; an upgrade is something the user opts into, not something you steer them toward. min_tiercan appear at two levels — both must clear the user's tier for a value to be usable. Tier ordering isfree < basic < pro < unlimited.- Field-level (
field.min_tier): floor to configure the field at all.field.min_tier <= user_tier. - Per-choice (
field.choices[i].min_tier, choice fields only): floor to set that specific value.choice.min_tier <= user_tier. Choices withoutmin_tierare always available (assuming the field itself is).
- Field-level (
- Fields without
min_tier(project_name, project_slug, etc.) are tier-agnostic. - Choices are objects, not bare strings — the value to send is at
choice.value. The schema lists every choice regardless of tier and tags gated ones withmin_tier; filter client-side before proposing a value to the user. Don't hardcode choice lists, always read them from the schema.
- Creating a new project:
If the user explicitly asked for a tier-gated feature they can't use, surface that before attempting the call, and lead with the in-tier path. Something like: "Subscriptions requires a Pro license; you're on free. I can skip it and build the rest, or if you want to upgrade I can wait." Don't bring up upgrades unprompted when the user hasn't asked for anything gated.
Construct the payload. Two ways to provide settings, combinable:
--set key=value(repeatable) for individual fields. Booleans accepttrue/false/yes/no/y/n.null/none/empty parse to None on the client side, but most string fields reject null server-side (you'll getfield: This field may not be null.). Use null only on fields explicitly documented as nullable —pegasus_versionandlicenseare the main ones.--config-file pathto load a YAML or JSON file. If the file has adefault_context:top-level key (realpegasus-config.yamlshape), it's unwrapped automatically.--setvalues override file values.
Call create or update. The response is the full project in
pegasus-config.yamlshape (see "The config shape" below).On a 400, read the response body. Two shapes are possible:
- Field-keyed (DRF serializer errors — bad slug, invalid choice,
missing required field):
The CLI renders each as{ "project_slug": ["Sorry, your project ID must be a valid Python module name..."] }field: message. - Flat with optional
help_url(business / license errors, e.g. a license tier that doesn't support a requested feature):
When{ "error": "Subscriptions is not available on your current license...", "help_url": "https://www.saaspegasus.com/billing/" }help_urlis present, always relay it to the user — it's where they go to fix the underlying issue (upgrade their license, set up a GitHub repo, etc.). The CLI prints it as a second line prefixedMore info: <url>.
Either way, adjust and retry, or report to the user.
- Field-keyed (DRF serializer errors — bad slug, invalid choice,
missing required field):
The config shape
The API speaks the same key shape as a project's local pegasus-config.yaml,
with a few specifics:
- JSON booleans on output, but input also accepts
"y"/"n"strings — so an agent can paste yaml back without translating. - Required on create: only
project_nameandproject_slug. Everything else uses model defaults.author_name,email, andlicenseauto-populate from the user's profile if omitted. project_slugmust be a valid Python identifier, lowercase, no leading or trailing underscore, and not a reserved name (apps,templates,pegasus, stdlib module names, etc.). Server normalizes/validates.- Renamed wire keys (different from the model field name):
project_name↔ modelnameuse_auto_reload↔ modeluse_browser_reload
pegasus_versionis the pinned version (e.g."2026.5"for a major release or"2026.5.1"for a patch) ornullto track latest. Major versions do not have a trailing.0— it's"2026.5", not"2026.5.0". The value must match an actual released version — the server validates against its release list and rejects guessed strings like"2026.5.0"withUnknown Pegasus version. If the user just wants the latest, usenull; don't try to construct a version string. Output also includes_pegasus_version(read-only, the resolved version that would be used at build time).css_themeis a read-only output field derived fromcss_framework.licenseis a UUID string. Passnullfor free tier. Must belong to the requesting user.
Read-only fields (output only, ignored on input)
id_pegasus_version(resolved version)github_username(computed from the linked GitHub repo or user profile)css_theme(computed fromcss_framework)
You can safely PATCH the entire GET response back — read-only keys are silently dropped.
Defaults you should usually leave alone
The schema decides which choice fields you can touch. Don't try to be clever
about deprecated alternatives — if bundler isn't in the schema, the
question "vite or webpack?" doesn't exist for this context. Similarly,
don't ask the user "should we use tailwind or bootstrap?" when the schema
only lists tailwind.
For feature booleans the user hasn't mentioned, omit them from the
payload — the server applies the model default. Don't enumerate them when
proposing the call; it bloats the conversation. The booleans that default
on because they're recommended for typical SaaS apps: use_sentry,
use_health_checks, use_impersonation, use_async, use_celery,
use_translations, use_browser_reload, use_dark_mode, use_api_keys,
post_process (ruff). Only flip these to false if the user explicitly
asks.
For AI coding-tool rules (use_ai_rules_*), all default off. If the
user asks for "AI rules" / "agents" generically without naming a tool,
prefer use_ai_rules_claude=true (its UI label is "Claude Code
(Recommended)"). Only set use_ai_rules_agents, use_ai_rules_cursor, or
use_ai_rules_junie when the user names that specific tool.
For the front-end framework, strongly prefer HTMX. Don't surface React as an option unprompted — when proposing a project config, just go with HTMX and move on. Only switch to React if the user explicitly asks for React, a SPA, or describes a JS-heavy frontend (rich client-side state, live collaboration, etc.). HTMX is the right default for typical server-rendered SaaS apps and keeps the project simpler.
Field interdependencies
The server enforces several couplings. Knowing them keeps you from proposing conflicting settings:
bundler=viteforcesinclude_static_files=false.css_framework != tailwindforcesuse_flowbite=falseanduse_shadcn=false.use_subscriptions=falseclearssubscription_billing_modelandsubscription_pricing_ui.use_teams=falseclearsuse_teams_example.use_async=falseclearsuse_async_example.docker_mode=fullrequiresdatabase=postgres(rejected otherwise).deploy_platform=kamalrequiresdatabase=postgres.
License × feature gating
License tiers (low to high): free, basic, pro, unlimited.
The server validates feature compatibility at create/update time and again at build time. If a project has features its license can't support, the API rejects with a 400 keyed per offending feature.
You should pre-check via the schema's min_tier rather than discovering
through 400s. If the user wants something their tier can't do, default
to the in-tier path:
- Propose the specific subset of gated features to drop, and proceed with what their tier supports.
- Only if the user pushes back ("but I really need subscriptions") should you surface the upgrade option, and even then as information rather than a sales pitch.
- If the user came in asking for an upgrade, that's different — help them there.
If the user has no license at all and the free tier flag is active for them,
their tier is free. Otherwise no license means they can't build at all
(the API will create projects but pegasus projects push will refuse).
Common patterns
"Create a project for me with X, Y, Z":
- Get schema → check user_tier supports X, Y, Z.
- If anything's gated, tell the user and confirm before proceeding.
pegasus projects create --json --set project_name="..." --set project_slug=... [--set k=v ...]- Show the resulting project to confirm.
"Show me my project / what's in it":
pegasus projects show <id> --jsonand present relevant subset to user.
"Add feature X" / "switch to React" / etc:
pegasus projects show <id> --jsonto see current state.pegasus projects fields --for <id> --jsonto confirm the field is configurable for this project's release/tier.pegasus projects update <id> --json --set key=value.
"Apply these settings from this yaml file":
pegasus projects update <id> --json --config-file path/to/pegasus-config.yaml.- Combine with
--setto override specific values.
"What can I configure?" / "What features are available?":
- For a new project:
pegasus projects fields --json. - For an existing project:
pegasus projects fields --for <id> --json— the response will reflect that project's release and current values. - Parse JSON; don't rely on the table.
Pushing the project
pegasus projects push <id> is what actually generates the code — it
renders the project into a linked GitHub repo. It's a separate flow from
create/update.
Before the first push: connect a GitHub repo
A push will fail with No GitHub repository configured for this project
unless a GitHub repo has been linked to the project. A newly-created
project never has one — repo linking happens in the Pegasus web UI,
not via the CLI.
When you're about to push a project for the first time, proactively
tell the user up front that they need to visit
https://www.saaspegasus.com/projects/download/<id>/ to connect a
GitHub repo before the push will work. Don't wait for the 400 — flag it
when you confirm "ready to push?" so they don't bounce off an avoidable
error. The same page is where they'd attach a license if their tier
requires one.
The upgrade prompt (interactive trap)
The push command is interactive when a newer Pegasus version is available, which is essentially always for a freshly-created project. It prompts:
Upgrade options:
1. Upgrade to latest stable version
2. Upgrade to latest dev version
3. Don't upgrade
Select an option (1, 2, 3) [3]:
A bare pegasus projects push <id> from a Bash tool call will hang on
this prompt and abort. Always pass one of these flags:
--no-upgrade— push at the project's currently-pinned Pegasus version. This is the right default for a first push of a freshly-created project (matches option 3, the prompt's default).--upgrade— upgrade to the latest stable Pegasus version, then push.--dev— upgrade to the latest dev version, then push.
If the user just created the project and you're pushing for the first
time, default to --no-upgrade and don't bring up the upgrade options
unprompted — they picked their version at create time. For pushing an
existing project to a newer release (the "upgrade Pegasus" flow), see
the upgrade-pegasus skill.
Setting the PR title (--pr-title)
pegasus projects push accepts --pr-title "<text>" to set the title
of the resulting GitHub PR. When the push follows a config change,
always pass it — a good title makes the PR reviewable at a glance,
and the agent has all the context to write one the user won't have to.
The natural title is a short summary of what just changed in
pegasus projects update. Examples:
- After
--set front_end_framework=react→--pr-title "Switch frontend to React" - After turning on multiple features →
--pr-title "Add subscriptions and teams" - After
--set pegasus_version=2026.5.1→--pr-title "Upgrade Pegasus to 2026.5.1"
When the change set is large, lead with the headline change rather than exhaustively listing every flag — the PR diff covers the rest.
For the first push of a new project, --pr-title is optional;
something like "Initial Pegasus project setup" is fine if you want
to set one. For the upgrade-an-existing-project flow, see
upgrade-pegasus for title conventions specific to upgrades.
After a successful push
The push output includes the resulting GitHub repository URL. Once it
completes, offer to clone the repo locally (e.g.
git clone <url> <path>) so the user can start working on it. Don't
auto-clone — ask first and let them pick the target directory. If
they're not local-dev oriented (e.g. they're pushing from CI or just
wanted the rendered code in GitHub), they'll say no, and that's fine.
Gotchas
- Always parse JSON, never the table. Repeating because it bites:
pegasus projects fieldswithout--jsonis a Rich table that can be truncated by terminal height. If you think the schema is "missing" a field, you're almost certainly reading truncated output — re-run with--json. - Slug uniqueness is per-user. Two users can both have a project with
slug
my_app. You can't have two on one account. - PATCH is partial. Unspecified fields stay as-is. To "reset" a field,
you must explicitly set it (e.g.,
--set ai_chat_mode=none). - License downgrade can break a project. If the user PATCHes to a lower license tier while pro features are on, the API will 400. Either remove the pro features in the same PATCH or do it as two steps.
- Don't try to discover available choices. The schema endpoint lists every choice for every choice-typed field. Use it.
- Pegasus build is a separate step. Creating/updating a project doesn't
generate any code — that happens via
pegasus projects push(creates a GitHub PR with the rendered project). Build-time validation is stricter than create-time validation; if it passes the API it might still fail at build with a license/feature/release combo issue.
Output for the user
Pegasus CLI commands print Rich tables by default. When you're acting as an agent for a human:
- Show meaningful state changes (the project's new name, the feature you changed, etc.), not the entire 60-field config dump.
- Surface license/tier issues clearly — these are conversion moments where the user might want to upgrade.
- For multi-step flows (check tier → propose payload → confirm → execute), pause at each step rather than running blind.