# Naming and Content Rules — N1 to N23

Mined from all **484 shipped connectors**: 30,726 non-unified actions, 133,406 inputs,
124,243 args. Every rule states its corpus adherence, so none of this is style preference —
it is what the connectors running in production actually do.

**These are not violated.** `scripts/gates/conventions.js` enforces them. Read this before
writing YAML; run the gate after.

---

## N1 — `actionId` is snake_case

`^[a-z][a-z0-9_]*$` — **30,713 of 30,726**. Thirteen violations exist and all are mistakes.

```yaml
actionId: list_candidates        # ✅
actionId: list_agentSessions     # ❌ linear — camelCase
actionId: get_to-do_lists        # ❌ basecamp — hyphen
```

The actionId becomes the MCP tool name customers call (`{provider}_{actionId}`). A stray
capital changes that name.

---

## N2 — Input names carry no brackets

**498 violations across 16 connectors.** This is the rule most worth internalising, because
the corpus shows both halves of it:

> Brackets are the **provider's wire syntax**. They belong in the **arg** name, never the
> **input** name.

```yaml
# ✅ 7shifts — sanitised input, literal arg
inputs:
  - name: start_gte
    type: datetime_string
    required: false
    description: Return shifts starting on or after this timestamp.
args:
  - name: start[gte]              # the provider's literal parameter
    value: $.inputs.start_gte
    in: query
    condition: '{{present(inputs.start_gte)}}'

# ❌ amplemarket — bracket leaked into the input name
inputs:
  - name: page[size]
  - name: tags[]
```

3,316 args legitimately carry brackets. That is why 7% of arg names differ from the input
feeding them — correct, not drift.

Never encode a type in the name either: no `user_ids[]`, no `_arr`, no `_str`. The `type` and
`array` fields say that already.

---

## N3 — Every input states `required`

**133,406 of 133,406 — 100%.** No exceptions anywhere in the corpus.

It defaults to `false`, so an omitted flag silently turns a mandatory parameter optional and
the failure surfaces as a provider 400 that looks like a bad value.

---

## N4 — Input `type` is one of six

**string** 77,252 · **number** 23,960 · **object** 13,062 · **boolean** 9,946 ·
**enum** 7,812 · **datetime_string** 1,362

Nothing else ships. No `integer`, no `date`, no `array` — an array is `array: true` plus a
`type`.

---

## N5 — An arg fed by an optional input carries a `condition`

**276 violations of 79,750 — 99.7% adherence.**

```yaml
- name: cursor
  value: $.inputs.cursor
  in: query
  condition: '{{present(inputs.cursor)}}'     # ✅ required whenever the input is optional
```

Without it the parameter is sent empty when the caller omits it. Many providers reject an
empty value as malformed rather than ignoring it, so the action fails for callers who simply
did not pass an optional filter.

Only bare `$.inputs.x` args need this. An arg whose value already handles absence —
`'{{inputs.limit ?? 25}}'` — must not have one.

---

## N6 — Description present, starting with a capital

**30,726 of 30,726 have a description. 100% start with a capital.**

The description is what an agent reads to decide whether to call the action. A trailing
period is *not* a convention — only 37.2% have one.

---

## N7 — `baseUrl` points at production

**Zero of 484** shipped connectors have a static baseUrl naming a sandbox, demo or staging
host. This one is unanimous.

Build production unless the customer explicitly asked for sandbox.

---

## N8 — One environment, `production`, unless sandbox is in scope

**570 of 619 auth blocks declare production alone — 92.1%.** Only 41 connectors have a test
environment at all.

```yaml
environments:
  - key: production
    name: Production
```

An unrequested sandbox is surface the customer did not ask for. If they did ask, record it
as `scope.sandbox: true` in `run.json` — then the gate permits it.

---

## N9 — Labels are Title Case, no trailing period

**94.4% Title Case.** Only 41 of 30,726 end with a period. Median length 18 characters.

```yaml
label: List Application Feedback     # ✅
label: Get accommodations            # ❌ sentence case
label: Update Item.                  # ❌ trailing period
```

Small words stay lowercase inside the label: `a an the of for to by in and or with from on
at as per via`.

---

## N10 — Sandbox is one conditional connector, never two

29 connectors serve both environments from a single baseUrl:

```yaml
baseUrl: '{{credentials.environment == "sandbox" ? "https://api.sandbox.example.com" : "https://api.example.com"}}'
```

Auth URLs branch the same way, on `credentials.environment` or `context.environment`. Never
ship a second connector for sandbox.

---

## N11 — An arg fed by a *required* input carries no condition

A required input is always present, so a condition on it is dead code. When this fires, one
of the two is wrong: either the condition is spurious or the input should be optional.

---

