# Official Schema vs Shipped Corpus

Two sources describe the connector YAML, and **they disagree**:

- The published reference — <https://docs.stackone.com/guides/connector-engine/connector-yaml-reference>
  and <https://docs.stackone.com/guides/connector-engine/expression-language>
- The 481 shipped connectors in `connectors/src/configs`

The docs describe a **superset**. Several documented features have **zero adoption across all
30,784 shipped actions**, and in five cases the docs and the corpus outright disagree — four shapes the corpus never
uses, plus one restriction it routinely ignores.

**Rule: follow the corpus where it disagrees.** Shipped connectors are the proof that a shape
works. Treat a zero-adoption doc feature as unverified — it may be unsupported, deprecated, or
simply never needed.

---

## Five outright conflicts — use the corpus form

### 1. Enum values

```yaml
# ✅ CORPUS — 14,849 uses
oneOf:
  values:
    - active
    - hired

# ❌ DOCS — 0 uses anywhere
values:
  - key: active
    label: Active
    description: ...
```

### 2. Header location

```yaml
in: headers      # ✅ corpus — 636 inputs, 22,121 args
in: header       # ❌ docs — 0 uses
```

### 3. `requiredScopes`

Docs call it an array. All 12,387 uses are a **bare string**:

```yaml
requiredScopes: root_readonly
```

Prefer `authRequirements` regardless — it is the modern form and a map keyed by auth-block id.

### 4. Pagination

The docs describe a `pagination` object inside `paginated_request`:

```yaml
# ❌ DOCS — 0 uses in the corpus
pagination:
  type: cursor
  request:
    cursor_field: cursor
  response:
    cursor_path: $.paging.next
    data_path: $.data
```

Every shipped connector uses `response` + `iterator` directly on the step instead — but
**only under `functionName: paginated_request`**. The current CLI schema is strict: on a
plain `request`, `response` accepts only `collection`/`indexField`/`dataKey`, and
`nextKey`/`iterator` fail `stackone validate` with a misleading "Unknown field" error
(measured twice). When you page, switch the step's `functionName`:

```yaml
# ✅ CORPUS shape, on functionName: paginated_request
response:
  dataKey: results
  nextKey: '{{urlParam($._links.next, "cursor")}}'
iterator:
  key: cursor
  in: query
  mode: cursor
```

`paginated_request` itself is used 117 times against `request` 29,328 — because most list
endpoints return one page, not because plain `request` can carry the pagination keys.

### 5. `${…}` is not "interpolation only"

The docs describe `${…}` as pure substitution — no operators, no conditionals, no function
calls — and say that needing logic forces the whole value into JEXL. **35 connectors say
otherwise**: 867 interpolations contain an operator and 176 call a function.

```yaml
# ✅ all shipped and working
url: /v2/companies/${inputs.company_id || credentials.company_id}
url: /accounts/${inputs.account_id ?? credentials.accountId}/ai-gateway/gateways
value: Token ${encodeBase64(credentials.apiKey + ":" + credentials.apiSecret, "base64")}
```

Functions seen inside `${…}`: `replace` 96 · `regexMatch` 52 · `encodeBase64` 16 ·
`present` 12.

The `??` fallback to a credential is the standard way to make an input optional when the
connection already carries a sensible default — the user passes `account_id` only when they
want to override the connected account.

**Still prefer JEXL for anything branching.** A ternary or a multi-clause condition reads
better as `'{{…}}'`, and that is what the corpus does for those. But an operator inside
`${…}` is not an error, and a build loop must not flag it as one.

---

## Documented but unused — do not reach for these

| Feature | Corpus uses |
|---|---|
| `default` on an input | 0 |
| `annotations` (MCP hints: idempotentHint, readOnlyHint, …) | 0 |
| `errorHandler` on a step | 0 |
| `result.rawRequest` / `result.rawResponse` | 0 |
| `group_data` step function | 0 |
| `merge_collections` step function | 0 |
| `code_execution_lambda` step function | 0 |
| field `type: url` | 0 |

`code_execution_lambda` appears in some skills' `features:` frontmatter, so the runtime may
support it — but nothing ships with it. If you believe you need one of these, prefer an
approach the corpus demonstrates.

---

## Real features the corpus uses that examples alone would not reveal

### Step-level `condition` — 632 uses, 391 distinct

A whole step can be skipped, not just an arg:

```yaml
steps:
  - stepId: emit
    condition: '{{present(inputs.payload.message.events)}}'
    stepFunction:
      functionName: emit_event
      parameters: {...}
```

Common forms:

```yaml
condition: '{{present(inputs.payload.message.events)}}'    # payload shape varies
condition: '{{inputs.resource_type == "group"}}'           # branch on an input
condition: '{{steps.check_request_status.successful}}'     # only if a prior step succeeded
```

