docs(hub): document durable workflow routing (#2943)

* docs(hub): document durable workflow routing

* docs(hub): make workflow guide progressive
This commit is contained in:
Mohamed Boudra
2026-08-06 16:07:41 +02:00
committed by GitHub
parent bb55e5bc0d
commit deb5e5c729
13 changed files with 693 additions and 578 deletions
+11 -9
View File
@@ -65,7 +65,7 @@ Request body:
```json
{
"projectSlug": "my-project",
"yaml": "environments:\n - name: production\n kind: daemon\n daemon: build-server\n cwd: /workspace\ntriggers:\n - name: deploy\n on: manual.run\n environment: production\n filters:\n from_users: [automation]\n agent:\n provider: opencode\n mode: full-access\n prompt: Deploy the project"
"yaml": "environments:\n - name: production\n kind: daemon\n daemon: build-server\n cwd: /workspace\ntriggers:\n - name: deploy\n on: manual.run\n max_runtime: 2h\n filters:\n from_users: [automation]\n steps:\n - id: deploy\n environment: production\n max_runtime: 90m\n idle_timeout: 10m\n agent:\n provider: opencode\n mode: full-access\n prompt:\n - text: Deploy the project"
}
```
@@ -117,17 +117,16 @@ Request body:
"trigger": "deploy",
"actor": "automation",
"deliveryKey": "deploy-2026-08-04-001",
"input": {
"ref": "main"
}
"input": "repo=project investigate the failed sync"
}
```
`expectedVersionId` is optional. When supplied, Hub rejects the dispatch if
that configuration revision is no longer active. `input` is application data
made available to the trigger; it may be any JSON value. Use a unique,
stable `deliveryKey` for each dispatch. Reusing it makes the request resolve to
the existing trigger instead of starting a duplicate run.
that configuration revision is no longer active. `input` is the same string
used by a provider message: consecutive leading `key=value` tokens are parsed
as the trigger's declared inputs, and the remainder becomes `${{ paseo.prompt }}`.
Use a unique, stable `deliveryKey` for each dispatch. Reusing it makes the
request resolve to the existing trigger instead of starting a duplicate run.
On success, Hub returns `200`:
@@ -159,10 +158,13 @@ curl --fail-with-body -sS -X POST "$HUB_URL/api/manual-runs" \
"trigger": "deploy",
"actor": "automation",
"deliveryKey": "deploy-2026-08-04-001",
"input": {"ref": "main"}
"input": "repo=project investigate the failed sync"
}'
```
See [Hub workflows](/docs/hub/workflows) for input types, defaults, choices,
rejected input, and manual invocation examples.
## Daemon enrollment
`daemons:enroll` issues a short-lived enrollment token for a daemon. The API
+38 -53
View File
@@ -1,6 +1,6 @@
---
title: How Hub works
description: The Hub object model, how an inbound event reaches a project, and what happens when a configuration is activated.
description: How a provider event reaches a workflow and a Paseo daemon, for hosted and self-hosted Hub.
nav: How it works
order: 62
category: Hub
@@ -8,72 +8,57 @@ category: Hub
# How Hub works
## The object model
Hub connects the places where requests arrive to the machines where your agents run. The same flow applies to hosted and self-hosted Hub.
```text
organization
├── members
├── connections GitHub installations, Slack workspaces, Discord guilds
├── daemons registered machines that run agents
└── projects
└── configuration
├── environments
└── triggers
GitHub / Slack / Discord / manual request
↓
Hub
matches a project trigger
↓
workflow
runs ordered agent steps
↓
Paseo daemon
starts the agent
```
**Connections belong to the organization.** One organization can hold several GitHub installations, several Slack workspaces, and several Discord guilds at the same time. Your personal GitHub account and two company organizations can sit side by side.
## The pieces
**Every connection gets a slug**, generated from the account it points at: `getpaseo-github`, `boudra-github`, `acme-slack`. Slugs are unique inside the organization, and configuration uses them to name a specific connection.
- A **connection** lets Hub receive events from GitHub, Slack, or Discord.
- A **daemon** is a registered machine running the Paseo daemon.
- A **project** groups one configuration with the connections and daemons it uses.
- An **environment** names where a workflow step runs: a daemon, its working directory, and an optional worktree.
- A **trigger** says which provider event can start a workflow and which events are allowed through.
- A **workflow** is the ordered set of steps that runs after a trigger matches.
- A **step** starts one agent execution, with its own prompt, agent selection, reply capabilities, and limits.
**Daemons belong to the organization too.** Register a machine once and any project can use it.
The configuration lives in `.paseo/hub.yml` when the project uses a GitHub source. A project has one active configuration revision at a time.
**A project is one set of environments and triggers.** Projects are how you separate work that should stay separate: one product, one team, one repository. They share the organization's connections and daemons, so separating costs you nothing.
## From event to agent
**A project has one active configuration.** You edit it in the dashboard or sync it from a repository, and it applies as a whole.
1. A provider sends an event to Hub. GitHub and Slack use webhooks; Discord uses its gateway connection; manual runs use the Hub API.
2. Hub verifies the provider event and identifies its project resource, such as a repository, workspace, or guild.
3. Hub evaluates triggers and their filters, including the required `from_users` allowlist.
4. A matching trigger creates a workflow run from the active configuration revision.
5. The workflow evaluates its next step. A false `if` condition skips that step; a true condition starts it on the configured daemon.
6. The daemon starts the agent and Hub records its replies, structured output, status, and completion.
7. The next step sees the completed step's output. When no steps remain, the workflow run is complete.
**Nothing is attached to a project in the dashboard.** No screen assigns a repository or a channel. The project's configuration decides what it listens to, through trigger filters.
## Routing
```text
event arrives
→ the connection verifies it (signature, or an authenticated gateway)
→ the event carries a resource id (repository, workspace, guild)
→ Hub finds compiled routes referencing that resource
→ the trigger's filters run
→ the trigger's environment executes
```
Routes are compiled when a configuration is activated, not evaluated per event. A repository is not owned by one project, so if two projects both name it, both run.
The [Workflows guide](/docs/hub/workflows) starts with a one-step Slack example and adds inputs, structured outputs, and routing one concept at a time.
## Activation
Activating a configuration resolves what you wrote into stable identities:
When Hub syncs `.paseo/hub.yml`, it validates the configuration and resolves its references:
| You write | Hub resolves it to |
| ---------------------------- | --------------------------------------- |
| `filters.repo: owner/repo` | a GitHub repository id and a connection |
| `filters.workspace: T0123…` | a Slack workspace and a connection |
| `filters.guild: 9876…` | a Discord guild and a connection |
| `environment.daemon: my-box` | a registered daemon id |
- `filters.repo`, `filters.workspace`, and `filters.guild` must name resources available through the organization's connections.
- `environment.daemon` must name a registered daemon.
- Step ids, expressions, input filters, output schemas, and durations must be valid.
If any of those cannot be resolved through a connection in the organization, activation fails and the previous revision stays active. You find out when you push, rather than when someone comments on an issue.
If activation fails, Hub keeps the previous active revision. The Configuration tab shows the failed sync and its validation error; Activity continues to reflect the last active revision.
A trigger with no resource filter routes to every connection of that provider in the organization. Add `filters.connection: <slug>` to narrow it to one.
## Security boundaries
## Executions
Triggers require a non-empty `from_users` allowlist for externally sourced events. Protect the configuration repository because anyone who can change the active configuration can choose which connections, daemons, and agent capabilities a project uses.
A matched trigger produces an execution. Hub sends the create request to the daemon, then owns that agent's lifecycle: reconnect recovery, output observation, and completion.
- `timeout` is absolute and defaults to `1h`.
- `idle_timeout` starts only when the daemon reports the agent idle, resets on the next idle report, and clears when the agent reports running. It defaults to `5m`.
- Every dispatched agent gets a per-execution MCP server with `finish_execution`. Completion is authoritative when the agent calls it.
- `allow_outputs` adds a single-use `reply` tool for Slack and Discord executions.
If Hub loses the create response, or the daemon reconnects, Hub resends the same create intent with the same execution id. The daemon returns the existing agent instead of running the prompt twice.
## Trust boundary
A project's configuration can reference any connection in its organization. Anyone who can push to the configuration repository controls that project's access to those connections, so protect that branch the way you protect a release branch.
Triggers require a `from_users` allowlist for the same reason. Without one, any stranger commenting on a public issue could start an agent on your machine.
GitHub-triggered steps receive a scoped GitHub credential for the triggering repository. Slack and Discord do not implicitly choose a GitHub connection.
+190 -136
View File
@@ -1,6 +1,6 @@
---
title: Hub examples
description: Copyable hub.yml configurations for issue triage, PR review, Discord and Slack mentions, and isolated worktrees.
description: Copyable Hub workflow configurations for typed routing, classification, direct answers, implementations, and provider replies.
nav: Examples
order: 70
category: Hub
@@ -8,175 +8,229 @@ category: Hub
# Hub examples
Complete configurations you can adapt. Field details are in the [`hub.yml` reference](/docs/hub/configuration/hub-yml).
These examples use the durable step syntax. Replace the daemon, paths, provider identifiers, and provider filters with resources in your organization. The shared workflow page explains the contract behind each pattern.
Every example assumes one environment:
## Model and provider selection
Use finite choices when caller input selects an agent provider or model.
```yaml
environments:
- name: dev
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/api
```
cwd: /Users/you/code/project
## Answer a mention on an issue
The baseline. An agent runs in your checkout and can push, because GitHub-triggered executions get a scoped `GH_TOKEN`.
```yaml
triggers:
- name: mention
on: github.issue_comment
environment: dev
- name: selectable-agent
on: manual.run
max_runtime: 2h
filters:
repo: acme/api
contains: "@paseo"
from_users: [alice, bob]
agent:
provider: codex
mode: full-access
prompt: |
You were mentioned on ${{ paseo.event.github.trigger_url }}.
${{ paseo.event.github.comment.body }}
Reply by commenting on the thread with gh.
from_users: [automation]
inputs:
provider:
type: string
default: codex
choices: [codex, claude]
model:
type: string
default: standard
choices: [standard, fast]
steps:
- id: work
environment: development
max_runtime: 90m
idle_timeout: 10m
agent:
provider: ${{ paseo.inputs.provider }}
model: ${{ paseo.inputs.model }}
mode: full-access
prompt:
- text: ${{ paseo.prompt }}
```
## Review a pull request in its own worktree
Invoke it with `provider=claude model=fast investigate the sync`. An undeclared leading key stops header parsing and becomes prompt text.
`checkout-branch` on the PR's head branch puts the agent on the right code without disturbing your working tree.
## Repository routing
```yaml
triggers:
- name: review
on: github.pull_request_review_comment
environment: review
filters:
repo: acme/api
contains: "@paseo review"
from_users: [alice]
agent:
provider: claude
mode: full-access
timeout: 30m
auto_archive: true
prompt: |
Address this review comment on PR #${{ paseo.event.github.pull_request.number }}:
${{ paseo.event.github.comment.body }}
```
Add the worktree to the environment it uses:
Use a declared input plus `filters.inputs` when separate triggers should own separate repositories or projects.
```yaml
environments:
- name: review
- name: project
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/api
worktree:
mode: checkout-branch
branch: ${{ paseo.event.github.pull_request.head.ref }}
```
## Triage new issues on a fresh branch
```yaml
environments:
- name: triage
cwd: /Users/you/code/project
- name: paseo
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/api
worktree:
mode: branch-off
newBranch: triage/${{ paseo.event.github.issue.number }}
base: main
cwd: /Users/you/code/paseo
triggers:
- name: triage
on: github.issues
environment: triage
filters:
repo: acme/api
from_users: [alice, bob]
agent:
provider: codex
mode: full-access
prompt: |
Investigate issue #${{ paseo.event.github.issue.number }}:
${{ paseo.event.github.issue.title }}
${{ paseo.event.github.issue.body }}
If you find the cause, open a pull request.
```
## Discord mention with repository access
The trigger fires from Discord, so the GitHub token has to be requested by connection slug.
```yaml
triggers:
- name: discord
on: discord.mention
environment: dev
filters:
guild: "123456789012345678"
from_users: ["345678901234567890"]
agent:
provider: codex
mode: full-access
env:
GH_TOKEN: ${{ paseo.connections.acme-github.token }}
allow_outputs:
- type: discord.reply
prompt: |
${{ paseo.event.discord.trigger_message.body }}
Reply in the thread when you are done.
```
## Slack mention that replies in-thread
```yaml
triggers:
- name: slack
- name: project-request
on: slack.mention
environment: dev
max_runtime: 2h
filters:
workspace: T01234567
channels: [C01234567]
from_users: [U01234567]
agent:
provider: codex
mode: full-access
idle_timeout: 10m
allow_outputs:
- type: slack.reply
prompt: ${{ paseo.event.slack.trigger_message.body }}
inputs: { repo: project }
inputs:
repo:
type: string
choices: [project, paseo]
steps:
- id: project-work
environment: project
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: ${{ paseo.prompt }}
allow_outputs:
- type: slack.reply
max: 5
- name: paseo-request
on: slack.mention
max_runtime: 2h
filters:
workspace: T01234567
from_users: [U01234567]
inputs: { repo: paseo }
inputs:
repo:
type: string
choices: [project, paseo]
steps:
- id: paseo-work
environment: paseo
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: ${{ paseo.prompt }}
allow_outputs:
- type: slack.reply
max: 5
```
## One trigger, several repositories
The same event can match multiple triggers, so the input filters make these routes exclusive.
Omit `repo` and the trigger listens to every repository the GitHub connection can see. Pin the connection when the organization has more than one.
## Safety gate and direct answer
For a request that may either be answered or implemented, let a read-only classifier choose a finite branch. Supplying `kind=answer` or `kind=implementation` skips classification.
```yaml
environments:
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/project
triggers:
- name: any-repo
- name: request
on: github.issue_comment
environment: dev
max_runtime: 2h
filters:
connection: acme-github
repo: example/project
contains: "@paseo"
from_users: [alice]
agent:
provider: codex
mode: full-access
prompt: |
${{ paseo.event.github.repository_full_name }}:
${{ paseo.event.github.comment.body }}
from_users: [maintainer]
inputs:
kind:
type: string
choices: [answer, implementation]
steps:
- id: classify
if: ${{ paseo.inputs.kind == null }}
environment: development
max_runtime: 2m
idle_timeout: 30s
agent:
provider: codex
mode: read-only
prompt:
- text: Classify this request as answer or implementation.
- text: ${{ paseo.prompt }}
output:
schema:
type: object
additionalProperties: false
required: [kind]
properties:
kind:
enum: [answer, implementation]
- id: answer
if: ${{ paseo.inputs.kind == 'answer' || steps.classify.outputs.kind == 'answer' }}
environment: development
max_runtime: 10m
idle_timeout: 2m
agent:
provider: codex
mode: read-only
prompt:
- text: Answer the request. Do not change files.
- text: ${{ paseo.prompt }}
- id: implementation
if: ${{ paseo.inputs.kind == 'implementation' || steps.classify.outputs.kind == 'implementation' }}
environment: development
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: Implement the request, verify it, and report the result.
- text: ${{ paseo.prompt }}
allow_outputs:
- type: github.reply
max: 5
```
`cwd` is fixed per environment, so a trigger like this makes sense when the agent works from a token rather than a checkout.
The answer and implementation conditions cannot both be true for one classification. The workflow ends after the answer step because there is no later matching step.
## PR progress and final updates
Keep progress and final updates on the implementation step. The step can use up to five replies while it works, then finish normally.
```yaml
environments:
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/project
triggers:
- name: pr-work
on: github.pull_request_review_comment
max_runtime: 2h
filters:
repo: example/project
contains: "@paseo"
from_users: [maintainer]
steps:
- id: implement-review
environment: development
max_runtime: 90m
idle_timeout: 10m
auto_archive: true
agent:
provider: codex
mode: full-access
prompt:
- text: |
Address the review request.
Request: ${{ paseo.prompt }}
allow_outputs:
- type: github.reply
max: 5
```
For Slack and Discord, use `slack.reply` or `discord.reply` instead. GitHub-triggered agents also receive the scoped GitHub credential for the triggering repository, so the agent can use `gh` for repository updates.
See [Hub workflows](/docs/hub/workflows) for partials, deadlines, structured output retry, and provider invocation details.
+120 -116
View File
@@ -1,6 +1,6 @@
---
title: hub.yml reference
description: Every field in a Paseo Hub configuration: environments, triggers, filters, outputs, and merge variables.
description: The authored Hub configuration: environments, triggers, typed inputs, workflow steps, routing, and prompt partials.
nav: hub.yml reference
order: 69
category: Hub
@@ -8,143 +8,147 @@ category: Hub
# `hub.yml` reference
The complete field reference for a Hub configuration. For where the file lives and how it activates, see [Configuration](/docs/hub/configuration).
```text
environments: where agents run
triggers: what starts them
```
Both keys are required. `environments` needs at least one entry.
## Environments
An environment says where an agent runs.
A configuration has `environments` and `triggers`. Execution fields belong to each trigger's `steps`.
```yaml
environments:
- name: dev
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/your-repo
cwd: /Users/you/code/project
triggers:
- name: request
on: manual.run
max_runtime: 2h
filters:
from_users: [automation]
steps:
- id: work
environment: development
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: ${{ paseo.prompt }}
```
| Field | Required | Notes |
| ---------- | -------- | ----------------------------------------------------------- |
| `name` | yes | Referenced by a trigger's `environment`. |
| `kind` | yes | `daemon`. Only daemon environments run today. |
| `daemon` | yes | The daemon's display name, resolved to an id on activation. |
| `cwd` | yes | Absolute path on that machine. |
| `worktree` | no | Run in an isolated git worktree instead of `cwd` directly. |
## Environments
### Worktrees
| Field | Required | Notes |
| ---------- | ----------- | --------------------------------------------------------------------------------------------------------- |
| `name` | yes | Lowercase identifier referenced by a step. |
| `kind` | yes | `daemon`, `fly`, or `docker` in the authored schema; workflow steps must resolve to a daemon environment. |
| `daemon` | daemon only | Daemon name, resolved when the revision activates. |
| `cwd` | daemon only | Absolute path on the daemon. |
| `image` | fly/docker | Image name. |
| `worktree` | no | `branch-off`, `checkout-branch`, or `checkout-pr` target. |
```yaml
worktree:
mode: branch-off
newBranch: hub/issue-${{ paseo.event.github.issue.number }}
base: main
```
Three modes:
| Mode | Fields | Behavior |
| ----------------- | ---------------------------- | ------------------------------------------ |
| `branch-off` | `newBranch`, optional `base` | New branch off `base`, or the current head |
| `checkout-branch` | `branch` | Existing branch |
| `checkout-pr` | `prNumber` | The pull request's head |
`newBranch`, `base`, and `branch` accept merge variables. `prNumber` does not, because it is a literal number. For a per-event pull request use `checkout-branch` with `${{ paseo.event.github.pull_request.head.ref }}`.
See [Git worktrees](/docs/worktrees) for how the daemon sets them up.
The `worktree` object is part of the environment. Its fields are exact authored names: `newBranch` and optional `base` for `branch-off`, `branch` for `checkout-branch`, and positive integer `prNumber` for `checkout-pr`.
## Triggers
```yaml
triggers:
- name: review
on: github.pull_request_review_comment
environment: dev
filters:
repo: acme/api
contains: "@paseo"
from_users: [alice, bob]
agent:
provider: codex
mode: full-access
prompt: ${{ paseo.event.github.comment.body }}
timeout: 45m
idle_timeout: 5m
auto_archive: true
```
| Field | Required | Notes |
| ------------- | ------------ | ----------------------------------------------------------------------------------------------- |
| `name` | yes | Lowercase identifier, unique in the configuration. |
| `on` | yes | `provider.event`, such as `slack.mention` or `manual.run`. |
| `max_runtime` | yes | Positive duration for the complete trigger run, up to 24h. |
| `filters` | no in schema | Provider filters; externally sourced triggers still require a non-empty `from_users` allowlist. |
| `inputs` | no | Typed leading `key=value` invocation headers. |
| `values` | no | Derived expressions. |
| `steps` | yes | One or more ordered steps. |
| Field | Required | Default | Notes |
| --------------- | -------- | ------- | --------------------------------------------------------- |
| `name` | yes | | Unique within the configuration. |
| `on` | yes | | `provider.event`, see below. |
| `environment` | yes | | An environment `name`. |
| `agent` | yes | | `provider`, `mode`, optional `model`, `thinkingOptionId`. |
| `prompt` | yes | | Supports merge variables. |
| `filters` | yes | | `from_users` is mandatory. See below. |
| `env` | no | | Environment variables for the agent process. |
| `allow_outputs` | no | none | Lets the agent reply to the source. |
| `timeout` | no | `1h` | Absolute wall clock. Max `24h`. |
| `idle_timeout` | no | `5m` | Starts on the first idle report from the daemon. |
| `auto_archive` | no | `false` | Archive the agent when the execution ends. |
### Inputs
Durations are a positive integer plus `ms`, `s`, `m`, or `h`.
`agent.provider` and `agent.mode` are the same identifiers the Paseo CLI uses, such as `codex`, `claude`, and `full-access`. See [Supported providers](/docs/supported-providers).
## Events and filters
`on` and `filters` decide which events reach a trigger. Both are documented in [Triggers](/docs/hub/triggers), with a page per provider for the events and data each one exposes.
`filters` is required, and `from_users` must be non-empty. Validation rejects a trigger without it.
## Merge variables
`${{ ... }}` expressions are available in `prompt`, `env`, and worktree branch fields. Values reach the agent's process only through `env`; nothing else is injected.
### Event data
`${{ paseo.event.<provider>.<path> }}` exposes the provider's own payload, unflattened. The full field list per provider lives with its trigger page: [GitHub](/docs/hub/triggers/github), [Slack](/docs/hub/triggers/slack), [Discord](/docs/hub/triggers/discord).
Manual runs expose `actor`, `input`, `config`, `trigger`, and `delivery_id` under `paseo.event.manual`.
### Connection credentials
`${{ paseo.connections.<slug>.token }}` mints a token from a named connection. GitHub connections are the only ones that provide a token today.
Use it when a trigger needs a provider other than the one that fired it:
Each input has:
```yaml
triggers:
- name: discord-fix
on: discord.mention
environment: dev
filters:
guild: "123456789"
from_users: ["987654321"]
agent:
provider: codex
mode: full-access
env:
GH_TOKEN: ${{ paseo.connections.acme-github.token }}
prompt: ${{ paseo.event.discord.trigger_message.body }}
inputs:
repo:
type: string
required: false
choices: [project, paseo]
agent:
type: string
default: codex
choices: [codex, claude]
```
You must name the connection. An organization can hold several GitHub installations and Hub will not guess between them.
`type` is `string`, `number`, or `boolean`. `required`, `default`, and `choices` are optional. `required` and `default` cannot be combined. Defaults and choices must match the declared type; a default must be one of the choices.
GitHub-triggered executions already receive a scoped `GH_TOKEN` for the triggering repository. You do not need to map one.
Inputs may be referenced as `${{ paseo.inputs.name }}`. A dynamic authority-bearing field such as a provider, model, mode, or environment requires finite `choices` at activation. A prompt cannot supply authority.
## Outputs
### Values
Values bind expressions under their own namespace:
```yaml
allow_outputs:
- type: slack.reply
values:
selected_repo: ${{ paseo.inputs.repo ?? steps.classify.outputs.repo }}
```
`slack.reply` and `discord.reply` give the agent a `reply` tool for the conversation that triggered it. The reply is plain text, posted in-thread, and can be sent once per execution.
The grammar supports paths, JSON literals, parentheses, `!`, `==`, `!=`, `&&`, `||`, and `??`. It does not support function calls, JavaScript, arithmetic, mutation, or implicit string coercion. Referenced steps must exist and value dependencies cannot cycle.
Every execution also gets `finish_execution`, whether or not `allow_outputs` is set.
### Steps
| Field | Required | Notes |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `id` | yes | Lowercase step identifier, unique within the trigger. |
| `environment` | yes | Environment name or a finite input expression resolving to one. |
| `max_runtime` | yes | Positive step hard limit, up to 24h. |
| `idle_timeout` | yes | Positive idle limit, no longer than the step hard limit. |
| `agent` | yes | `provider`, optional `model`, `mode`, and `thinkingOptionId`. |
| `prompt` | yes | Non-empty list of `text` and GitHub-only `include` blocks. |
| `if` | no | Expression deciding whether this ordered step runs. |
| `output` | no | `{ schema: <JSON Schema> }` for structured `finish_execution`. |
| `allow_outputs` | no | Reply capabilities such as `slack.reply`, `discord.reply`, or `github.reply`, each with optional `max`. |
| `auto_archive` | no | Archives the step's agent when it ends. |
Prompt blocks are objects, not a scalar prompt:
```yaml
prompt:
- text: Request: ${{ paseo.prompt }}
- include: developer.md
```
Use `${{ paseo.prompt }}`, `${{ paseo.inputs.* }}`, `${{ steps.*.outputs.* }}`, and `${{ values.* }}` in prompts, conditions, and agent selection fields. Provider event payloads are not part of this workflow expression namespace; provider adapters put the normalized request into the prompt and preserve the raw event as evidence.
## Prompt partials
`include` paths are relative to `.paseo/partials/` and are resolved at the exact GitHub configuration commit. Hub stores the resolved content and SHA-256 hash in the immutable revision. Missing files, unsafe paths, symlinks, submodules, directories, and nested includes are rejected. Manual configurations cannot use repository partials.
## Deadlines
The trigger's `max_runtime` is the hard limit for the complete workflow run. Each step also has `max_runtime` and `idle_timeout`:
```yaml
max_runtime: 2h
steps:
- id: classify
max_runtime: 2m
idle_timeout: 30s
- id: implement
max_runtime: 90m
idle_timeout: 10m
```
The effective step hard and idle deadlines are capped by the remaining trigger deadline. Meaningful daemon activity refreshes idle time, but cannot extend a hard deadline. Hub persists absolute deadlines, so a restart or deployment does not reset them. A step timeout fails the run; a trigger timeout stops later steps and interrupts a live agent.
## Provider invocation
The provider removes its mention or marker before Hub parses leading declared input tokens. Slack and Discord place the inputs immediately after the bot mention. GitHub places them after the configured marker. Manual runs send the same string as the API `input`:
```text
@Paseo repo=project investigate the failed sync
```
The first token that is not a declared input begins the prompt. The clean prompt is available as `${{ paseo.prompt }}`. The raw provider message remains separate Activity evidence. See [provider triggers](/docs/hub/triggers) for provider-specific marker and filter behavior.
## Removed fields
Do not put execution fields directly on a trigger. `environment`, `agent`, `prompt`, `timeout`, `idle_timeout`, `auto_archive`, and `allow_outputs` are step fields now. The duration field is `max_runtime`; `timeout` is not an alias.
Next: [Hub workflows](/docs/hub/workflows) for routing patterns and copyable configurations.
+1 -1
View File
@@ -56,4 +56,4 @@ While a project uses a GitHub source, the dashboard editor is read-only. The rep
`filters.repo` can name any repository the organization has a connection for. Keeping `hub.yml` in a private repository while triggers watch several public ones is a common setup, because push access to the configuration repository grants access to the organization's connections.
Next: the [`hub.yml` reference](/docs/hub/configuration/hub-yml).
Next: [Hub workflows](/docs/hub/workflows), then the [`hub.yml` reference](/docs/hub/configuration/hub-yml).
+1 -1
View File
@@ -57,7 +57,7 @@ To keep executions off your working tree, add a worktree:
```yaml
worktree:
mode: branch-off
newBranch: hub/${{ paseo.event.github.issue.number }}
newBranch: hub/investigation
base: main
```
+2 -1
View File
@@ -46,7 +46,8 @@ A project is one set of environments and triggers. Split your work into projects
1. [How it works](/docs/hub/concepts)
2. [Daemons](/docs/hub/daemons)
3. [Triggers](/docs/hub/triggers)
4. [Configuration](/docs/hub/configuration)
4. [Workflows](/docs/hub/workflows)
5. [Configuration](/docs/hub/configuration)
[Quickstart](/docs/hub/quickstart) goes end to end if you would rather start by doing.
+13 -8
View File
@@ -48,18 +48,23 @@ environments:
triggers:
- name: mention
on: github.issue_comment
environment: dev
max_runtime: 2h
filters:
repo: yourname/your-repo
contains: "@paseo"
from_users: [your-github-login]
agent:
provider: codex
mode: full-access
prompt: |
Someone asked for help on ${{ paseo.event.github.issue.html_url }}.
${{ paseo.event.github.comment.body }}
steps:
- id: work
environment: dev
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: |
Someone asked for help.
${{ paseo.prompt }}
```
`daemon` is the name you gave it in step 3. `cwd` is a directory on that machine.
+21 -87
View File
@@ -1,6 +1,6 @@
---
title: Discord triggers
description: Discord mentions as Hub triggers, snowflake filters, thread context, and the full paseo.event.discord data reference.
description: Configure Discord mentions as Hub triggers and route them into durable workflows.
nav: Discord
order: 67
category: Hub
@@ -10,15 +10,13 @@ category: Hub
## Events
| `on` | Fires when |
| ----------------- | ------------------------------------------------- |
| `discord.mention` | The bot is mentioned in a guild channel or thread |
Mentioning a role the bot holds counts as mentioning the bot.
| `on` | Fires when |
| ----------------- | ------------------------------------------------------------------------------ |
| `discord.mention` | The bot or one of its managed roles is mentioned in a guild channel or thread. |
## Filters
Discord filters take snowflake IDs. Turn on **Developer Mode** in Discord's advanced settings, then right-click a channel or user to copy its ID.
Discord filters use quoted snowflake IDs:
```yaml
filters:
@@ -27,92 +25,28 @@ filters:
from_users: ["345678901234567890"]
```
Quote them. Unquoted snowflakes parse as numbers and lose precision.
Turn on Developer Mode to copy IDs. `from_users` matches the author's user id, `guild` matches the connected guild, and `channels` matches the channel or thread parent. `pattern` matches the start of the text after the mention. All filters must pass.
- `from_users` matches the author's user id.
- `guild` is resolved to the connection on activation.
- `channels` matches the channel the message was posted in; in a thread, the thread's parent channel counts.
- `pattern` matches the start of the text after the mention.
## Invocation
## Replying
Put leading inputs directly after the mention:
```text
@Paseo repo=project investigate the failed sync
```
Hub consumes only declared consecutive headers and passes `investigate the failed sync` as `${{ paseo.prompt }}`. See [Hub workflows](/docs/hub/workflows) for the provider-neutral input contract.
## Replies and repository access
Put the reply capability on a step:
```yaml
allow_outputs:
- type: discord.reply
max: 5
```
One plain-text reply per execution, in the triggering thread or channel.
The reply is posted in the triggering thread or channel. A Discord trigger has no implicit GitHub credential. If a step needs another connection, configure that connection through the supported step environment for your deployment; Hub does not guess between GitHub installations.
## Repository access
A Discord trigger has no implicit GitHub credential. Name the connection you want:
```yaml
env:
GH_TOKEN: ${{ paseo.connections.acme-github.token }}
```
Hub will not guess between several GitHub installations.
## Data reference
The triggering message is `paseo.event.discord.trigger_message`:
| Path | Value |
| -------------------- | ----------------------------------------------------- |
| `id` | Message snowflake |
| `content` | Raw text, including the mention |
| `body` | Text with the mention stripped, usually what you want |
| `url` | Canonical message link |
| `author` | Author identity, including the user id |
| `channel` | Channel identity |
| `thread` | Thread identity, or `null` outside a thread |
| `created_at` | Message creation time |
| `attachments` | Typed list, see below |
| `referenced_message` | Identity of a replied-to message, or `null` |
Each attachment has `id`, `filename`, `url`, `content_type`, and `size`. A `referenced_message` carries `id`, `channel_id`, and `guild_id`, without the message content.
Also available: `paseo.event.discord.event_type` and `paseo.event.discord.guild.id`.
### Thread context
In a thread, `paseo.event.discord.trigger_thread_context.messages` holds the preceding messages, **oldest first**, never including the triggering message. Each entry exposes the same identity, content, author, channel, creation time, attachment, and reference fields as `trigger_message`.
This is how you give an agent the conversation rather than one line:
```yaml
prompt: |
Thread so far:
${{ paseo.event.discord.trigger_thread_context.messages }}
Latest request:
${{ paseo.event.discord.trigger_message.body }}
```
Outside a thread the list is empty.
## Example
```yaml
triggers:
- name: discord-fix
on: discord.mention
environment: dev
filters:
guild: "123456789012345678"
channels: ["234567890123456789"]
from_users: ["345678901234567890"]
agent:
provider: codex
mode: full-access
env:
GH_TOKEN: ${{ paseo.connections.acme-github.token }}
allow_outputs:
- type: discord.reply
prompt: |
${{ paseo.event.discord.trigger_message.body }}
Context: ${{ paseo.event.discord.trigger_message.url }}
Reply in the thread when you're done.
```
See the [workflow examples](/docs/hub/configuration/examples) for complete step configurations.
+24 -84
View File
@@ -1,6 +1,6 @@
---
title: GitHub triggers
description: GitHub events Hub can trigger on, how filters match them, and the full paseo.event.github data reference.
description: Configure GitHub events as Hub triggers and route them into durable workflows.
nav: GitHub
order: 65
category: Hub
@@ -10,104 +10,44 @@ category: Hub
## Events
| `on` | Fires when |
| ------------------------------------ | ------------------------------------- |
| `github.issue_comment` | A comment on an issue or pull request |
| `github.issues` | An issue is opened or edited |
| `github.pull_request_review` | A review is submitted |
| `github.pull_request_review_comment` | A comment on a diff |
| `on` | Fires when |
| ------------------------------------ | ------------------------------------------------- |
| `github.issue_comment` | A comment on an issue or pull request is created. |
| `github.issues` | An issue is opened or edited. |
| `github.pull_request_review` | A review is submitted. |
| `github.pull_request_review_comment` | A comment is added to a diff. |
Which repositories produce events is set on the GitHub connection's installation, not in Paseo.
Which repositories produce events is set on the GitHub App installation. `filters.repo` narrows a trigger to one `owner/name` repository.
## Filters
```yaml
filters:
repo: acme/api
repo: example/project
contains: "@paseo"
from_users: [alice, bob]
from_users: [maintainer]
```
- `from_users` matches the GitHub **login** of the event's sender.
- `repo` is `owner/name`, resolved to the repository's immutable id on activation.
- `contains` is a substring test; `pattern` must match at the start.
`from_users` matches the sender's GitHub login. `contains` checks the event text; `pattern` checks its start. The text is the comment body for comments and reviews, and the issue title plus body for issues. All filters must pass.
The text those two search depends on the event:
## Invocation
| Event | Text searched |
| ------------------------------------ | -------------------- |
| `github.issue_comment` | The comment body |
| `github.issues` | Issue title and body |
| `github.pull_request_review` | The review body |
| `github.pull_request_review_comment` | The comment body |
Put the configured marker in the message, then put leading inputs in the text parsed after that marker:
## Tokens
```text
@paseo repo=project agent=claude investigate the failed sync
```
A GitHub-triggered execution automatically receives a `GH_TOKEN` scoped to the triggering repository, with `contents`, `issues`, and `pull_requests` write access. It is minted per execution and revoked when the execution ends. You do not map it in `env`.
Hub consumes only declared consecutive headers and passes `investigate the failed sync` as `${{ paseo.prompt }}`. See [Hub workflows](/docs/hub/workflows) for input types, defaults, choices, and rejected Activity records.
For a token from a _different_ connection, or from a non-GitHub trigger, name it explicitly:
## Credentials and replies
GitHub-triggered steps receive a scoped `GH_TOKEN` for the triggering repository. The agent can use `gh` to comment, push, and open a pull request. A step may also declare:
```yaml
env:
GH_TOKEN: ${{ paseo.connections.acme-github.token }}
allow_outputs:
- type: github.reply
max: 5
```
## Data reference
GitHub's webhook payload is available directly under `paseo.event.github`, unflattened. Anything GitHub sends, you can read:
```yaml
prompt: |
${{ paseo.event.github.issue.title }}
by ${{ paseo.event.github.sender.login }}
on ${{ paseo.event.github.repository.full_name }}
```
Common paths by event:
| Event | Useful paths |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| `github.issue_comment` | `comment.body`, `comment.html_url`, `issue.number`, `issue.title`, `issue.body` |
| `github.issues` | `issue.number`, `issue.title`, `issue.body`, `action` |
| `github.pull_request_review` | `review.body`, `review.state`, `pull_request.number` |
| `github.pull_request_review_comment` | `comment.body`, `comment.path`, `comment.diff_hunk`, `pull_request.number` |
All events also carry `sender.login`, `repository.full_name`, and `repository.default_branch`.
Hub adds an envelope alongside GitHub's fields:
| Path | Value |
| ---------------------- | ---------------------------------------------- |
| `delivery_id` | GitHub's webhook delivery id |
| `event_name` | `issue_comment`, `issues`, … |
| `repository_full_name` | `owner/name` |
| `installation_id` | The installation that delivered the event |
| `received_at` | When Hub accepted it |
| `trigger_url` | Canonical URL of the comment, issue, or review |
`trigger_url` is only present when the event has one. It is the field you usually want in a prompt.
## Example
```yaml
triggers:
- name: review-fix
on: github.pull_request_review_comment
environment: dev
filters:
repo: acme/api
contains: "@paseo"
from_users: [alice]
agent:
provider: codex
mode: full-access
prompt: |
${{ paseo.event.github.sender.login }} left this on PR
#${{ paseo.event.github.pull_request.number }},
in ${{ paseo.event.github.comment.path }}:
${{ paseo.event.github.comment.body }}
Fix it and push. Reply on the thread with gh when you're done.
${{ paseo.event.github.trigger_url }}
```
See the [workflow examples](/docs/hub/configuration/examples) for review and PR workflows.
+12 -15
View File
@@ -2,27 +2,31 @@
title: Hub triggers
description: How Hub matches an inbound event to a trigger: events, filters, and the allowlist that gates every execution.
nav: Triggers
order: 64
order: 65
category: Hub
---
# Triggers
A trigger says: when this event arrives, from this source, from these people, run this agent with this prompt.
A trigger says which provider event can start a workflow. The [Hub workflows](/docs/hub/workflows) page covers the steps, inputs, routing, prompts, and deadlines that run after a match.
```yaml
triggers:
- name: mention
on: github.issue_comment
environment: dev
filters:
repo: acme/api
contains: "@paseo"
from_users: [alice]
agent:
provider: codex
mode: full-access
prompt: ${{ paseo.event.github.comment.body }}
max_runtime: 2h
steps:
- id: work
environment: dev
max_runtime: 90m
idle_timeout: 10m
agent: { provider: codex, mode: full-access }
prompt:
- text: ${{ paseo.prompt }}
```
Field-by-field detail is in the [`hub.yml` reference](/docs/hub/configuration/hub-yml). This page covers matching.
@@ -84,11 +88,4 @@ Both run. Triggers are not ordered and do not shadow each other, in one configur
## Replying
`allow_outputs` gives the agent a single-use `reply` tool for the conversation that triggered it:
```yaml
allow_outputs:
- type: slack.reply
```
Only `slack.reply` and `discord.reply` exist. GitHub-triggered agents reply with the scoped `GH_TOKEN` they already have, so `gh issue comment` works.
Put `allow_outputs` on the step that should reply. The available provider reply types are `slack.reply`, `discord.reply`, and `github.reply`; set `max` when a step needs more than one update. GitHub-triggered agents also receive a scoped GitHub credential for `gh`.
+22 -67
View File
@@ -1,6 +1,6 @@
---
title: Slack triggers
description: Slack mentions as Hub triggers, ID-based filters, replies, and the full paseo.event.slack data reference.
description: Configure Slack mentions as Hub triggers and route them into durable workflows.
nav: Slack
order: 66
category: Hub
@@ -10,89 +10,44 @@ category: Hub
## Events
| `on` | Fires when |
| --------------- | -------------------------------------------------------- |
| `slack.mention` | The bot is mentioned in a channel it has been invited to |
| `on` | Fires when |
| --------------- | --------------------------------------------------------- |
| `slack.mention` | The bot is mentioned in a channel it has been invited to. |
The bot must be in the channel. `/invite @Paseo` first, or nothing arrives.
The mention is required. Direct messages, slash commands, and interactive components do not produce this trigger.
## Filters
Slack filters take IDs, never display names. Copy a channel ID from the channel's details; copy a user ID from a member's profile menu.
Slack filters use IDs, not display names:
```yaml
filters:
workspace: T01234567
channels: [C01234567]
from_users: [U01234567]
pattern: "repo="
```
- `from_users` matches the Slack user id of the author.
- `workspace` is the team id, resolved to the connection on activation.
- `channels` matches the channel id the message was posted in.
- `pattern` matches the start of the text **after** the mention, and must end on a word boundary. `contains` is treated the same as `pattern` here.
`from_users` matches the author's Slack user id. `workspace` is the team id. `channels` matches the channel id. `pattern` (or `contains`) matches the start of the text after the mention. All filters must pass.
The mention itself is always required. A message that does not mention the bot never produces an event.
## Invocation
## Replying
Put leading inputs directly after the mention:
```text
@Paseo repo=project agent=claude investigate the failed sync
```
Hub consumes only declared consecutive headers and passes `investigate the failed sync` as `${{ paseo.prompt }}`. See [Hub workflows](/docs/hub/workflows) for input types, defaults, choices, and rejection behavior.
## Replies and workflow shape
Put the reply capability on a step:
```yaml
allow_outputs:
- type: slack.reply
max: 5
```
That gives the agent one plain-text reply, posted in the thread. A root message gets a new thread; a threaded message gets a reply in the same thread.
## Data reference
Everything lives under `paseo.event.slack`.
| Path | Value |
| ------------ | --------------------------------- |
| `event_type` | `mention` |
| `event_id` | Slack's Events API event id |
| `event_ts` | Event timestamp from the envelope |
| `event_time` | Envelope delivery time |
| `team` | Workspace identity |
| `app` | The installed app identity |
The message itself is `paseo.event.slack.trigger_message`:
| Path | Value |
| ------------ | ----------------------------------------------------- |
| `ts` | Native message timestamp, Slack's message id |
| `content` | Raw text, including the `<@BOT>` mention |
| `body` | Text with the mention stripped, usually what you want |
| `author` | Author identity, including the user id |
| `channel` | Channel identity |
| `thread` | Thread identity, or `null` for a root message |
| `created_at` | Derived from the message `ts` |
Use `body`, not `content`, unless you specifically need the mention text.
## Example
```yaml
triggers:
- name: slack-mention
on: slack.mention
environment: dev
filters:
workspace: T01234567
channels: [C01234567]
from_users: [U01234567]
agent:
provider: codex
mode: full-access
idle_timeout: 10m
allow_outputs:
- type: slack.reply
prompt: |
${{ paseo.event.slack.trigger_message.body }}
Reply in the thread when you're done.
```
## Limits
Direct messages, slash commands, and interactive components do not produce triggers. Enterprise Grid org-wide installs are not supported.
The reply is posted in the triggering thread. A root message gets a new thread; a threaded message stays in that thread. Use the shared [workflow examples](/docs/hub/configuration/examples) for complete configurations.
+238
View File
@@ -0,0 +1,238 @@
---
title: Hub workflows
description: Learn how Hub turns a provider event into one or more ordered agent steps.
nav: Workflows
order: 64
category: Hub
---
# Hub workflows
A workflow is the work Hub performs after a trigger matches an event. You describe it in `.paseo/hub.yml`, next to the environments where your agents run.
The basic shape is small:
```text
Slack mention → Hub trigger → workflow step → Paseo daemon → agent
```
You can stop there with one step. Add deterministic inputs when the person invoking the workflow should choose a route. Add structured outputs when an agent should make a decision for a later step.
## Your first workflow
Start with one Slack trigger and one agent step:
```yaml
environments:
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/project
triggers:
- name: slack-help
on: slack.mention
max_runtime: 2h
filters:
workspace: T01234567
channels: [C01234567]
from_users: [U01234567]
steps:
- id: answer
environment: development
max_runtime: 30m
idle_timeout: 5m
agent:
provider: codex
mode: read-only
prompt:
- text: |
Help with this request:
${{ paseo.prompt }}
allow_outputs:
- type: slack.reply
```
Mention the bot in the configured channel:
```text
@Paseo how do I run the project locally?
```
Hub removes the mention and gives the agent `how do I run the project locally?` as `${{ paseo.prompt }}`. The agent can use the configured `slack.reply` capability to answer in the thread.
What happens next:
- Slack delivers the mention to Hub.
- Hub checks the trigger event and filters, including the user allowlist.
- Hub creates a workflow run and starts the `answer` step on the matching daemon.
- The agent works with the prompt and finishes through Hub.
- Project → Activity records the event, step, outcome, and any reply.
For GitHub, Discord, and manual runs, only the trigger and invocation change. The workflow steps work the same way. See [Triggers](/docs/hub/triggers) for provider matching.
## Deterministic inputs
A deterministic input is a typed value supplied by the person or system that invokes the workflow. Hub validates it before starting an agent. Use one when the caller should make an explicit choice, such as selecting a repository or agent.
Declare the input on the trigger:
```yaml
inputs:
repo:
type: string
choices: [project, paseo]
agent:
type: string
default: codex
choices: [codex, claude]
```
The caller puts declared inputs at the start of the message:
```text
@Paseo repo=project agent=claude investigate the failed sync
```
Hub stores these values under `paseo.inputs` and passes the remaining text as the prompt:
```text
paseo.inputs.repo = "project"
paseo.inputs.agent = "claude"
paseo.prompt = "investigate the failed sync"
```
Inputs can be `string`, `number`, or `boolean`. Add `required`, `default`, or `choices` when needed. A value that does not match its type or choices, a missing required value, or a duplicate input creates a rejected Activity record and starts no agent.
The first word that is not a declared input starts the prompt. This lets ordinary text remain ordinary text:
```text
@Paseo repo=project status=blocked explain the failure
```
If `status` is not declared, the prompt is `status=blocked explain the failure`. It is not treated as workflow input.
### Deterministic routing
Use `filters.inputs` when different triggers should own different choices:
```yaml
filters:
from_users: [U01234567]
inputs:
repo: project
```
Create a second trigger with `repo: paseo` and the same input declaration. The two routes are exclusive for a supplied `repo` value. See [Repository routing](/docs/hub/configuration/examples#repository-routing) for a complete configuration.
## Structured outputs
A structured output is a JSON value an agent returns when it finishes a step. Declare its JSON Schema when a later step needs to use the decision.
This example asks one agent whether the request needs an answer or implementation, then runs only the matching step:
```yaml
steps:
- id: classify
environment: development
max_runtime: 2m
idle_timeout: 30s
agent:
provider: codex
mode: read-only
prompt:
- text: Classify the request as answer or implementation.
- text: ${{ paseo.prompt }}
output:
schema:
type: object
additionalProperties: false
required: [kind]
properties:
kind:
enum: [answer, implementation]
- id: answer
if: ${{ steps.classify.outputs.kind == 'answer' }}
environment: development
max_runtime: 10m
idle_timeout: 2m
agent:
provider: codex
mode: read-only
prompt:
- text: Answer the request without changing files.
- text: ${{ paseo.prompt }}
- id: implementation
if: ${{ steps.classify.outputs.kind == 'implementation' }}
environment: development
max_runtime: 90m
idle_timeout: 10m
agent:
provider: codex
mode: full-access
prompt:
- text: Implement the request and verify the result.
- text: ${{ paseo.prompt }}
```
The classifier calls the `finish_execution` capability with:
```text
finish_execution({ output: { kind: "implementation" } })
```
Hub validates that object against the schema. If it is invalid, the capability returns an MCP error and the same agent can correct and call it again. A valid output completes the step and makes it available as `${{ steps.classify.outputs.kind }}` to later steps.
Steps run in order. When a step's `if` condition is false, Hub skips it and evaluates the next step. In this example, only one downstream condition can be true. If the answer step runs, the workflow ends without starting the implementation step.
## Deterministic input or classifier?
Choose based on where the decision comes from:
| Use | When | Example |
| ------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Deterministic input | The caller knows the answer and should control the route. | `repo=project` selects the project environment. |
| Classifier output | The route depends on the request and the caller should not have to label it. | A read-only agent decides whether a request is an answer or implementation. |
| Both | Give experienced callers an explicit override and use classification as the fallback. | Supplied `repo` skips classification; absent `repo` runs it. |
An agent-produced value cannot grant arbitrary authority. If an output selects a provider, model, mode, or environment, the configuration must prove the possible choices are finite.
The namespaces stay separate:
- `${{ paseo.inputs.repo }}` — deterministic caller evidence.
- `${{ steps.classify.outputs.repo }}` — structured agent evidence.
- `${{ values.selected_repo }}` — a composed value.
Compose an override and fallback with `??`:
```yaml
values:
selected_repo: ${{ paseo.inputs.repo ?? steps.classify.outputs.repo }}
steps:
- id: classify
if: ${{ paseo.inputs.repo == null }}
# ...
```
When `repo` is supplied, the classifier is skipped and its output is not read. When it is absent, classification must succeed before a downstream route can run.
## Common patterns
Once the basic workflow makes sense, use these complete examples:
- [Model and provider selection](/docs/hub/configuration/examples#model-and-provider-selection)
- [Repository and project routing](/docs/hub/configuration/examples#repository-routing)
- [Safety gate and direct answer](/docs/hub/configuration/examples#safety-gate-and-direct-answer)
- [PR progress and final updates](/docs/hub/configuration/examples#pr-progress-and-final-updates)
- [Prompt partials](/docs/hub/configuration/hub-yml#prompt-partials)
- [Deadlines](/docs/hub/configuration/hub-yml#deadlines)
- [Provider invocation](/docs/hub/configuration/hub-yml#provider-invocation)
## Next
The [`hub.yml` reference](/docs/hub/configuration/hub-yml) covers every field, expression, prompt partial, reply capability, and deadline. It also documents activation errors and the exact limits for each field.
Then return to [Configuration](/docs/hub/configuration) to connect the file to a project, or [Activity](/docs/hub/activity) to inspect a run.