Building an agent
Scope, author, test a draft, publish, verify what landed, and schedule, plus how to write a system prompt that survives revision.
1. Scope it
Decide these before you write the manifest:
- The job it owns and the output expected.
- The input it accepts. Input is free-form JSON or text; the
systemprompt is the input contract. - The model and tools it needs.
- How it is triggered: on demand, from an app, on a schedule, or by Slack mention.
- Whether it needs a companion app.
- Its visibility:
orgorpersonal. - What real systems, data, spend, or side effects a test may touch.
Check what already exists before you design around an integration. Do not assume a new one is needed:
railcode db list # data connections
railcode query list # admin-published saved queries
railcode connector list # connectors you can reach
railcode connector catalog # providers that could be linkedInspect any plausible match before you write the manifest (railcode connector docs <name>
for http rows, railcode connector tools <name> for mcp rows). Copy exact names, endpoints,
and tool slugs rather than guessing. Prefer saved queries over adhoc_sql.
If nothing suitable exists, you have three options:
- Have an admin connect a database and publish a saved query.
- Create an org-managed connector for a shared credential.
- Link a bundled provider as a connector.
Note that a user-added custom MCP connector works for apps but not for agent manifests.
2. Author the manifest
railcode login # if the CLI has no usable saved token
railcode agent pull <agent> --output agent.json # for an existing agent--file on create / update / test reads JSON or YAML. The CLI picks the parser by
extension (.yaml / .yml → YAML, anything else → JSON). pull and show --manifest emit
JSON only. Convert it yourself if you want YAML on disk. It round-trips back through --file
unchanged.
See the manifest reference for the tools vocabulary and limits.
3. Test the draft
railcode agent test --file agent.yaml --input '{"key":"value"}' --traceUse --input-file for larger or sensitive payloads.
Testing invokes real configured models and tools. It may incur spend, read real data, or cause tool side effects. Get authorization before you run a test with side effects.
An agent that reads a companion app proves nothing against an empty app. Seed it first:
railcode app kv set <collection> <key> '<json>' --app <app>
railcode app files upload <path> --app <app>Do not rely on the exit code. A request that reached the runtime can exit 0 even when
the run's printed status is failed. Check Status: or inspect --json.
Remember that a draft test does not persist writes; see draft caveats.
4. Publish
railcode agent create --file agent.yaml
railcode agent create --file agent.yaml --visibility personal
railcode agent update <agent> --file agent.yamlupdate replaces the stored manifest, so start from railcode agent pull to preserve fields
on purpose. Read all ratification warnings before you consider the change done.
On create / test, omitting --visibility defaults to org. On update, omitting it
leaves the existing visibility alone. Never pass it just to be explicit, because an omitted
flag and an explicit org are different requests on the server side.
5. Verify what landed
railcode agent run <agent> --input '{"key":"value"}' --trace
railcode agent show <agent> --manifestA clean run status is not proof that the data landed. When the agent writes through
app_data_write, check the app's stores directly:
railcode app kv collections --app <app> # collections + record counts
railcode app kv list <collection> --app <app> # what app_kv_set actually wrote
railcode app files list --app <app> # what publish_artifact_to_app produced
railcode app files download <name> --app <app> # open the generated .docx/.pdf yourselfAdd --scope user --user <uuid> or --scope role --role <uuid> to inspect a non-shared
namespace. --scope all lists across every scope with owner attribution. This is also the
fastest way to catch a personal agent writing into its owner's private scope when the team
expected shared records.
6. Schedule it
Each agent has at most one schedule. The schedule is a resource under the agent, not part of its manifest.
railcode agent schedule show <agent>
railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTC
railcode agent schedule pause <agent>
railcode agent schedule resume <agent>
railcode agent schedule run-now <agent> [--trace]
railcode agent schedule delete <agent> --yesset upserts. add is create-only and errors if a schedule exists. update is update-only
and errors when none exists. --cron takes a five-field expression, --timezone an IANA
zone. run-now executes synchronously against real services.
A scheduled run passes null input. There is no per-schedule payload, so write the
system prompt such that a run with no input knows exactly what to do.
Writing the system prompt
The system prompt is the agent's whole contract: what job it owns, how to read its input,
and what to return. Two habits keep it working as it evolves.
Prefer positive instruction. Say what the agent should do rather than what it should not. "Quote figures only from the uploaded materials, and write a bracketed placeholder where one is missing" gives the model something to aim at. "Don't invent figures" forbids one path and leaves the rest to guesswork. A hard boundary ("never email anyone outside the attendee list") should be stated outright. But when a "don't" is standing in for a "do", write the "do".
Audit the whole prompt on every update. Each run reads the prompt cold. The agent has no memory of previous versions, earlier runs, or the bug you were chasing last week. So prompts collect leftovers that read fine to us and mislead the agent:
- Corrections phrased as history: "We no longer do X, do Y instead." This agent never did X. The sentence introduces X and asks it to carry both. State only Y.
- Debug leftovers: a temporary "for now, only process the first three rows", or a workaround for a bug that has since been fixed.
- Orphaned steps: instructions that name a tool, app, connector, or field the manifest no longer declares.
- The same rule three times in slightly different words, each added during a different test round. Restatements compete. Keep the clearest one.
Before you update, read the stored system end to end from railcode agent pull, not from
memory of what you last wrote. Rewrite it as the procedure someone encountering it cold would
follow. A system prompt should read as a specification, never as a changelog.
Permissions
list,show, andpullonly ever return org agents plus the caller's own personal agents. Someone else's personal agent is a 404, never a 403, so its existence is not confirmed, admins included.- For an org agent:
runneeds an invoke grant.update/delete/testand schedule mutations are allowed for the agent's creator or any org owner/admin. - For a personal agent: invoke and manage are both owner-only, with no admin override. There is no emergency override.
deletearchives the agent while keeping run history, and requires--yesoutside a TTY.
Agent manifest
The tools vocabulary, what each key grants and its permission gate, read scopes, limits, and how a draft test differs from a saved run.
Companion apps
Agents can't own storage, so they work through an app. The pairing pattern, and four things to know about driving a run from a backend function.