> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections

> Declare the connections a workflow acts through by key, and let the platform resolve them on whichever agent runs it

## Overview

A workflow that clones a repository, opens a pull request or posts a refund acts through a **connection** - a GitHub, Stripe or Linear account connected to the agent or to its organisation. Rather than freezing a connection id into the source, declare each connection once by **key** and reference the key. At run time the engine resolves the key against the connections of the agent the run belongs to, so one definition runs unchanged on a hand-built agent, a duplicated agent and a template install.

```typescript theme={null}
export const ticketToPr = createWorkflow({
  name: 'ticket-to-pr',
  inputSchema: z.object({ ticketId: z.string(), repo: z.string(), baseRef: z.string().default('main') }),
  connections: [{ key: 'github', integrationType: 'github', required: true }],
  workspace: {
    kind: 'git',
    repo: template('input.repo'),
    ref: template('input.baseRef'),
    credentialsRef: 'github',          // the key, not a connection id
  },
})
```

<Note>
  Declaring `connections` on `createWorkflow`, and the compile-time check described below, ship with the next lua-cli release (after 3.31.0). Run-time resolution, the `connection.resolved` event and the push-time refusal are live on the platform, and agent templates already declare keys the same way.
</Note>

## Declaring connections

`connections` on `createWorkflow` is a list of declarations:

