# Action Configuration

> **Every action carries `tags`.** At least one, naming the business domain the action
> belongs to. They drive filtering in the StackOne UI, and 237 of the 271 shipped actions
> declare them.
>
> This is not gated. Two shipped connectors omit tags entirely, so a blocking check would
> fail correct work — but an action without them is harder for a customer to find.

> **`actionId` mirrors the provider's own naming — exactly.** If the provider calls the
> operation "people search", the action is `people_search`. If it calls it
> `conversations.list`, the action is `conversations_list`. Take the provider's operation
> name, lowercase it, and replace separators with underscores. Nothing else.
>
> Do **not** reorder it into a verb-first house style. `people_search` stays `people_search`;
> it does not become `search_people`. A connector that renames the provider's operations
> forces every caller to learn a second vocabulary for the same API, and it is the single
> largest source of scored naming deviations.
>
> This is why the endpoint `id` recorded in research matters: build creates one action per
> `endpoints[].id`, and gate B3 requires the actionId to match it exactly. Get the name right
> in research and the build inherits it.

Entry point for building non-unified (custom) actions. Non-unified actions pass through the provider's API response directly — no field mapping or StackOne schema transformation.

**Always `actionType: custom`. Always exactly one step.**

## Action Structure

```yaml
- actionId: list_users              # snake_case, matches provider endpoint name
  authRequirements:                 # per-auth-type scopes/permissions (see "When Required" below)
    oauth2:
      - users:read
  effects:                          # semantic data effects (see "effects" section below)
    - read
  label: List Users                 # Title Case, matches endpoint name
  categories:
    - ats                           # provider category (e.g. ats, iam, hris, crm)
  actionType: custom                # always 'custom'
  description: Retrieve a list of users   # 1 sentence
  details: Returns a paginated list of all users in the system. Supports limit and offset for pagination.  # 2-3 sentences
  tags:
    - Users                         # groups action by resource for searchability
  resources: PROVIDER_DOCS_URL/docs/users#list   # API doc URL for this endpoint
  inputs:
    # → read `references/input-config.md` for full reference
  steps:
    # → load step-function-request or step-function-upload-download skill
  result:
    data: $.steps.step_id.output.data
```

## Required Fields

| Field | Notes |
| :--- | :--- |
| `actionId` | snake_case — must match the endpoint name from provider |
| `label` | Title Case — must match the endpoint name from provider |
| `categories` | UI categorization — list format (e.g. `- ats`) |
| `actionType` | Always `custom` |
| `description` | 1 sentence |
| `details` | 2-3 sentences about functionality |
| `tags` | At least one tag grouping by resource (e.g. `- Users`, `- Custom Fields`) |
| `effects` | Semantic data effects — `read`, `write`, `delete`, `search`, or `execute`. See "effects" section below. |
| `steps` | Exactly one step function |
| `result` | Always `result:\n  data: $.steps.{stepId}.output.data` |

## Conditional Fields

| Field | When Required | Notes |
| :--- | :--- | :--- |
| `authRequirements` | When connector has scopeDefinitions OR multi-auth with action restrictions | Per-auth-type scopes/permissions. See "authRequirements" section below. |

## Optional Fields

| Field | Notes |
| :--- | :--- |
| `resources` | API doc URL for this endpoint — **mandatory** if available in provider docs |
| `inputs` | Request parameters — include if the endpoint accepts any parameters |

## Deprecated Fields (BLOCKED)

| Field | Status | Action Required |
| :--- | :--- | :--- |
| `requiredScopes` | **BLOCKED — will fail schema review** | Run `/migrate-to-auth-requirements <provider>` to convert to `authRequirements`. Do NOT use on new connectors. |

---

## authRequirements

Per-auth-type scope and permission declarations. Replaces the deprecated `requiredScopes` field.

### Structure

```yaml
authRequirements:
  <auth_type_key>:             # MUST match authentication block key exactly (e.g. oauth2, custom, api_key)
    - scope_or_permission_1    # list of scopes/permissions for THIS auth type
    - scope_or_permission_2
```

### When authRequirements is Required

