diff --git a/public-docs/hub/api.md b/public-docs/hub/api.md index 4539ede35..3f55be735 100644 --- a/public-docs/hub/api.md +++ b/public-docs/hub/api.md @@ -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 diff --git a/public-docs/hub/concepts.md b/public-docs/hub/concepts.md index 4bb5bc670..725771073 100644 --- a/public-docs/hub/concepts.md +++ b/public-docs/hub/concepts.md @@ -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: ` 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. diff --git a/public-docs/hub/configuration/examples.md b/public-docs/hub/configuration/examples.md index 795825a7b..b5323da18 100644 --- a/public-docs/hub/configuration/examples.md +++ b/public-docs/hub/configuration/examples.md @@ -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. diff --git a/public-docs/hub/configuration/hub-yml.md b/public-docs/hub/configuration/hub-yml.md index 6af336983..f8b1eaad3 100644 --- a/public-docs/hub/configuration/hub-yml.md +++ b/public-docs/hub/configuration/hub-yml.md @@ -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.. }}` 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..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: }` 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. diff --git a/public-docs/hub/configuration/index.md b/public-docs/hub/configuration/index.md index 000972c03..e04b85dd9 100644 --- a/public-docs/hub/configuration/index.md +++ b/public-docs/hub/configuration/index.md @@ -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). diff --git a/public-docs/hub/daemons.md b/public-docs/hub/daemons.md index 97880b909..0e3b6c60d 100644 --- a/public-docs/hub/daemons.md +++ b/public-docs/hub/daemons.md @@ -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 ``` diff --git a/public-docs/hub/index.md b/public-docs/hub/index.md index 16a85259b..908ef32eb 100644 --- a/public-docs/hub/index.md +++ b/public-docs/hub/index.md @@ -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. diff --git a/public-docs/hub/quickstart.md b/public-docs/hub/quickstart.md index ef54aaa04..05edb2be1 100644 --- a/public-docs/hub/quickstart.md +++ b/public-docs/hub/quickstart.md @@ -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. diff --git a/public-docs/hub/triggers/discord.md b/public-docs/hub/triggers/discord.md index 75549aae1..35a822ed7 100644 --- a/public-docs/hub/triggers/discord.md +++ b/public-docs/hub/triggers/discord.md @@ -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. diff --git a/public-docs/hub/triggers/github.md b/public-docs/hub/triggers/github.md index 181022e32..67821405b 100644 --- a/public-docs/hub/triggers/github.md +++ b/public-docs/hub/triggers/github.md @@ -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. diff --git a/public-docs/hub/triggers/index.md b/public-docs/hub/triggers/index.md index e3010d967..1b010816a 100644 --- a/public-docs/hub/triggers/index.md +++ b/public-docs/hub/triggers/index.md @@ -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`. diff --git a/public-docs/hub/triggers/slack.md b/public-docs/hub/triggers/slack.md index 70ad7acba..c189c5b92 100644 --- a/public-docs/hub/triggers/slack.md +++ b/public-docs/hub/triggers/slack.md @@ -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. diff --git a/public-docs/hub/workflows.md b/public-docs/hub/workflows.md new file mode 100644 index 000000000..bb7d01b0d --- /dev/null +++ b/public-docs/hub/workflows.md @@ -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.