Write prompts
Read run values in a prompt, and use conditions, loops and filters
Write the text an agent step receives, and let Orbital fill in the run’s values before each visit. One prompt can then say something different on a retry, after a review or in a different project.
review [prompt="Review the change against this goal: {{ inputs.goal }}"]The template language is Nunjucks. Every example below shows the prompt and the text it renders to.
Inline prompts and prompt files
Section titled “Inline prompts and prompt files”A short prompt fits in the node’s prompt attribute, such as prompt="Review the goal for risks.". The attribute sits inside double quotes, so write strings inside the template with single quotes, as every example here does.
A longer prompt belongs in its own file. prompt_file="prompts/review.md" names a file relative to the workflow file, and a prompts folder beside the workflow is the usual place. A node takes one or the other, never both. Orbital reads a prompt file again before each later visit, so an edit reaches a running workflow. If the edited file is invalid, Orbital reports the problem and keeps the prompt it last checked.
Read a value
Section titled “Read a value”{{ … }} prints a value. The goal is inputs.goal, each declared input is inputs.<name>, and anything the run has gathered is under context. Template variables lists every name.
Work towards this goal: {{ inputs.goal }}Extra notes: {{ inputs.notes }}This run was started without the optional notes input, so that line ends empty:
Work towards this goal: Add a dark mode toggle to the settings pageExtra notes:A value the run does not hold renders as nothing, never as an error or the word undefined. Validation catches most mistakes first. A prompt that reads a context value some earlier path never writes fails orbital validate, and so does a name that is not a template variable.
Conditionals
Section titled “Conditionals”{% if %} includes text only when its test holds. {% elif %} tries another test and {% else %} covers the rest. Compare with == and !=, and join tests with and, or and not. An empty value counts as false.
{% if context.verdict == 'reject' -%}Fix every finding the reviewer listed.{% elif context.verdict == 'pass' -%}Tidy the change and write the summary.{% else -%}Review the change from the start.{% endif -%}With verdict set to reject:
Fix every finding the reviewer listed.The dashes in -%} stop each tag from leaving a blank line behind. Whitespace control explains them.
{% for %} repeats its text for each item. Inside it, loop.index counts from 1, loop.first and loop.last mark the ends, and loop.length is the number of items. {% else %} inside a loop renders when there is nothing to loop over.
Check each folder:{% for name, folder in run.folders -%}{{ loop.index }}. {{ name }} at {{ folder.path }}{% else -%}This project has no folders.{% endfor -%}In a project with the folders web and api:
Check each folder:1. web at /work/shop/web2. api at /work/shop/apiContext values are single pieces of text, numbers or true and false, so a loop has two useful sources: the folders under run.folders or project.folders, and a list written in the prompt itself, such as {% for area in ['tests', 'types'] %}. A json output is stored as text, and a loop over it walks its characters.
Filters
Section titled “Filters”A filter changes a value on its way into the prompt. Write it after a |, and chain as many as you need.
| Filter | What it does |
|---|---|
default('x') |
Uses x when the value is missing. default('x', true) also replaces empty text. |
trim |
Removes spaces and line breaks from both ends. |
join(', ') |
Joins a list into one piece of text. |
length |
Counts the items in a list, the folders in run.folders, or the characters in text. |
upper and lower |
Change the case. |
replace('a', 'b') |
Replaces every a with b. |
Branch: {{ inputs.title | trim | lower | replace(' ', '-') }}Priority: {{ inputs.priority | default('normal') | upper }}Last failure: {{ context.failure_reason | default('none', true) }}Folders: {{ run.folders | length }}Checks: {{ ['lint', 'tests', 'types'] | join(', ') }}With title set to Fix Login Redirect, no priority input, no failure yet and one folder:
Branch: fix-login-redirectPriority: NORMALLast failure: noneFolders: 1Checks: lint, tests, typesfailure_reason is empty text after a success rather than missing, which is why that line needs default('none', true). Nunjucks has more filters; its list of built-in filters covers them all.
Whitespace control
Section titled “Whitespace control”Every line break in the prompt stays in the output, including the ones around {% %} tags. A condition that is false leaves an empty line where it stood:
Review the change.{% if context.failure_reason %}The last attempt failed: {{ context.failure_reason }}{% endif %}Report what you found.Review the change.
Report what you found.A dash inside a tag removes the spaces and line breaks on that side. {%- trims before the tag and -%} trims after it. {{-, -}}, {#- and -#} work the same way.
Review the change.{%- if context.failure_reason %}The last attempt failed: {{ context.failure_reason }}{%- endif %}Report what you found.Review the change.Report what you found.When the step fails and comes back, the same prompt adds the failure on its own line between the other two.
Comments
Section titled “Comments”{# … #} holds a note for whoever edits the prompt. The agent never sees it.
{# The reviewer reads this on a phone, so keep the prompt short. -#}Review the change in three sentences or fewer.Review the change in three sentences or fewer.Write a literal {{
Section titled “Write a literal {{”Text between {% raw %} and {% endraw %} renders exactly as written. Use it when the agent must see template syntax of its own, such as a placeholder in a file it edits.
Add a greeting to the email template. It must say:{% raw %}Hello {{ customer.name }}, your order has shipped.{% endraw %}Add a greeting to the email template. It must say:Hello {{ customer.name }}, your order has shipped.What a prompt cannot do
Section titled “What a prompt cannot do”A prompt cannot read files, so include, import and extends are unavailable, and it cannot run shell commands. Orbital checks everything a prompt reads before the run starts. A prompt that pulled in another file or a command’s output would read things that check never saw.
To give a prompt a file’s contents or a command’s result, add a command node before it. Its facts land in context, where the prompt reads them. To share text between prompts, put it in one prompt file and point several nodes at it.
Patterns
Section titled “Patterns”Show the failure only on a retry
Section titled “Show the failure only on a retry”Orbital writes context.failure_reason after every step. It is empty on success and holds the reason on failure. A step reached again through a failure edge can explain what went wrong, while its first visit reads as normal.
Implement the goal: {{ inputs.goal }}{%- if context.failure_reason %}
Your last attempt failed: {{ context.failure_reason }}Fix that first.{%- endif %}After the tests failed:
Implement the goal: Fix the login redirect
Your last attempt failed: npm test exited with code 1: 2 of 48 tests failedFix that first.On the first visit, the prompt ends after the goal.
Use the goal and a declared input
Section titled “Use the goal and a declared input”With inputs="ticket_url" on the workflow, the new-run form asks for a ticket URL and every prompt can read it.
Goal: {{ inputs.goal }}
Implement the ticket at {{ inputs.ticket_url }}. Keep the change to what the ticket asks for.Goal: Ship the ticket as one pull request
Implement the ticket at https://github.com/acme/shop/issues/42. Keep the change to what the ticket asks for.inputs.ticket_url always renders what the run started with. context.ticket_url renders the current value, which a later step may have changed.
List each branch’s result after a fan-in
Section titled “List each branch’s result after a fan-in”After a fan-in, each branch’s result sits under context.parallel.results, in edge order, with its node, outcome, failure_reason and context. Quote the index. Read each branch on its own line. Validation checks every value a prompt reads, and it cannot check a loop over the results.
Two reviewers read the change. Combine their findings into one list.
{{ context.parallel.results['0'].node }}: {{ context.parallel.results['0'].context.assessment }}{{ context.parallel.results['1'].node }}: {{ context.parallel.results['1'].context.assessment }}Two reviewers read the change. Combine their findings into one list.
security: The coupon code is not escaped before the query.tests: Nothing covers an expired coupon.Point at a folder
Section titled “Point at a folder”run.folders.<name>.path is where the run works on that folder now, which is a worktree once the run has one. project.folders.<name>.path is the folder as the project holds it. The name is the folder’s name in the project.
Work only inside {{ run.folders.web.path }}.Do not edit {{ project.folders.web.path }}. That folder belongs to the person who started the run.Work only inside /home/me/.orbital/worktrees/r7Kq2/web.Do not edit /work/shop/web. That folder belongs to the person who started the run.Reuse an earlier agent’s answer
Section titled “Reuse an earlier agent’s answer”context.response.<node> is the final prose of the agent step called <node>, without the outputs it handed over. It is empty until that step completes, and it names agent steps only.
The planner wrote this plan:
{{ context.response.plan }}
Implement it step by step.The planner wrote this plan:
1. Read the return URL from the query string.2. Redirect there after sign-in.
Implement it step by step.Quick reference
Section titled “Quick reference”| Write | To |
|---|---|
{{ context.verdict }} |
Print a value. |
{% if context.verdict == 'reject' %} … {% endif %} |
Include text only when a test holds. |
{% for name, folder in run.folders %} … {% endfor %} |
Repeat text for each item. |
{{ inputs.title | trim }} |
Change a value with a filter. |
{%- and -%} |
Trim the line break before or after a tag. |
{# note #} |
Leave a note the agent never sees. |
{% raw %} … {% endraw %} |
Print template syntax as written. |
Related
Section titled “Related”- Template variables: every name a prompt can read.
- Pass context between steps: how steps write the values prompts read.
- Context: what each context value holds and when it exists.
- History and threads: what a prompt already carries from earlier steps.