> For the index of every page in Orbital's docs, read https://docs.beta.runorbital.dev/llms.txt.

# 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.

```dot title="review.dot"
review [prompt="Review the change against this goal: {{ inputs.goal }}"]
```

The template language is [Nunjucks](https://mozilla.github.io/nunjucks/templating.html). Every example below shows the prompt and the text it renders to.

## 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

`{{ … }}` 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](/reference/template-variables/) lists every name.

```jinja title="prompt"
Work towards this goal: {{ inputs.goal }}
Extra notes: {{ inputs.notes }}
```

This run was started without the optional `notes` input, so that line ends empty:

```text title="renders"
Work towards this goal: Add a dark mode toggle to the settings page
Extra 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

`{% 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.

```jinja title="prompt"
{% 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`:

```text title="renders"
Fix every finding the reviewer listed.
```

The dashes in `-%}` stop each tag from leaving a blank line behind. [Whitespace control](#whitespace-control) explains them.

## Loops

`{% 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.

```jinja title="prompt"
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`:

```text title="renders"
Check each folder:
1. web at /work/shop/web
2. api at /work/shop/api
```

Context 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

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`. |

```jinja title="prompt"
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:

```text title="renders"
Branch: fix-login-redirect
Priority: NORMAL
Last failure: none
Folders: 1
Checks: lint, tests, types
```

`failure_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](https://mozilla.github.io/nunjucks/templating.html#builtin-filters) covers them all.

## 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:

```jinja title="prompt"
Review the change.
{% if context.failure_reason %}
The last attempt failed: {{ context.failure_reason }}
{% endif %}
Report what you found.
```

```text title="renders"
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.

```jinja title="prompt"
Review the change.
{%- if context.failure_reason %}
The last attempt failed: {{ context.failure_reason }}
{%- endif %}
Report what you found.
```

```text title="renders"
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

`{# … #}` holds a note for whoever edits the prompt. The agent never sees it.

```jinja title="prompt"
{# The reviewer reads this on a phone, so keep the prompt short. -#}
Review the change in three sentences or fewer.
```

```text title="renders"
Review the change in three sentences or fewer.
```

## 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.

```jinja title="prompt"
Add a greeting to the email template. It must say:
{% raw %}Hello {{ customer.name }}, your order has shipped.{% endraw %}
```

```text title="renders"
Add a greeting to the email template. It must say:
Hello {{ customer.name }}, your order has shipped.
```

## 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](/reference/node-types/#command) 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

### 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.

```jinja title="prompt"
Implement the goal: {{ inputs.goal }}
{%- if context.failure_reason %}

Your last attempt failed: {{ context.failure_reason }}
Fix that first.
{%- endif %}
```

After the tests failed:

```text title="renders"
Implement the goal: Fix the login redirect

Your last attempt failed: npm test exited with code 1: 2 of 48 tests failed
Fix that first.
```

On the first visit, the prompt ends after the goal.

### 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.

```jinja title="prompt"
Goal: {{ inputs.goal }}

Implement the ticket at {{ inputs.ticket_url }}. Keep the change to what the ticket asks for.
```

```text title="renders"
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

After a [fan-in](/workflows/run-steps-in-parallel/), 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.

```jinja title="prompt"
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 }}
```

```text title="renders"
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

`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.

```jinja title="prompt"
Work only inside {{ run.folders.web.path }}.
Do not edit {{ project.folders.web.path }}. That folder belongs to the person who started the run.
```

```text title="renders"
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

`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.

```jinja title="prompt"
The planner wrote this plan:

{{ context.response.plan }}

Implement it step by step.
```

```text title="renders"
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

| 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

- [Template variables](/reference/template-variables/): every name a prompt can read.
- [Pass context between steps](/workflows/pass-context/): how steps write the values prompts read.
- [Context](/reference/context/): what each context value holds and when it exists.
- [History and threads](/reference/history-and-threads/): what a prompt already carries from earlier steps.
