A playbook for turning "make a routine that does X" into a valid, saved Crewship
routine — grounded in what this crew actually has, tested before it ships.
-
Clarify only the genuinely ambiguous essentials. Ask at most 2–3 questions,
then default the rest. The three that usually matter:
- Trigger cadence — manual, on a schedule (when?), webhook, or event.
- Where the output goes — a Slack channel, an issue, a file, a return value.
- What to do on failure — retry, alert someone, or just stop.
Do not interrogate. If the goal is clear, proceed with sensible defaults
(manual trigger, return the result, stop on failure).
-
Ground in what this crew actually has. Read the [CONNECTED INTEGRATIONS]
block in your prompt. Use ONLY integrations listed there, and declare each one
the routine needs in integrations_required (lowercase connector slugs like
"github", "slack"). Read [AVAILABLE ROUTINES] and reuse/compose
existing routines with a call_pipeline step where one already fits — don't
re-build what's there. Also read the [CONTAINER RESOURCES] block: it lists
the datastores (e.g. Postgres at a host/port) and installed CLIs your crew's
container already has. If the routine uses any of them — typically from an
agent_run step that opens a DB connection or shells out to a CLI — declare
them in the top-level resources block (resources.datastores[] /
resources.tools[]). These can't be inferred from the step graph, so without
the declaration the manifest is incomplete and the run-time resource
precondition gate has nothing to check against.
-
Prefer linear steps. A short, top-to-bottom sequence is easier to read,
test, and approve. Avoid branching (if:), DAG needs:, and loops unless the
goal genuinely requires them. Keep it to the fewest steps that do the job.
-
Write valid DSL. The definition is a JSON object:
- Top level:
dsl_version (always "1.0"), name, description,
inputs[], outputs[], integrations_required[], egress_targets[],
credentials_required[], resources, steps[].
resources (only when the routine touches container datastores/CLIs):
{ "datastores": [{ "type":"postgres", "name":"app-db", "note":"writes table runs" }], "tools": [{ "type":"ansible", "name":"deploy.yml" }] }.
type is the engine/tool family (postgres|redis|mysql|mongodb|other for
datastores; ansible|terraform|kubectl|bash|python|other for tools).
- Step types (the
type field selects the shape):
agent_run — { "id", "type":"agent_run", "agent_slug", "prompt", "complexity":"fast|moderate|smart" }
http — { "id", "type":"http", "http": { "method", "url", "headers", "body", "credential_ref": {"type":"slack"} } }
transform — { "id", "type":"transform", "transform": { "input":"{{ steps.x.output }}", "expression":".field" } } (pure-Go jq subset, no LLM)
wait — { "id", "type":"wait", "wait": { "kind":"approval", "approval_prompt":"…" } } (also datetime / event)
call_pipeline — { "id", "type":"call_pipeline", "pipeline_slug":"other-routine", "inputs": {…} }
- Templating: reference inputs as
{{ inputs.name }} and a prior step's
result as {{ steps.<step-id>.output }}. Steps run in order by default.
Minimal example:
{
"dsl_version": "1.0",
"name": "daily-standup-digest",
"description": "Summarize yesterday's commits and post to Slack.",
"inputs": [{ "name": "repo", "type": "string", "required": true }],
"integrations_required": ["github", "slack"],
"steps": [
{ "id": "summarize", "type": "agent_run", "agent_slug": "alex",
"complexity": "fast",
"prompt": "Summarize commits in {{ inputs.repo }} since yesterday." },
{ "id": "post", "type": "http",
"http": { "method": "POST", "url": "https://slack.com/api/chat.postMessage",
"credential_ref": { "type": "slack" },
"body": "{{ steps.summarize.output }}" } }
]
}
-
Save and test. Call the save_routine tool with
{ name, description, definition, sample_inputs } — do NOT curl the save
endpoint. The tool validates (a fast dry-run) before saving. If it returns
an error, READ it, fix the DSL (bad template path, missing input, wrong
step shape), and retry — do not hand the user a routine that never passed
validation. Use list_routines to check existing routines before authoring
a duplicate.
-
Tell the user the real outcome. A routine is risky and lands as
proposed (a MANAGER must approve it before it can run) when it contains an
http step, a code step, declares egress_targets or credentials_required,
or names an integration the crew hasn't connected. A routine built from only
agent_run / transform / wait / call_pipeline with all integrations
already connected goes live immediately. Say which one happened — never
claim a proposed routine is live.
-
Present a short readable summary. Describe the trigger and each step in
plain language ("On a manual run: 1) Alex summarizes the repo's commits,
2) the summary is posted to Slack"). Never dump raw JSON at the user.
-
Run a saved routine when asked. To invoke an existing routine, call the
run_routine tool with { slug, inputs } — do NOT shell out to curl or
re-improvise the work by hand. The run executes synchronously and returns the
run result/status; report the real outcome to the user. Use list_routines
to find the slug first if you don't have it.