# pfp
A fast CLI for managing [Prefect 3](https://docs.prefect.io/) deployments and flow runs, built for both human and AI agent use.
## What it does
pfp talks directly to the Prefect REST API, bypassing the official CLI's limitations: truncated output, unreliable exit codes, and awkward parameter syntax.
- **Substring matching** — `pfp run happy-t` finds `happy_terraform/happy-terraform-prod`
- **Correct exit codes** — 0 for success, 1 for flow failure, 2 for CLI errors
- **`--watch` that works** — polls until completion with state change reporting
- **Dotted path parameters** — `--set config.action=destroy` builds nested JSON
- **Parameter validation** — typos caught before submission with "did you mean?" suggestions
- **`--json` on everything** — structured output for programmatic consumption
- **Full deployment names** — no truncation, ever
## Installation
From [crates.io](https://crates.io/crates/pfp):
```bash
cargo install pfp
```
From source:
```bash
git clone https://github.com/devinbarry/pfp.git
cd pfp
cargo install --path .
```
## Configuration
pfp reads your existing Prefect configuration. No extra config files needed.
**API URL** is resolved from `~/.prefect/profiles.toml`:
```toml
active = "self-hosted"
[profiles.self-hosted]
PREFECT_API_URL = "https://prefect.example.com/api"
```
The `PREFECT_API_URL` environment variable takes priority if set.
**Authentication** is optional. If your server requires it, set `PREFECT_API_AUTH_STRING` with a `username:password` value — pfp encodes it as HTTP Basic Auth:
```bash
export PREFECT_API_AUTH_STRING="admin:secret"
```
## Usage
### pfp ls
List all deployments:
```
$ pfp ls
DEPLOYMENT STATUS WORK POOL
happy_ansible/happy-ansible-prod ACTIVE docker-prod
happy_terraform/happy-terraform-prod ACTIVE docker-prod
hello_world/hello_world-dev ACTIVE docker-dev
update_hosts/update_hosts-prod ACTIVE docker-prod
```
```bash
pfp ls --json # JSON array of deployment objects
```
### pfp run
Run a deployment by substring match:
```bash
pfp run happy-t # create flow run and exit
pfp run happy-t --watch # poll until completion
pfp run happy-t --set config.action=plan # override parameters
```
Combining `--watch` with parameters:
```
$ pfp run happy-t --watch --set config.action=apply --set config.auto_approve=true
Resolved: happy_terraform/happy-terraform-prod
Created flow run 'venomous-alligator' (171a3f55-...)
17:35:27 | Completed
```
With `--watch`, the exit code reflects the flow run outcome: 0 for Completed, 1 for Failed/Cancelled/Crashed.
```bash
pfp run happy-t --json # JSON output of created flow run
pfp run happy-t --watch --json # JSON object per state change
```
### pfp runs
Show recent flow runs for a deployment:
```
$ pfp runs happy-t
FLOW RUN STATE STARTED DURATION ID
production-apply COMPLETED 2026-02-21 17:34 45s e130c152
production-destroy COMPLETED 2026-02-21 17:34 8s 171a3f55
production-plan COMPLETED 2026-02-21 00:05 3s 7137cfe7
```
```bash
pfp runs happy-t --json # JSON array of flow run objects
```
### pfp inspect
Fetch one flow run by its full UUID. Unlike `pfp runs`, this performs an exact
lookup and is not limited to recent runs, so it is safe for automation even
when many runs are created concurrently.
```bash
pfp inspect e130c152-db01-428a-9698-e8404cd2c5d3
pfp inspect e130c152-db01-428a-9698-e8404cd2c5d3 --json
```
### pfp logs
Show logs for a flow run (requires full UUID):
```
$ pfp logs e130c152-db01-428a-9698-e8404cd2c5d3
2026-02-21T17:34:41 | INFO | Action: apply
2026-02-21T17:35:27 | INFO | Flow run completed successfully
```
Get the flow run UUID from `pfp runs <query> --json`.
```bash
pfp logs e130c152-db01-428a-9698-e8404cd2c5d3 --json # JSON array of log entries
```
### pfp pause / pfp resume
```bash
pfp pause happy-t # pause the deployment
pfp resume happy-t # resume it
```
`pause` and `resume` change the deployment-wide paused state. To activate every
schedule attached to one deployment without changing that state, use:
```bash
pfp schedule-resume happy-t
```
The command fails if the deployment has no schedules. If activation fails part
way through, pfp attempts to restore every schedule that was initially inactive
and reports whether that rollback was complete.
### pfp cancel
```bash
pfp cancel e130c152-db01-428a-9698-e8404cd2c5d3 # cancel a running flow run
```
## Substring matching
All commands that take a deployment name use unique substring matching against the full `flow_name/deployment_name` identifier:
| 0 | Error: `No deployment matching 'query'` |
| 1 | Uses the match |
| 2+ | Error: `Ambiguous match 'query', candidates:` with list |
Use `pfp ls` to discover available deployment names and find a unique substring.
## Parameters
The `--set` flag builds nested JSON from dotted paths:
```bash
--set config.action=destroy --set config.auto_approve=true
```
Produces:
```json
{"config": {"action": "destroy", "auto_approve": true}}
```
Values are auto-typed:
| `true` / `false` | boolean |
| `42` | integer |
| `3.14` | float |
| `["a","b"]` | JSON array |
| `{"k":"v"}` | JSON object |
| anything else | string |
Parameters from `--set` are merged with the deployment's defaults. Explicit values override defaults.
### Validation
Before submitting a flow run, pfp validates `--set` parameters against the deployment's OpenAPI schema. Typos are caught immediately:
```
$ pfp run happy-t --set config.dry_urn=true
Error: unknown parameter 'config.dry_urn'
Valid parameters for config:
action, ansible_debug, ansible_limit, ansible_tags,
deployment_name, dry_run, git_ref, inventory_name,
playbook_name, vault_secrets
Did you mean 'config.dry_run'?
```
Validation is automatic — no flags needed. If a deployment has no schema (older Prefect versions), validation is skipped and parameters are passed through as before.
### Params file
For large or deeply-nested parameters — an array of objects, say — that are awkward to express with repeated `--set` flags, pass the whole parameters object as JSON with `--params-file`:
```bash
pfp run happy-t --params-file payload.json # read from a file
The payload must be a JSON object matching the deployment's parameters shape:
```json
{
"environment": "production",
"config": {
"action": "apply",
"vault_secrets": [
{"path": "kv/prod/app/db", "field": "PASSWORD", "env_var": "DB_PASSWORD"},
{"path": "kv/prod/app/r2", "field": "ACCESS_KEY", "env_var": "AWS_ACCESS_KEY_ID"}
]
}
}
```
Precedence is deployment defaults < `--params-file` < `--set`, so a single `--set config.action=plan` can still override one field of a large payload.
The payload is validated against the deployment schema — the same client-side validation as `--set` — before the run is created. An unreadable file, malformed JSON, or a non-object top-level fails fast with exit code 2 before any API call.
## Exit codes
| 0 | Success — command completed, flow run finished (if `--watch`) |
| 1 | Flow failure — flow run ended in Failed, Cancelled, or Crashed (only with `--watch`) |
| 2 | CLI error — bad arguments, no match, ambiguous match, API unreachable |
## License
MIT