| Condition | Required? | Reason |
|-----------|-----------|--------|
| Single auth type + no `scopeDefinitions` | **NO** | All actions implicitly work with that auth type |
| Multiple auth types + no `scopeDefinitions` + all actions work with all auth types | **NO** | No filtering needed |
| `scopeDefinitions` exist (any number of auth types) | **YES** | Scopes must be declared per auth type per action |
| Multiple auth types + some actions restricted to specific auth types | **YES** | Must express which auth type can run which action |

### Patterns

**OAuth-only scopes:**
```yaml
authRequirements:
  oauth2:
    - users:read
```

**Multi-auth with different scopes per auth type:**
```yaml
authRequirements:
  oauth2:
    - employees:read
  custom:
    - EmployeeIntegration:View
```

**Multi-auth where one auth type has no scopes:**
```yaml
authRequirements:
  custom: []                  # empty array = no scopes needed for this auth type
  oauth2:
    - https://www.googleapis.com/auth/generative-language.retriever
```

**Non-OAuth permissions (role-based):**
```yaml
authRequirements:
  custom:
    - PTOPlanIntegration:View
```

### Placement

`authRequirements` goes immediately after `actionId` (before `categories`, `actionType`, etc.):

```yaml
- actionId: list_employees
  authRequirements:            # ← here, right after actionId
    oauth2:
      - employees:read
  categories:
    - hris
  ...
```

### Rules

- Each key MUST match an authentication block key exactly
- Use empty array `[]` for auth types that need no scopes — do not omit the key
- Every scope referenced MUST exist in the connector's top-level `scopeDefinitions`
- For existing connectors using `requiredScopes`, run `/migrate-to-auth-requirements <provider>`

---

## effects

Declares the semantic data effects of an action — what the action's steps actually DO. Required on every action.

### Values

| Effect | Meaning |
|--------|---------|
| `read` | Reads data without modification (LIST, GET, download) |
| `write` | Creates or modifies data (CREATE, UPDATE, edit, upload) |
| `delete` | Removes data (DELETE, void, archive, remove) |
| `search` | Query-based read (search, find, query endpoints) |
| `execute` | Non-CRUD / RPC-style (approve, decline, cancel, send, sync, publish, trigger) |

### How to Derive effects

Analyze the **whole action** — HTTP method, URL path, description, and what the action actually does. Do NOT rely on `actionId` prefix alone.

**Step 1: Check HTTP method (initial signal)**

| Method | Initial Signal |
|--------|---------------|
| `GET` | `read` |
| `DELETE` | `delete` |
| `POST`, `PUT`, `PATCH` | Ambiguous — proceed to Step 2 |

**Step 2: For POST/PUT/PATCH — check URL path semantics (overrides method)**

| URL path contains | Effect | Example |
|-------------------|--------|---------|
| `/search`, `/find`, `/query` | `search` | `POST /contacts/search` |
| `/approve`, `/decline`, `/cancel` | `execute` | `POST /timeoff/{id}/approve` |
| `/send`, `/sync`, `/publish`, `/trigger`, `/run`, `/invoke` | `execute` | `POST /messages/send` |
| `/archive`, `/void`, `/remove`, `/revoke` | `delete` | `POST /employees/{id}/archive` |
| None of the above | `write` | `POST /users`, `PATCH /users/{id}` |

**Step 3: Cross-check with action description/details**

If the description says "Retrieve", "List", "Get", "Download" → `read`
If the description says "Search", "Find", "Query" → `search`
If the description says "Create", "Update", "Edit", "Upload" → `write`
If the description says "Delete", "Remove", "Void", "Archive" → `delete`
If the description says "Approve", "Decline", "Cancel", "Send", "Sync", "Publish" → `execute`

**Priority:** URL path semantics > HTTP method > description. When signals conflict, URL path wins.

**Examples:**

```yaml
# GET /users → read (method = GET)
effects:
  - read

# POST /contacts/search → search (URL contains /search, overrides POST)
effects:
  - search

# POST /employees/{id}/archive → delete (URL contains /archive, overrides POST)
effects:
  - delete

# POST /timeoff/{id}/approve → execute (URL contains /approve, overrides POST)
effects:
  - execute

# PATCH /users/{id} → write (method = PATCH, no special URL path)
effects:
  - write

# DELETE /files/{id} → delete (method = DELETE)
effects:
  - delete

# POST /messages/send → execute (URL contains /send)
effects:
  - execute

# POST /users → write (method = POST, no special URL path, creates data)
effects:
  - write
```

