# Riders Pulse AI Workflow Patch

This document is stored in `integration_docs` and can be given to an LLM by URL:

```text
https://pulse.riders.agency/api/v1/docs/integrations/workflow-ai-patch-contract.md
```

The model must return a small patch for the current draft workflow. It must not return the full workflow context.

## Core Model

A workflow is a directed graph:

- `actions` are graph nodes: triggers, integrations, logic, control steps, and future internal actions.
- `connections` are directed edges between actions.
- `test_input_json` is the shared input payload for test runs.
- `variable_contract` describes template variables.
- `action_catalog.supported_now` contains action types that can be imported and executed now.
- `action_catalog.planned_extension_points` contains architecture targets that are not importable until runtime support lands.

The UI exports `riders-pulse-workflow-context-v1`. The LLM must return only `riders-pulse-workflow-patch-v1`.

The importer is tolerant to formatting: minified JSON, pretty JSON, fenced JSON, a single operation object, an array of operations, or an answer with extra text around the first valid JSON object. The best answer is clean JSON only.

## Integration Docs Search

Integrations are selected by intent, not by pasting a huge catalog into the prompt.

Search:

```text
https://pulse.riders.agency/api/v1/docs/integrations/search?q=<intent>&limit=5
```

Raw markdown for a selected integration:

```text
https://pulse.riders.agency/api/v1/docs/integrations/<public_slug>.md
```

Rules for the LLM:

- Infer the user intent first.
- Read this main contract and only the relevant integration docs listed in `relevant_integration_docs`.
- Use `available_integrations` from search results; do not invent integration UUIDs.
- If a required project integration is missing, leave the dependency id empty and mention the requirement in `change_description`.

## Prompt Template

```text
You edit a Riders Pulse workflow.

Read the contract:
https://pulse.riders.agency/api/v1/docs/integrations/workflow-ai-patch-contract.md

Use only relevant integration docs from relevant_integration_docs.
Below is context JSON with format riders-pulse-workflow-context-v1.

Task: <describe the change>

Return only valid JSON riders-pulse-workflow-patch-v1.
Do not return markdown.
Do not return the full context or full workflow.
Return minimal patch operations.
Fill change_description with a short summary; an empty string is allowed.
Keep action_key stable when an existing action keeps the same role.
Do not invent integration UUIDs or secrets.
Use only action_type_key values from action_catalog.supported_now for importable actions.
Use planned_extension_points only as notes, not as importable actions.
```

## Patch JSON

```json
{
  "format": "riders-pulse-workflow-patch-v1",
  "schema_version": 1,
  "base_workflow_id": "uuid-from-context",
  "change_description": "Short summary. Empty string is allowed.",
  "operations": []
}
```

Rules:

- `base_workflow_id` must match `workflow.workflow_id` from the context.
- `change_description` is copied into the next workflow version description when the user saves/publishes.
- `operations` are applied in order to the current draft.
- Patch must be minimal. Do not include unchanged actions just to mirror the whole graph.
- Do not return `riders-pulse-workflow-context-v1`.

## Operations

### `update_action`

Update an existing action label, type, or a partial `config`.

```json
{
  "op": "update_action",
  "action_key": "send_email_to_ceo",
  "set": {
    "label": "Send email to CEO",
    "config": {
      "subject": "Order {{ orders_paid.id }}",
      "body": "Total: {{ orders_paid.total }}\nRun: {{ system.link }}"
    }
  }
}
```

`set.config` is merged over the current config. Omit unchanged fields.

### `add_action`

```json
{
  "op": "add_action",
  "action": {
    "action_key": "send_telegram_to_ops",
    "label": "Send Telegram to ops",
    "action_type_key": "telegram.send_message",
    "config": {
      "action_dependency_id": "existing-telegram-integration-uuid",
      "chat_id": "{{ orders_paid.telegram_chat_id }}",
      "text": "Order {{ orders_paid.id }}"
    },
    "ui": {
      "position": { "x": 720, "y": 260 }
    }
  }
}
```

### `upsert_action`

Add an action or update it if `action_key` already exists.

```json
{
  "op": "upsert_action",
  "action": {
    "action_key": "check_total",
    "label": "Check total",
    "action_type_key": "condition.branch",
    "config": {
      "text": "{{ orders_paid.total }} > 10000"
    }
  }
}
```

### `delete_action`

Delete an action from the draft. Historical workflow runs are not changed.

```json
{
  "op": "delete_action",
  "action_key": "old_notification",
  "delete_connections": true
}
```

### `add_connection`

```json
{
  "op": "add_connection",
  "from": "trigger_1",
  "to": "send_email_to_ceo",
  "branch": ""
}
```

For `condition.branch`, use `branch: "true"` or `branch: "false"`.

### `delete_connection`

```json
{
  "op": "delete_connection",
  "from": "trigger_1",
  "to": "old_notification",
  "branch": ""
}
```

If `branch` is omitted, all connections between `from` and `to` are removed.

### `replace_connections`

Use only when rebuilding the graph.

```json
{
  "op": "replace_connections",
  "connections": [
    { "from": "trigger_1", "to": "check_total", "branch": "" },
    { "from": "check_total", "to": "send_email_to_ceo", "branch": "true" },
    { "from": "check_total", "to": "stop_low_value", "branch": "false" }
  ]
}
```

### `set_test_input`

```json
{
  "op": "set_test_input",
  "test_input_json": {
    "id": "1001",
    "total": 12500
  }
}
```

### `set_change_description`

Alternative way to set the version description. If both top-level `change_description` and this operation exist, the last applied value wins.

```json
{
  "op": "set_change_description",
  "description": "Added total check and Telegram notification"
}
```

## Action Object

