Use profiles and .devctl.override.yaml to select plugins and keep local adjustments out of shared config.
Profiles let a repository define named ways to run devctl. A profile selects which plugins participate in the pipeline and can add environment overrides for those plugins. This is useful when a repository has several valid development modes: frontend-only, backend-only, full-stack, production-like, or a local debugging setup.
A profile is not a separate pipeline. It is a selection step before plugins are started. Once devctl has selected the active profile, the rest of the system behaves normally: selected plugins run config.mutate, build.run, prepare.run, validate.run, and launch.plan, and devctl supervises the services returned by the launch plan.
.devctl.yamlProfiles live in the shared project config under profiles:. Each profile names plugin IDs from the top-level plugins: list.
profile:
active: development
profiles:
development:
display_name: Development
description: Hot reload, verbose logs, local services
plugins:
- api
- web
env:
LOG_LEVEL: debug
backend:
display_name: Backend Only
description: API plus database, no frontend
plugins:
- api
- database
plugins:
- id: api
path: python3
args: [./plugins/api.py]
- id: web
path: python3
args: [./plugins/web.py]
- id: database
path: python3
args: [./plugins/database.py]
The plugins: field inside a profile is a list of plugin IDs. It does not control ordering. Ordering still comes from plugin priority, with ID as the stable tie-breaker.
Use --profile when you want an explicit one-off choice:
devctl up --profile backend
devctl plan --profile development
devctl plugins list --profile backend
Use profile.active when the repository should have a default profile:
profile:
active: development
Selection precedence is:
1. --profile <name>
2. profile.active from .devctl.override.yaml
3. profile.active from .devctl.yaml
4. no profile: load all top-level plugins
The final case is the backward-compatible mode. Existing repositories with only a top-level plugins: list continue to load all plugins.
default profileA profile named default is allowed, but it is not magic. It is active only when selected explicitly:
profile:
active: default
profiles:
default:
plugins: [api, web]
or:
devctl up --profile default
If .devctl.yaml defines profiles.default but neither profile.active: default nor --profile default is provided, devctl loads all top-level plugins. The mere presence of a profile named default does not change old behavior.
.devctl.override.yamlA local .devctl.override.yaml can add or adjust profiles without changing the shared project config. devctl loads .devctl.yaml first, then merges .devctl.override.yaml if it exists.
# .devctl.override.yaml
profile:
active: local-debug
profiles:
local-debug:
display_name: Local Debug
description: Backend plus local trace settings
plugins:
- api
- database
env:
LOG_LEVEL: trace
DEVCTL_TRACE: "1"
The expected workflow is that .devctl.override.yaml is local to one developer. Add it to .gitignore unless your team intentionally wants to commit a shared override template.
The override file is deliberately simple. It is the same schema as .devctl.yaml, but every field is optional.
| Field shape | Merge rule |
|---|---|
Scalars such as profile.active and strictness | Override value wins when non-empty. |
Maps such as profiles and env | Keys merge; override keys win. |
| Profile records | Merge field by field. |
Profile plugins list | Replaced when the override provides a non-empty list. |
Top-level plugins list | Merged by plugin id; existing IDs are patched, new IDs are appended. |
Plugin args list | Replaced when the override provides a non-empty list. |
Plugin env map | Merged; override keys win. |
This means a local override can adjust an existing profile's env without restating its plugin list:
profiles:
development:
env:
LOG_LEVEL: trace
It can also add a local plugin and a profile that selects it:
profiles:
tracing:
plugins: [api, jaeger-local]
plugins:
- id: jaeger-local
path: ./plugins/devctl-jaeger-local
Profile env is applied to every selected plugin and overlays that plugin's configured env for that run.
profiles:
backend:
plugins: [api]
env:
LOG_LEVEL: debug
plugins:
- id: api
path: python3
args: [./plugins/api.py]
env:
LOG_LEVEL: info
API_ONLY: "1"
When backend is active, the api plugin receives LOG_LEVEL=debug and API_ONLY=1.
List the profiles devctl sees after applying the local override:
devctl profiles list
Show the resolved active profile:
devctl profiles active
If no profile is selected, profiles active prints (none). That means devctl will load all top-level plugins.
Dynamic commands are discovered from the plugins selected by the active profile. If a plugin is excluded by --profile backend, dynamic commands from that plugin are not registered for that invocation.
This is intentional. A profile describes the active operating mode, and dynamic commands should come from the plugins participating in that mode.
| Problem | Cause | Solution |
|---|---|---|
profile "x" not found | The selected profile is not present after merging .devctl.yaml and .devctl.override.yaml. | Run devctl profiles list and check spelling. |
profile "x" references unknown plugin "y" | The profile names a plugin ID that is not defined or discovered. | Add the plugin to top-level plugins: or fix the ID. |
profiles active prints (none) | No --profile and no profile.active are set. | This is valid backward-compatible all-plugins mode. Set profile.active if you want a default. |
profiles.default is ignored | default is not implicit. | Use profile.active: default or --profile default. |
| Local changes affect teammates | .devctl.override.yaml was committed accidentally. | Add .devctl.override.yaml to .gitignore unless the team wants it committed. |
devctl help user-guidedevctl help scripting-guidedevctl help plugin-authoringdevctl profiles listdevctl profiles active