### Placement

After `authRequirements` (if present), before `categories`:

```yaml
- actionId: get_folder
  authRequirements:
    oauth2:
      - data:read
  effects:                # ← after authRequirements, before categories
    - read
  categories:
    - data management
```

Without `authRequirements`:

```yaml
- actionId: list_projects
  effects:                # ← right after actionId
    - read
  categories:
    - database
```

### Existing Connectors

If an existing connector's actions are missing `effects`, auto-add them during any build or test workflow. Analyze each action's HTTP method, URL path, and description to derive the correct effect. This is a universal backfill per the override policy.

## Result Pattern

Always this exact format — `stepId` must match the `stepId` in the step:

```yaml
result:
  data: $.steps.fetch_users.output.data
```

## Skills to Load for Details

An action has two main parts — **inputs** and **steps**. Args live inside the step and wire input values to the provider.

| Step | What it covers | Skill to load |
| :--- | :--- | :--- |
| 1. Define inputs | What inputs the action accepts from the caller (types, enum, object, array, variants) | `input-config` |
| 2. Build the step function | The HTTP call sent to the provider | `step-function-request` for REST calls; `step-function-upload-download` for file operations |
| 3. Wire inputs into args | Args live inside the step and pass each input value to the provider (query, body, headers) | `args-config` |

### Choosing the Right Step Function for File Actions

**Load `step-function-upload-download` when:**
- Provider accepts the file via `multipart/form-data`
- Provider returns a binary file response (e.g. `application/octet-stream`, `application/pdf`, `text/plain`, or any other file content type)

**Load `step-function-request` when:**
- Endpoint is a normal REST API with JSON request/response
- Provider accepts the file as a base64 string in the request body
- Provider accepts file content as any normal body or query parameter


## Complete Examples

### List Action

```yaml
- actionId: list_users
  authRequirements:
    oauth2:
      - users:read
  effects:
    - read
  label: List Users
  categories:
    - ats
  actionType: custom
  description: Retrieve a list of users
  details: Returns a paginated list of all users in the system. Supports limit and offset parameters for pagination. Results can be filtered by status.
  tags:
    - Users
  resources: PROVIDER_DOCS_URL/docs/users#list
  inputs:
    - name: limit
      description: Maximum number of users to retrieve
      type: number
      in: query
      required: false
    - name: offset
      description: Number of users to skip
      type: number
      in: query
      required: false
  steps:
    - stepId: fetch_users
      description: Retrieve users from the API
      stepFunction:
        functionName: request
        parameters:
          url: /users
          method: get
          args:
            - name: limit
              value: $.inputs.limit
              in: query
              condition: '{{present(inputs.limit)}}'
            - name: offset
              value: $.inputs.offset
              in: query
              condition: '{{present(inputs.offset)}}'
  result:
    data: $.steps.fetch_users.output.data
```

### Get Action

```yaml
- actionId: get_user
  authRequirements:
    oauth2:
      - users:read
  effects:
    - read
  label: Get User
  categories:
    - ats
  actionType: custom
  description: Retrieve a single user by ID
  details: Get detailed information about a specific user including profile, permissions, and activity data. Returns the full user object.
  tags:
    - Users
  resources: PROVIDER_DOCS_URL/docs/users#get
  inputs:
    - name: userId
      description: Unique identifier for the user
      type: string
      in: path
      required: true
  steps:
    - stepId: get_user
      description: Fetch user by ID
      stepFunction:
        functionName: request
        parameters:
          url: /users/${inputs.userId}
          method: get
  result:
    data: $.steps.get_user.output.data
```

### Create Action