```json
{
  "action_key": "send_email_to_ceo",
  "label": "Send email to CEO",
  "action_type_key": "email.send",
  "category": "Notifications",
  "type_label": "Send Email",
  "config": {},
  "ui": {
    "position": { "x": 440, "y": 320 }
  }
}
```

Rules:

- Keep `action_key` if the action still has the same meaning.
- New `action_key` must be stable snake_case or match the local convention.
- `label` is user-facing and can be renamed freely.
- `action_type_key` must exist in `action_catalog.supported_now`.
- `config` must match the action type.
- `ui.position` is optional; the importer auto-places actions when it is missing.

## Connections

```json
{
  "from": "trigger_1",
  "to": "send_email_to_ceo",
  "branch": "",
  "description": "Default route"
}
```

Rules:

- `from` and `to` must reference existing `actions[].action_key`.
- `branch` is empty for normal flow.
- For `condition.branch`, use `branch: "true"` or `branch: "false"`.
- One action can fan out to multiple downstream actions.
- Multiple upstream actions can point to the same downstream action.

## Variables

Webhook payload variables use the webhook `endpoint_key` namespace:

```text
{{ orders_paid.id }}
{{ orders_paid.total }}
{{ orders_paid.customer.email }}
```

Do not use hardcoded aliases like `{{ input.order.id }}`.

Schedule variables:

```text
{{ daily_sync.scheduled_at }}
{{ schedule.scheduled_at }}
```

Telegram relay variables:

```text
{{ telegram_relay.command }}
{{ telegram_relay.message.text }}
{{ telegram_relay.chat.id }}
{{ support_bot.message.text }}
{{ telegram.update_id }}
{{ bot_session.state_key }}
{{ bot_session.context_json }}
```

Action outputs:

```text
{{ steps.get_data.status_code }}
{{ steps.get_data.body }}
{{ steps.get_data.json.items }}
{{ steps.send_telegram.message_id }}
```

System variables:

```text
{{ system.link }}
{{ system.id }}
{{ system.run_url }}
{{ system.run_id }}
```

Prefer `system.link` and `system.id`.

## Supported Action Types

Use only these in `add_action` and `upsert_action` until more runtime support lands:

- `webhook.trigger`: starts from external HTTP webhook. Main fields: `endpoint_key`, `auth_type`, `sample_payload_json`, `json_fields`.
- `telegram.relay.trigger`: starts from Telegram Bot API updates saved by Riders Pulse relay. Main fields: `action_dependency_id`, `relay_key`, `allowed_updates`.
- `schedule.trigger`: starts periodically. Main fields: `enabled`, `schedule_key`, `interval_seconds`, `timezone`.
- `manual.trigger`: manual start.
- `http.request`: calls external endpoint. Main fields: `method`, `url`, `headers_json`, `body`, `timeout_seconds`, `retry_policy`.
- `telegram.send_message`: sends Telegram through an existing `telegram.bot` integration. Main fields: `action_dependency_id`, `chat_id`, `text`, `parse_mode`.
- `max.send_message`: sends MAX through an existing `max.bot` integration. Main fields: `action_dependency_id`, `chat_id`, `text`.
- `email.send`: sends SMTP email through an existing `smtp.server` integration. Main fields: `smtp_action_dependency_id`, `to`, `cc`, `bcc`, `subject`, `body`.
- `condition.branch`: routes by expression in `text`; outgoing connections use `branch` true or false.
- `delay.wait`: waits before continuing; main field `seconds`.
- `stop`: stops a branch.
- `bot.set_state`: saves Telegram bot dialog state. Main fields: `state_key`, `context_json`, `merge_context`.

## Planned Extension Points

These are architecture targets. Do not place them in importable `actions` until runtime support is present:

- `ai.agent`: run internal or external AI agent with tools and structured output.
- `bot.wait_for_reply`: ask a bot/user and resume workflow on reply.
- `db.query`: read or write governed internal database records.
- `task.event_trigger`: start workflow from internal task system events.
- `task.create`: create internal tasks from workflow data.
- `workflow.call`: call another workflow as a sub-workflow.

## Validation Checklist

- `format` is exactly `riders-pulse-workflow-patch-v1`.
- `base_workflow_id` matches `workflow.workflow_id`.
- `change_description` is a short summary or empty string.
- `operations` contains only requested changes.
- `add_action` and `upsert_action` use unique stable `action_key` values.
- Every new connection references existing actions or actions added earlier in the same patch.
- Importable `action.action_type_key` values exist in `action_catalog.supported_now`.
- Existing integration UUIDs are preserved; fake UUIDs are not created.
- Webhook variables use `{{ <endpoint_key>.<field> }}`.
- Schedule variables use `{{ <schedule_key>.scheduled_at }}` or `{{ schedule.scheduled_at }}`.
- HTTP outputs use `{{ steps.<action_key>.json.<field> }}` or `{{ steps.<action_key>.body }}`.

## Future Direct Import

The same patch JSON is intended for a future internal API or MCP-style endpoint:

```http
POST /api/v1/workflows/{workflow_id}/ai-import
Content-Type: application/json
```

Server responsibilities:

- Validate patch schema and action catalog.
- Reject unsupported action types in importable actions.
- Reject dangling connections.
- Preserve existing integration IDs only if the user has access.
- Save draft only; never publish automatically.
- Apply operations transactionally.
- Return diagnostics and normalized draft JSON.

## Versioning And Rollback

When the draft is published, Riders Pulse creates a `workflow_versions` record. The optional `change_description` from the patch is copied into the version description field in the editor; the user can edit or clear it before publishing.

Rollback restores the selected version into the visual draft and switches the current workflow version to it. Old `workflow_runs` and action attempts stay linked to the exact version originally used.