`steps.<id>.successful` is how you branch on an earlier step's outcome — useful for a
poll-then-fetch chain where the fetch should not run if the poll failed.

### `applicableScopes` and `displayScopes` in guides — 156 uses

Guide sections and steps can be filtered by the scopes the user selected:

```yaml
sections:
  - title: Configure permissions
    content: |
      In the **Permissions** section, select the permissions for your use case:
    displayScopes: true                 # render action badges for the enabled scopes
    applicableScopes: >-                # space-separated; section hidden unless one is selected
      users:read users:write courses:read
```

This is how one guide serves several scope combinations without showing irrelevant steps.

---

## Defaults the docs state and the corpus relies on

Never stated in a shipped file, because they are defaults:

| Field | Default |
|---|---|
| input `in` | `query` |
| step `method` | `get` |
| `includeBearer` | `true` |
| input `required` | `false` |
| input `array` | `false` |
| field `secret` | `false` |

Corpus style is to write them explicitly anyway — 214,966 of 214,966 inputs state `required`.
Follow that: explicit beats inherited when an agent is reading the YAML to decide what to send.

---

## The three expression formats — names and rules

The corpus shows *what* each looks like; the docs name them and give the rules.

| Format | Syntax | Purpose |
|---|---|---|
| **JSONPath** | `$.inputs.x` | direct data access, no transformation |
| **String interpolation** | `${inputs.x}` | building URLs and strings |
| **JEXL** | `'{{expression}}'` | logic, conditionals, functions |

**Two rules that are not visible from examples:**

1. **JEXL must be single-quoted in YAML.** Unquoted `{{...}}` parses as a YAML flow mapping.
2. **Never mix formats in one field.**
   ```yaml
   value: '{{$.inputs.limit ?? 25}}'   # ✅ JEXL wrapping a JSONPath
   value: '$.inputs.limit ?? 25'       # ❌ JSONPath cannot express logic
   ```

**The docs say string interpolation has no operators, conditionals or function calls. The
corpus disagrees** — see conflict 5 above. `${…}` evaluates expressions in practice.

### Contexts available to expressions

| Context | |
|---|---|
| `$.credentials.*` | connection credentials |
| `$.inputs.*` | action inputs |
| `$.steps.{stepId}.output` | a previous step's result |
| `$.config.*` | connector configuration |
| `$.environment.key` | selected environment |
| `credentials.environment` | the selected environment's key — how one auth block serves sandbox and production |
| `$.response` | the current response, inside handlers |
| `$.iterator` | the current item, inside iteration |
| `${apiHostUri}` | StackOne callback base — use in OAuth `redirect_uri`, never hardcode |

`$.config`, `$.environment`, `$.response` and `$.iterator` appear in the docs but are rare or
absent in the corpus — the first two are the documented way to read connector config and the
selected environment.

---

## Full function list

The corpus uses `present` 89,072 times and everything else under 200. These all exist:

**Presence** `present(v)` · `missing(v)`

**String** `capitalize(v, mode?)` · `truncate(v, maxLength, suffix?)` · `padStart(v, len, pad?)` ·
`encodeBase64(v)` · `decodeBase64(v)` · `regexMatch(v, pattern, groupIndex?)` · `replace` ·
`lower` · `upper` · `asString`

**Crypto** `sha256(v, encoding?)` · `hmacSha256(v, key, encoding?)` · `md5(v, encoding?)`

**Array** `includes(arr, v)` · `includesSome(arr, v)` · `dedupe(arr)` · `join(arr, sep?)` ·
`reduce(arr, operation, field?)` — operations: `sum`, `avg`, `count`, `min`, `max`, `concat`,
`flatten`

**Object** `keys(obj)` · `values(obj)` · `zipObject(keys, values)` · `groupBy(arr, key)`

**Date** `now()` · `yearsElapsed(start, format, end?)` · `hasPassed(date, format, yearsToAdd?)` ·
`nextAnniversary(date, format)` · `deltaFromNowMs(date, format?)` · `isoToUnix` · `unixToIso`

**URL** `urlParam(url, name)` · `webhookEventsUrl(...)`

**JEXL operators** — arithmetic `+ - * / // % ^` · comparison `== != > >= < <= in` ·
logical `! && ||` · ternary `? :` · nullish `??`

Note `//` (integer division), `^` (power) and `in` (membership) are documented but essentially
unused in the corpus.

---

## When to consult the published docs

The corpus answers "what shape works". The docs answer:

- what a field **means** and its default
- which properties are **required**
- the full function signatures, including optional arguments
- MCP-facing concerns — `annotations`, how `actionId` becomes `{provider}_{actionId}`
- fields with no shipped example

Read them for a case the corpus does not cover, then prefer a corpus-demonstrated shape if one
exists.