```yaml
- actionId: create_user
  authRequirements:
    oauth2:
      - users:write
  effects:
    - write
  label: Create User
  categories:
    - ats
  actionType: custom
  description: Create a new user
  details: Creates a new user in the system with the provided details. Returns the created user object with its assigned ID and timestamps.
  tags:
    - Users
  resources: PROVIDER_DOCS_URL/docs/users#create
  inputs:
    - name: name
      description: Full name of the user
      type: string
      in: body
      required: true
    - name: role
      description: User role
      type: enum
      in: body
      required: false
      oneOf:
        values:
          - admin
          - member
          - guest
    - name: address
      description: User's address information
      type: object
      in: body
      required: false
      properties:
        - name: street
          description: Street address
          type: string
          required: false
        - name: city
          description: City name
          type: string
          required: false
        - name: country
          description: Country code
          type: string
          required: false
  steps:
    - stepId: create_user
      description: Create a new user
      stepFunction:
        functionName: request
        parameters:
          url: /users
          method: post
          args:
            - name: name
              value: $.inputs.name
              in: body
            - name: role
              value: $.inputs.role
              in: body
              condition: '{{present(inputs.role)}}'
            - name: address      # top-level object — nested properties are included automatically
              value: $.inputs.address
              in: body
              condition: '{{present(inputs.address)}}'
  result:
    data: $.steps.create_user.output.data
```

### Update Action

```yaml
- actionId: update_user
  authRequirements:
    oauth2:
      - users:write
  effects:
    - write
  label: Update User
  categories:
    - ats
  actionType: custom
  description: Update an existing user by ID
  details: Updates the specified fields of an existing user. Only provided fields will be updated; omitted fields remain unchanged.
  tags:
    - Users
  resources: PROVIDER_DOCS_URL/docs/users#update
  inputs:
    - name: userId
      description: Unique identifier for the user
      type: string
      in: path
      required: true
    - name: name
      description: Updated full name of the user
      type: string
      in: body
      required: false
    - name: address
      description: Updated address information
      type: object
      in: body
      required: false
      properties:
        - name: street
          description: Street address
          type: string
          required: false
        - name: city
          description: City name
          type: string
          required: false
        - name: country
          description: Country code
          type: string
          required: false
  steps:
    - stepId: update_user
      description: Update user by ID
      stepFunction:
        functionName: request
        parameters:
          url: /users/${inputs.userId}
          method: patch
          args:
            - name: name
              value: $.inputs.name
              in: body
              condition: '{{present(inputs.name)}}'
            - name: address      # top-level object — nested properties are included automatically
              value: $.inputs.address
              in: body
              condition: '{{present(inputs.address)}}'
  result:
    data: $.steps.update_user.output.data
```

### Delete Action

```yaml
- actionId: delete_user
  authRequirements:
    oauth2:
      - users:write
  effects:
    - delete
  label: Delete User
  categories:
    - ats
  actionType: custom
  description: Delete a user by ID
  details: Permanently deletes the specified user from the system. This action cannot be undone and will remove all associated data.
  tags:
    - Users
  resources: PROVIDER_DOCS_URL/docs/users#delete
  inputs:
    - name: userId
      description: Unique identifier for the user to delete
      type: string
      in: path
      required: true
  steps:
    - stepId: delete_user
      description: Delete user by ID
      stepFunction:
        functionName: request
        parameters:
          url: /users/${inputs.userId}
          method: delete
  result:
    data: $.steps.delete_user.output.data
```

## Critical Rules

### MUST DO:
- ✅ Always use `actionType: custom`
- ✅ Always include `tags` with at least one entry
- ✅ Exactly one step per action
- ✅ `stepId` in result must match `stepId` in steps
- ✅ Required args have no condition; optional args always have `condition: '{{present(inputs.field)}}'`
- ✅ Path params go in URL via `${inputs.field}` — never in args
- ✅ Always include `effects` — derive by analyzing HTTP method, URL path, and description (see effects section)
- ✅ Use `authRequirements` when connector has `scopeDefinitions` or multi-auth with action restrictions (see "When authRequirements is Required" table)
- ✅ Include `resources` URL whenever available

### MUST NOT:
- ❌ Use `requiredScopes` — it is **BLOCKED**. Run `/migrate-to-auth-requirements <provider>` to convert existing connectors
- ❌ Use `fieldConfigs`, `entrypointUrl`, `entrypointHttpMethod`, or `schema`
- ❌ Add more than one step per action
- ❌ Use `type: array` — always use `array: true` instead
- ❌ Create inputs that don't exist in the provider's actual API
- ❌ Skip required fields