| Field             | Type      | Notes                                                                                                                                               |
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`             | `string`  | `^[a-z][a-z0-9_-]{0,63}$`, unique within the workflow. The name `credentialsRef` and `requiredConnections` use.                                     |
| `integrationType` | `string`  | The integration the key stands for - `'github'`, `'stripe'`, `'linear'`, ... Only connections of this type are candidates when the key is resolved. |
| `required`        | `boolean` | Documents that the workflow cannot run without it.                                                                                                  |
| `description`     | `string`  | What the connection is used for; shown to whoever connects the account.                                                                             |

The declarations ride with every pushed version of the workflow and are not part of the graph hash, so adding one does not change the workflow's topology.

## Using a key

A declared key can stand anywhere a connection id is accepted:

| Where                                               | What the key does                                                                                                                                   |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workspace.credentialsRef`                          | Names the git-host connection that mints the checkout token for Job-tier steps. See [Job tier](/workflows/job-tier#credentials).                    |
| `requiredConnections` on a code, agent or tool step | Connections the step must be able to use. The engine checks each one before the step is dispatched.                                                 |
| `toolScope.connectionIds` on a Job-tier agent step  | Connections mounted into the coding turn as MCP servers. The bound ids are what the turn's proxy mounts; a key that does not bind closes the scope. |

The step code itself calls the integration the same way it always has - `Integrations.passthrough('stripe', ...)` names the integration, not the key.

## Example

A refund workflow that declares its Stripe connection once and posts the refund through it after an approval:

```typescript theme={null}
import { z } from 'zod';
import { createStep, createWorkflow, template, Integrations } from 'lua-cli';

const postRefund = createStep({
  id: 'postRefund',
  inputSchema: z.object({
    approved: z.boolean(),
    editedPayload: z.object({ ticketId: z.string(), amount: z.number() }).optional(),
    input: z.object({ ticketId: z.string(), amount: z.number() }),
  }),
  outputSchema: z.object({ refundId: z.string().nullable() }),
  sideEffects: 'external',
  onError: 'park',
  requiredConnections: ['stripe'],          // the declared key below
  async execute({ inputData }) {
    if (!inputData.approved) return { refundId: null };
    const r = inputData.editedPayload ?? inputData.input;
    const res = await Integrations.passthrough('stripe', {
      method: 'POST',
      path: '/v1/refunds',
      headers: { 'Idempotency-Key': `refund:${r.ticketId}` },
      body: { charge: r.ticketId, amount: r.amount },
    });
    return { refundId: res.body.id };
  },
});

export const refund = createWorkflow({
  name: 'refund-approval',
  inputSchema: z.object({ ticketId: z.string(), amount: z.number(), requesterId: z.string() }),
  outputSchema: z.object({ refundId: z.string().nullable() }),
  budget: { maxDurationSeconds: 14 * 24 * 3600 },
  connections: [{ key: 'stripe', integrationType: 'stripe', required: true }],
})
  .approval('approveRefund', {
    title: 'Refund request',
    details: template('Refund ${initData.amount} for ticket ${initData.ticketId}'),
    approver: { role: 'support-lead' },
    excludeInitiator: true,
    timeoutHours: 8,
    onTimeout: 'deny',
    editable: true,
    editablePaths: ['amount'],
    editedPayloadSchema: z.object({ ticketId: z.string(), amount: z.number().positive().max(500) }),
  })
  .then(postRefund)
  .commit();
```

Push it to any agent that has a Stripe connection and it runs. Push it to one that does not, and the `postRefund` step parks with a notice naming the key and the integration to connect (below).

## How a key is resolved

When a run first needs a key, the engine picks one connection:

1. The candidates are the connections of the declared `integrationType` that belong to the agent the run is on, plus the organisation's. A person's own connection is never a candidate - a run acts as the agent, not as a user.
2. Agent-scoped candidates win. Org-scoped ones are consulted only when the agent has none of that type.
3. One candidate is chosen. With several, the one whose account label equals the key is chosen; otherwise the step fails as ambiguous (below).

A key is resolved **once per run**, the first time any step needs it, and every later step reads the same id - a connection added mid-run does not change a run already under way. The choice is appended to the run's events:

```json theme={null}
{
  "type": "connection.resolved",
  "data": { "key": "stripe", "connectionId": "66f1c2...", "scope": "agent", "integrationType": "stripe" }
}
```

It is also stamped on the run: `lua workflows status <runId> --steps --json` returns the resolutions under `data.connections` (`key`, `connectionId`, `integrationType`, `scope`, `at`), and from the next CLI release `lua workflows status <runId>` prints them as a `Connections:` line (`stripe → 66f1c2... (stripe, agent)`).

## When resolution fails

| Situation                                                       | Step error                                                                                                                                                                                            | What to do                                                    |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| No connection of that type on the agent or its organisation     | `credentials_unresolved` - *Connect a stripe connection to this agent (or its org) for connection key 'stripe', then retry the step.*                                                                 | Connect the integration, then retry the step.                 |
| Several connections of that type and none labelled with the key | `credentials_unresolved`, listing the candidates - *Connection key 'stripe' matches several stripe connections on this agent - label the one to use 'stripe' (candidates: ...), then retry the step.* | Set one connection's label to the key, then retry.            |
| The connection resolved but cannot mint a token or be mounted   | `credentials_revoked` (unchanged)                                                                                                                                                                     | Re-authorise the connection, then retry.                      |
| The connection roster could not be reached                      | A transport fault, retried automatically: a worker-tier step waits (`step.throttled`); a Job-tier step fails `job_spawn_failed` as transient before any container is created and is re-armed.         | Nothing - the retry resolves the key once the roster answers. |

`credentials_unresolved` is not retried automatically and no model turn is billed for it. The step's `onError` decides what the run does: with `onError: 'park'` the run waits on an exception gate, so the connect-then-`lua workflows retry-step` loop resumes it without losing completed work. The run's events also carry a `run.log` notice naming the key and the integration to connect.

<Note>
  Resolution applies only to values that are declared keys. A value that is a connection id never enters the resolver: it is mounted or minted exactly as before, and a workflow with no `connections` block behaves exactly as it did.
</Note>

## Push-time validation

`lua push workflow` refuses a `credentialsRef` or `requiredConnections` entry that looks like a key but is not declared - with or without a `connections` block - and `lua compile` refuses it the same way from the next CLI release:

```
connection-key-undeclared: workspace.credentialsRef 'github' is neither a connection id nor a declared
connections[].key — declare it: connections: [{ key: 'github', integrationType: '<catalog slug, e.g. github>' }]
and it resolves on any agent
```

The message names the path and the exact declaration to add. A malformed declaration (a key outside the grammar, such as `GitHub`) is `connection-declaration-invalid`.

**Literal ids keep working.** A connection id - a 24-character hex id, or an id with a provider prefix - is not a key: it passes the compile as before and the server verifies it at push. Only a lowercase slug that is not declared fails, which is the shape that was already broken at run time.

**Templates are unchanged.** An agent template declares `connections[].key` the same way, and an install binds each key to the installer's connection; that binding still wins. Run-time resolution is the fallback for a definition that reaches the engine with an unbound key.

## Related

* [Job tier](/workflows/job-tier#credentials) - how a `credentialsRef` becomes a short-lived token for the checkout
* [Authoring](/workflows/authoring#createworkflow-config) - the `connections` field and the `requiredConnections` step option
* [Runs and events](/workflows/runs-and-events#the-events-ledger) - where `connection.resolved` appears
* [Workflows Command](/cli/workflows-command#runs) - the `Connections:` line on `status`