## N12 — Input descriptions are at least 10 characters

**99.1% adherence** — 1,220 weak descriptions in 133,406.

An agent choosing parameters sees only this text. A description that restates the name tells
it nothing:

```yaml
- name: employee_id
  description: Employee ID                                    # ❌ restates the name
  description: Identifier of the employee to fetch. Obtain
    from list_employees.                                      # ✅ says where to get one
```

Naming a source action is the corpus habit worth copying — it is how a caller resolves a
dependency without guessing.

---

## N13 — `info.version` is semver

`MAJOR.MINOR.PATCH`, digits only. A new connector is `0.1.0`.

---

## N14 — Every scope an action requires is defined

Each scope named in an action's `authRequirements` must exist in `scopeDefinitions`. A
definition no action uses is *reported*, never failed — it can sit in an `includes` chain
or document the provider's model ahead of the actions that will need it.

---

## N15 — Dynamic baseUrl credentials are declared and required

Every `${credentials.X}` interpolated into `baseUrl` must be a declared `configField` or
`setupField` with `required: true` — otherwise the host resolves to `undefined` at
runtime for any connection that omits it.

---

## N16 — Every action declares valid `effects`

Non-event actions declare `effects` from `read | search | write | delete | execute` —
all 271 shipped actions do. This becomes the MCP `readOnlyHint`/`destructiveHint` a
caller's agent trusts, and `stackone validate` does not check it; the gate is the only
guard between a missing effect and a mislabelled tool.

---

## N17 — `authRequirements`, never `requiredScopes`

`requiredScopes` is superseded and rejected in schema review. When the connector defines
`scopeDefinitions`, **every** action carries `authRequirements`, keyed by the
authentication block id it applies to (`[]` for an auth type that needs no scopes). A key
naming a block that does not exist silently grants nothing.

```yaml
authRequirements:
  oauth2: [channels:read]        # key = the auth block's id
  api_key: []                    # this auth type needs no scopes — say so
```

---

## N18 — Environments name deployment stages only

`production`, `sandbox`, `staging` — nothing else. All 18 environment entries in the
corpus are `production`. A region, data centre, or tenant here is a configField wearing
the wrong hat: `eu` beside `production` reads as an alternative stage to whoever connects
the account.

---

## N19 — Every action cites a documentation URL

Non-event actions carry `resources` with at least one provider documentation URL — all
271 shipped actions do. Validate uses it to pin the action to the page that documents it;
an endpoint no page describes cannot survive that phase.

---

## N20 — One step per action

Zero of the 271 shipped actions declare more than one step. A second step is almost
always a chained call that belongs in its own action — one action per provider operation
is what keeps coverage countable.

---

## N21 — Step URLs carry no query string

**98.75%** of the 32,148 corpus step urls are pure paths. Query parameters — *including
fixed literals* — are args with `in: query`; the runtime composes the URL:

```yaml
url: '/${inputs.container}'                # ✅ pure path
args:
  - name: restype
    value: container                       # literal value lives in the arg
    in: query
  - name: comp
    value: list
    in: query

url: '/${inputs.container}?restype=container&comp=list'   # ❌ breaks endpoint matching
```

A measured azureblobstorage run baked `?restype=container&comp=list` into four urls and
every consumer matching on method+url saw four actions that were simultaneously missing
and hallucinated.

---

## N22 — No empty-bracket suffix on arg names

`person_titles[]` is the serialization leaking into the name — write `person_titles` and
let `arrayFormat` (or the default) serialize the array. **Keyed** brackets are different:
`revenue_range[min]` is a real wire name for a subfield and stays.

---

## N23 — Every generated connector records its changelog

**Every published version must record what changed** — the initial `0.1.0` included
(`Initial release covering X.` is a real record). Missing is **blocking**:

```yaml
info:
  changelog:
    summary: What changed in this version.   # 1-2 customer-facing lines, overwritten each bump
    labels: [new-actions]   # optional; each one of: auth, downstream-change, bug-fix, new-actions
```

The summary is overwritten on each bump, never appended — history lives in version
control, not in this field. A stub or over-length summary, or a label outside the
vocabulary, is major. V10 re-checks the same contract at validate as the backstop.

---

## Not rules — measured and rejected

Do not invent gates from these; the corpus does not support them:

| Candidate | Reality |
|---|---|
| Closed verb vocabulary for actionId | **572 distinct prefixes**; 77 needed to cover 95% |
| Verb must match HTTP method | 95.3% — POST-based list and search APIs are normal |
| `list_` must name a plural | 92.9% — `list_time_off` is correct English |
| Array inputs must have plural names | 76.5% — too noisy |
| Descriptions end with a period | 37.2% — not a convention |
| actionId contains the provider name | **0.3%** — it does not |
