Onboarding New Developers: Turning Undeclared Setup Into One Command

A new engineer's first week is mostly spent reconstructing knowledge that already exists. Which Python version does this service want. Where do the environment variables come from. Why does the test suite fail until you run a migration nobody mentioned. What is the difference between the three services that all seem to do the same thing.
None of this is hard. All of it is undocumented, or documented somewhere that rotted six months ago, and the only reliable retrieval mechanism is interrupting someone who knows.
The framing that helps is this: onboarding cost is a measure of how much of your setup is undeclared. Every step that lives in a person's head rather than in a file is a step that has to be transferred by conversation. Reduce the undeclared surface and the first week shrinks as a side effect.
Here is a practical setup for doing that with StructKit. It is a pattern to adapt, not a case study — the specifics of your stack will differ, but the shape holds.
Make the setup executable
The single highest-leverage change is turning "here are the setup steps" into "here is the command."
Pre-hooks run in order before any file is written and abort generation on a non-zero exit, which makes them the right place for prerequisite checks. Post-hooks run after generation and report errors without halting:
pre_hooks:
- python3 --version || (echo "Python 3.11+ required" && exit 1)
- docker --version || (echo "Docker required — see the platform onboarding doc" && exit 1)
- git --version
post_hooks:
- python3 -m venv .venv
- .venv/bin/pip install --upgrade pip
- .venv/bin/pip install -r requirements-dev.txt
- cp .env.example .env
- docker compose up -d postgres
- .venv/bin/alembic upgrade head
- echo "Setup complete. Run 'make dev' to start the service."
files:
- .env.example:
file: github://acme/platform-templates/main/env/python-service.example
- requirements-dev.txt:
file: github://acme/platform-templates/main/python/requirements-dev.txt
- docker-compose.yml:
content: |
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: localdev
ports: ["5432:5432"]Two details matter more than the list of commands.
The pre-hooks fail with a message that says what to install. A new engineer hitting Python 3.11+ required on line one has a five-second problem. The same person hitting an obscure traceback forty seconds into a build has a Slack-interruption problem.
And .env.example is a remote reference. This is the fix for the file that always rots: when someone adds a required environment variable, they update the canonical copy, and every service regenerated afterward has it. The classic onboarding failure — an example env file missing three keys added last quarter — stops being possible in the same way.
Give every service the same front door
A new engineer joining a team with twelve services should not have to learn twelve conventions. The way in is a small set of files that mean the same thing everywhere, generated from one definition:
files:
- Makefile:
file: github://acme/platform-templates/main/python/Makefile
- CONTRIBUTING.md:
file: github://acme/platform-templates/main/CONTRIBUTING.md
- .github/CODEOWNERS:
content: |
* @acme/{{@ owning_team @}}
- README.md:
skip_if_exists: true
content: |
# {{@ service_name @}}
{{@ service_description @}}
**Owner:** @acme/{{@ owning_team @}}
**Slack:** {{@ mappings.teams[owning_team].slack @}}
**Runbook:** https://runbooks.acme.com/{{@ service_name | slugify @}}
## Local development
Run `make setup` once, then `make dev` to start the service
and `make test` to run the tests.
variables:
- service_name:
description: "Human-readable service name"
type: string
- service_description:
description: "One sentence: what this service does"
type: string
- owning_team:
description: "GitHub team that owns this service"
type: stringmake setup, make dev, make test mean the same thing in every repository. The Makefile is a remote reference, so improving it improves every service.
README.md carries skip_if_exists: true — the template seeds a good structure once and never overwrites what the team wrote afterward. That distinction, drawn file by file, is what makes a definition safe to re-run.
The mappings.teams[owning_team].slack reference pulls from a mappings file, so the team contact information in every README comes from one place:
mappings:
teams:
payments:
slack: "#payments"
email: payments@acme.com
platform:
slack: "#platform-team"
email: platform@acme.comstructkit generate --mappings-file ./mappings/teams.yaml .struct.yaml .When a team renames its channel, one file changes and the next regeneration carries it everywhere.
Let a new engineer create a correct service on day one
The more meaningful test of onboarding is not whether someone can run an existing service. It is whether they can create a new one that looks like it belongs.
Traditionally that means finding the most recent service, copying it, and hoping it was not itself a copy of something stale. With the definitions pinned in a source, it is one command:
structkit sources add platform github://acme/platform-structures@v1.4.0/structures
structkit list --source platform
structkit generate platform/service/python ./inventory-apiStructKit prompts for whatever the definition declares without a default — service name, owning team — and generates a service that has the current CI workflow, the current security policy, and the current Makefile. Not the versions that were current when someone last updated the golden repo.
Better still, commit the pin inside each repository's .struct.yaml so nothing depends on local configuration:
sources:
platform:
url: github://acme/platform-structures@v1.4.0/structures
folders:
- ./:
struct: platform/service/python
with:
service_name: inventory-api
owning_team: paymentsNow structkit generate in that repository produces the same result for anyone — a new hire, a CI runner, an AI assistant — from an explicitly versioned definition. Prefer tags or SHAs over branches; a definition pinned to @main means something different every week, which is precisely the ambiguity you are trying to remove.
Let them see what will happen before it happens
New engineers are cautious about running commands that modify repositories, and they should be. The dry run removes the risk:
structkit generate -d --diff .struct.yaml .-d writes nothing; --diff shows exactly what would change. This is also the best available answer to "is this repository set up correctly?" — an empty diff means it matches the standard. A non-empty one shows what is missing, already written.
For a first pass against a repository someone did not create, skip is the right conflict strategy:
structkit generate -f skip .struct.yaml .It adds what is absent and touches nothing that exists. The -f / --file-strategy options are overwrite (the default), skip, append, rename, and backup.
Point their assistant at the same templates
Most new engineers now have a coding assistant open. Left to itself, an assistant asked to add a service will invent a plausible layout that is not yours — which quietly reintroduces exactly the inconsistency you removed.
The MCP server fixes this by giving the assistant your templates as tools:
{
"mcpServers": {
"structkit": {
"command": "structkit",
"args": ["mcp", "--server"],
"env": {
"STRUCTKIT_STRUCTURES_PATH": "/path/to/acme/platform-structures/structures"
}
}
}
}Include this in the onboarding setup and a new engineer's assistant can list the approved structures, inspect their variables, preview the output, and generate — from the same definitions everyone else uses. The assistant becomes a way to navigate your conventions rather than a source of new ones.
The companion agent skill packages the discover-inspect-preview-generate procedure so the assistant follows it without being told each time:
npx skills add httpdss/structkit-skillsOnboarding the definitions themselves
One thing worth doing early: give new engineers a way to see the shape of the system rather than reading YAML files one at a time.
structkit info platform/service/pythonshows what a structure contains and what it needs. Over MCP, graph_structure with output: "mermaid" renders the dependency tree between definitions — which composes what, and where the shared pieces live. For someone trying to build a mental model of how twelve services relate, a diagram of the composition graph is worth an afternoon of reading.
What this does not fix
Worth being honest about the boundary. Generation removes the mechanical part of onboarding — environment setup, project creation, knowing where files go. It does nothing for the part that actually takes the longest: understanding the domain, knowing why the billing service retries the way it does, learning which parts of the codebase are load-bearing.
That knowledge transfers through code review, pairing, and reading incident write-ups. What this setup buys is that the first week is spent on that rather than on reconstructing a virtualenv.
It also does not survive neglect. A definition referencing a Makefile nobody maintains is as stale as a wiki page — the mechanism only helps if the canonical templates are actually canonical.
Wrap-up
Treat undeclared setup as a bug. Every step that exists only as knowledge is a step someone has to be told, and telling does not scale. Making prerequisites executable, referencing shared files at their source instead of copying them, pinning the definitions to a version, and giving assistants the same templates turns most of a first week into a single command and a short diff.
See Hooks for pre- and post-generation commands, Custom Structures for sources and pinning, and MCP Integration for the assistant setup.