# Pipelines

**You will:** Model the path a conversation follows: stages, the data it carries, where it goes next and how it ends.

**You need:**
- A key with pipelines:write (pipelines:read to read)
- cases:write to move and close cases

A pipeline is the path a conversation, called a **case**, follows. It has **stages** in order, **fields** that hold the data of the case, **routing rules** that send a case to another pipeline, **settings** such as business hours and automatic close, and **quick replies** your team sends from the inbox. A case lives in one stage at a time, and the agent linked to that stage works it.

The examples use this client:

```ts
import { Lelemon } from '@lelemon-ai/sdk';

const lelemon = new Lelemon({ apiKey: process.env.LELEMON_API_KEY });
```

## Create a pipeline

Send `stages` to create them in the same call, in order. The entry stage is the one marked `entry`, or the first. Link an agent to a stage with `agentId`. The pipeline is created `active`.

```bash title="cURL"
curl https://api.lelemon.cl/v2/pipelines \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: sales-pipeline-1" \
  --json '{
    "name": "Home visits",
    "description": "Leads who ask for a quote for a home installation.",
    "stages": [
      { "name": "New lead", "entry": true },
      { "name": "Quote sent" },
      { "name": "Visit booked" },
      { "name": "Won" },
      { "name": "Lost" }
    ],
    "quickReplies": [
      { "shortcut": "hours", "text": "We visit Monday to Saturday, from 9:00 to 18:00." }
    ]
  }'
```

```ts title="Node.js"
const pipeline = await lelemon.pipelines.create({
  name: 'Home visits',
  description: 'Leads who ask for a quote for a home installation.',
  stages: [
    { name: 'New lead', entry: true },
    { name: 'Quote sent' },
    { name: 'Visit booked' },
    { name: 'Won' },
    { name: 'Lost' },
  ],
  quickReplies: [{ shortcut: 'hours', text: 'We visit Monday to Saturday, from 9:00 to 18:00.' }],
});
const [newLead, quoteSent, visitBooked, won, lost] = pipeline.stages;
```

```json title="MCP"
{
  "tool": "pipelines.create",
  "arguments": {
    "body": {
      "name": "Home visits",
      "stages": [{ "name": "New lead", "entry": true }, { "name": "Won" }, { "name": "Lost" }]
    }
  }
}
```


Expected: `201` with `status: "active"` and `stages` holding the stage ids in order. The pipeline is this object:

**Pipeline**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `pipeline` |
| `id` | string | yes |  |
| `name` | string | yes |  |
| `description` | string or null | yes |  |
| `status` | enum | yes | `draft`, `active`, `paused`, `archived` |
| `stages` | string[] | yes | The ids of its stages, in order. |
| `quickReplies` | QuickReply[] | yes |  |
| `settings` | PipelineSettingsResponse | yes |  |
| `createdAt` | string | yes |  |
| `updatedAt` | string | yes |  |

## Stages

A stage is a step of the path. It has a `name`, a `position`, an optional `agentId` and the `requiredFields` a case needs before it can enter, and it can close the conversation, name the stage that follows and carry its own settings. List them in funnel order with `GET /v2/pipelines/{id}/stages`.

### The entry stage

Cases enter a pipeline through its entry stage, so the agent that answers new conversations belongs there. Exactly one stage is the entry. Mark another one with `entry: true` and the previous one stops being the entry. You cannot unmark the entry stage, and you cannot delete it: make another stage the entry first.

### Add, order and change stages

`position` is the place in the funnel, starting at 0. Creating or moving a stage to a position shifts the others.

```bash title="cURL"
curl https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: stage-follow-up-1" \
  --json '{ "name": "Follow-up", "position": 2 }'

curl -X PATCH https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages/$STAGE_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "name": "Quote accepted", "position": 3 }'

curl https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
const followUp = await lelemon.pipelines.stages.create(pipeline.id, { name: 'Follow-up', position: 2 });
await lelemon.pipelines.stages.update(pipeline.id, followUp.id, { name: 'Quote accepted', position: 3 });
for await (const stage of lelemon.pipelines.stages.list(pipeline.id)) {
  console.log(stage.position, stage.name, stage.entry);
}
```


`DELETE /v2/pipelines/{id}/stages/{stageId}` answers `409 resource_in_use` while cases are in the stage (move or close them first) and `409 conflict` for the entry stage.

### Stage kinds

A stage has no kind in the API. Its role comes from how you use it:

| Role | How you make it |
| --- | --- |
| Entry | `entry: true` |
| Worked by an agent | `agentId` set |
| Worked by a person | No `agentId`. Your team answers the cases here |
| Handoff | The stage you name in `config.stageId` when you grant `transfer_to_human` to an agent |
| Gate | `requiredFields` set: a case cannot enter without those fields |
| Closing | `closes: true`: a case that arrives there ends. See [Stages that close](#stages-that-close) |

### Stages that close

Mark a stage with `closes: true` and a case that arrives there ends: it is closed, and the next message from the person starts a new case. Give the stage a `closingOutcome` of up to 50 characters, such as `Resolved`, and that text is written in the case as its `outcome`. A stage with `statusType: "done"` closes too. `statusType` says what the stage means on the board: `todo`, `in_progress` or `done`; `null` leaves it without one.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages/$RESOLVED_STAGE_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "closes": true, "closingOutcome": "Resolved", "statusType": "done" }'
```

```ts title="Node.js"
await lelemon.pipelines.stages.update(pipeline.id, won!, {
  closes: true,
  closingOutcome: 'Resolved',
  statusType: 'done',
});
```


You can also close a case yourself, wherever it is, with `POST /v2/cases/{id}/close`. See [Move and close cases](#move-and-close-cases).

### The next stage and transitions

`nextStageId` is the stage that follows when no transition holds, and `transitions` lists where a case goes from this stage. Each transition has a `targetStageId`, a `priority` (lower numbers are tried first) and `conditions` that must all hold. A condition compares a `field` path with a `value` using `operator`: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `exists` or `in`. Every target must be a stage of the same pipeline, or the call answers `400 invalid_request` with `param` naming it. `transitions` replaces the whole list; `nextStageId: null` removes the next stage.

```ts
await lelemon.pipelines.stages.update(pipeline.id, quoteSent!, {
  nextStageId: visitBooked!,
  transitions: [
    {
      targetStageId: lost!,
      priority: 0,
      conditions: [{ field: 'data.budget', operator: 'lt', value: 100 }],
    },
  ],
});
```

### Settings of a stage

`config` holds the settings of a stage. Only the keys you send change, and `null` removes a key.

| Key | Use |
| --- | --- |
| `agentMode` | When the agent answers: `always`, `paused` (never) or `outside_business_hours` (only outside the business hours of the organization) |
| `followUp` | Messages for a person who went silent: `enabled`, `delays` (minutes of silence before each one), `maxAttempts`, `templateSequence` (`reengagement`, `reminder`, `last_chance`, `breakup`, `demo_reminder` or `no_show`, in order), and optionally `allowTemplates` and `maxTemplates` (1 to 5). Templates sent after the 24-hour window cost money, so `allowTemplates` is `false` unless you turn it on. It replaces the whole block |
| `maxDurationSeconds`, `minDurationSeconds` | The longest and the shortest duration of the stage, in seconds |
| `delayMinutes` | Minutes a case waits in the stage before it moves on |
| `noProgress` | A task for your team when a case stays too long in the stage: `minutes` (counted inside the business hours of the pipeline, when it has them), `title`, and optionally `description`, `fallbackAssigneeId` (the member who gets the task when the case has no assignee) and `tag` (added to the contact). It creates a reminder of type `call`, one per pass of the case through the stage, and closes it by itself when the case leaves the stage or closes. A case with no assignee and no `fallbackAssigneeId` gets no task. It replaces the whole block |
| `attentionRole` | The part the stage plays in the attention queue: `escalation` (cases here wait for the technical team and show as waiting for a reply to whoever escalated them) or `answer` (cases here came back with an answer and the customer still needs a reply; the owner of the case gets a notification when one enters). `null` removes it. Read the queue of a member with `GET /v2/case-queue` |

```ts
await lelemon.pipelines.stages.update(pipeline.id, quoteSent!, {
  config: {
    agentMode: 'outside_business_hours',
    followUp: {
      enabled: true,
      delays: [240, 1440],
      maxAttempts: 2,
      templateSequence: ['reengagement', 'breakup'],
    },
  },
});
```

A key that is not in the table answers `400 unknown_field`.

### Escalate a case to your technical team

A case that your agents cannot solve can become an issue in your Linear, and the answer of the team comes back to the case. Connect Linear once per organization, mark the stage where those cases wait, and say which stage receives the answer.

1. **Connect Linear.** `PUT /v2/integrations/linear` (`linear.connect`) with a Linear personal API key and the key or id of the team that receives the issues (and optionally a `project`). Lelemon checks them with Linear, keeps the key encrypted and never returns it. It needs the `linear:write` scope, which belongs to admins. The answer carries `webhook.url`.
2. **Register the webhook in Linear.** In Linear, Settings, API, Webhooks, create a webhook with that URL, the Issues resource and the team. Copy the signing secret Linear shows and send it with `PATCH /v2/integrations/linear` as `webhookSecret`. Until you do, Lelemon rejects every notification (`webhook.secretConfigured` says whether it is saved).
3. **Mark the stages.** Set `config.attentionRole` to `escalation` on the stage where those cases wait and to `answer` on the stage that receives the answer. They are the same marks the attention queue uses, so there is nothing else to configure.

When a case enters the `escalation` stage, Lelemon creates one issue with the contact, the pipeline, the last messages (sensitive values covered) and the link to the case, and saves the link in the case. A case that already has an open issue never gets a second one. If Linear does not answer, the escalation stays `pending` and Lelemon keeps trying every minute with growing waits. When the team completes the issue, Lelemon reads the last comment of the team, moves the case to the `answer` stage of the pipeline if it is still in the stage where it waited, and leaves the answer as an internal note. Entering the `answer` stage notifies the owner of the case as always; when the case ends elsewhere or has no owner, Lelemon notifies the owner or the admins itself. If the team cancels the issue, the case stays where it was with a note.

To escalate a case that is not in an `escalation` stage, call `POST /v2/cases/{id}/escalate-to-team` (`cases.escalateToTeam`) with an optional `answerStageId` (the `answer` stage of the pipeline by default). `GET /v2/cases/{id}/team-escalations` lists the escalations of a case with their issue, status and answer. `DELETE /v2/integrations/linear` disconnects.

```ts
await lelemon.linear.connect({ apiKey: process.env.LINEAR_API_KEY!, team: 'SUP' });
await lelemon.pipelines.stages.update(pipeline.id, quoteSent!, {
  config: { attentionRole: 'escalation' },
});
```

### Start the cases of a WhatsApp number in a stage

`whatsappPhoneNumberId` is the id of a WhatsApp number of the organization, from `GET /v2/whatsapp/phone-numbers`. A message that arrives at that number opens its case in this stage and the pipeline of the stage works it. Linking an agent to a number with `PATCH /v2/whatsapp/phone-numbers/{id}` does not let you choose: Lelemon uses the entry stage of your oldest pipeline that has no number yet, or creates a pipeline. Use `whatsappPhoneNumberId` to choose the stage yourself. A number feeds one pipeline only: linking it to a stage of another pipeline answers `409 conflict`, and moving it to another stage of the same pipeline is fine. Send `null` to release it. Disconnecting the number releases it too.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages/$ENTRY_STAGE_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "whatsappPhoneNumberId": "'"$PHONE_NUMBER_ID"'" }'
```

```ts title="Node.js"
await lelemon.pipelines.stages.update(pipeline.id, newLead!, {
  whatsappPhoneNumberId: 'pn_...',
});
```


### Link an agent to a stage

An agent works the cases of the stages it is linked to. Send `agentId` to link one and `null` to unlink.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/stages/$ENTRY_STAGE_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "agentId": "'"$AGENT_ID"'" }'
```

```ts title="Node.js"
await lelemon.pipelines.stages.update(pipeline.id, newLead!, { agentId: 'agt_...' });
await lelemon.pipelines.stages.update(pipeline.id, newLead!, { agentId: null });
```


The agent must be linked to the entry stage to be reachable; activation reports `no_stage` or `not_entry_stage` otherwise.

### Require fields before a stage

`requiredFields` lists the keys of fields a case needs before it can enter the stage. Moving a case there without them answers `422 required_fields_missing`, with `param` naming the missing field. The list replaces the previous one on every update.

```ts
await lelemon.pipelines.stages.update(pipeline.id, visitBooked!, {
  requiredFields: ['address', 'visit_date'],
});
```

## Fields

A field holds a piece of data of the case. Create the fields before the agents that fill them.

| Property | Use |
| --- | --- |
| `key` | Required. Letters, numbers and underscores, starting with a letter, up to 64. Fixed once created |
| `type` | Required. `text`, `email`, `phone`, `url`, `textarea`, `file` or `select`. Fixed once created |
| `label` | What people see. Defaults to the key |
| `required` | Whether the field must be filled |
| `source` | Who fills it: `team`, `channel`, `system` or `agent`. Default `team` |
| `instruction` | What the agent should ask or look for. Up to 1,000 characters |
| `sensitive` | Hides the value from the agent and from webhooks |
| `options` | For `select`: a list of `{ value, label }`. The label defaults to the value. Up to 80 characters each |
| `dependsOn` | The key of another `select`. The options of this field then depend on the value of that one |

A `select` can depend on another `select`. Give each option `whenParent`: the values of the parent for which the option is offered.

```bash title="cURL"
curl https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/fields \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: field-service-1" \
  --json '{
    "key": "service",
    "type": "select",
    "label": "Service",
    "required": true,
    "source": "agent",
    "instruction": "Ask which installation the customer needs.",
    "options": [
      { "value": "solar", "label": "Solar panels" },
      { "value": "heating", "label": "Heating" }
    ]
  }'

curl https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/fields \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: field-model-1" \
  --json '{
    "key": "system_size",
    "type": "select",
    "label": "System size",
    "source": "agent",
    "dependsOn": "service",
    "options": [
      { "value": "3kw", "label": "3 kW", "whenParent": ["solar"] },
      { "value": "5kw", "label": "5 kW", "whenParent": ["solar"] },
      { "value": "wall", "label": "Wall unit", "whenParent": ["heating"] }
    ]
  }'
```

```ts title="Node.js"
const service = await lelemon.pipelines.fields.create(pipeline.id, {
  key: 'service',
  type: 'select',
  label: 'Service',
  required: true,
  source: 'agent',
  instruction: 'Ask which installation the customer needs.',
  options: [
    { value: 'solar', label: 'Solar panels' },
    { value: 'heating', label: 'Heating' },
  ],
});
await lelemon.pipelines.fields.create(pipeline.id, {
  key: 'system_size',
  type: 'select',
  label: 'System size',
  source: 'agent',
  dependsOn: 'service',
  options: [
    { value: '3kw', label: '3 kW', whenParent: ['solar'] },
    { value: '5kw', label: '5 kW', whenParent: ['solar'] },
    { value: 'wall', label: 'Wall unit', whenParent: ['heating'] },
  ],
});
await lelemon.pipelines.fields.create(pipeline.id, {
  key: 'national_id',
  type: 'text',
  label: 'National ID',
  source: 'team',
  sensitive: true,
});
```


Expected: `201` with the field. A key that breaks the format answers `400 invalid_request` with `param: "key"`.

- **Who fills it.** The agent fills fields with `source: "agent"` through its `save_case_data` tool, which refuses `sensitive` fields. Your team or your system fills the others, with `PATCH /v2/cases/{id}` and `{ "fields": { ... } }`.
- **Update.** `PATCH /v2/pipelines/{id}/fields/{fieldId}` changes the label, `required`, `source`, `instruction`, `sensitive` and `dependsOn`. `options` replaces the list and cases keep the values they already have.
- **Delete.** `DELETE /v2/pipelines/{id}/fields/{fieldId}` answers `409 resource_in_use` while another field depends on it. The field stops being required in the stages that asked for it.

```ts
const fields = await lelemon.pipelines.fields.list(pipeline.id);
await lelemon.pipelines.fields.update(pipeline.id, service.id, { label: 'Installation' });
await lelemon.cases.update('cse_...', { fields: { service: 'solar', system_size: '5kw' } });
```

## Routing rules

A routing rule sends a case to another pipeline when it reaches a stage. With `mode: "create"` a new case opens in the other pipeline and the first one stays. With `mode: "move"` the case itself moves. `toStageId` is the stage of the destination; leave it out for its entry stage.

```bash title="cURL"
curl https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID/routing-rules \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: rule-won-1" \
  --json '{ "fromStageId": "'"$WON_STAGE_ID"'", "toPipelineId": "'"$ONBOARDING_PIPELINE_ID"'", "mode": "create" }'
```

```ts title="Node.js"
const rule = await lelemon.pipelines.routingRules.create(pipeline.id, {
  fromStageId: won!,
  toPipelineId: 'ppl_...',
  mode: 'create',
});
await lelemon.pipelines.routingRules.update(pipeline.id, rule.id, { enabled: false });
```


`GET /v2/pipelines/{id}/routing-rules` lists the rules in the order they are evaluated. `PATCH` changes `toStageId`, `mode` and `enabled`; it answers `422` when the values do not form a valid rule. `DELETE` removes a rule: cases already routed stay where they are, and new cases stop matching it.

## Pipeline settings

`settings` goes in `POST /v2/pipelines` and `PATCH /v2/pipelines/{id}`, and every read of a pipeline returns it with all its keys, `null` when a setting is not set. Only the settings you send change. `autoClose`, `businessHours` and `waitThresholds` replace their whole block, and `null` removes a setting.

| Setting | Use |
| --- | --- |
| `timezone` | IANA time zone of the scheduled work of the pipeline, such as `America/Santiago` |
| `askFor` | How the agent asks for the fields it fills: `one_at_a_time` as the conversation goes (the default) or `in_order` following the order of the fields |
| `autoClose` | Closes the cases that go silent: `enabled`, `inactivityMinutes` (5 to 525,600), an optional farewell `message`, `closeStageId`, `includeTaken` to close also the chats a person took over, and `reopenWindowHours` (0 to 720, 24 by default) |
| `businessHours` | When the pipeline attends: an optional `timezone`, `days` with `mon` to `sun` (each a list of `{ from, to }` as `HH:MM`, where `24:00` ends the day) and `holidays`, each a `date` with optional `ranges` that replace the ones of that weekday |
| `waitThresholds` | The minutes without an answer after which the wait of a case turns yellow (`yellowMinutes`, 10 by default) and red (`redMinutes`, 30 by default) |
| `stageOnTakeControl` | The stage a case moves to when a person takes control |
| `stageOnHandoff` | The stage a case moves to when the agent hands it to a person |

Without `businessHours` the pipeline attends all day and the minutes are counted by the clock. With them, the wait colors, `autoClose` and the warning before it count only the minutes inside the hours. A day that is not in `days` is closed; without `days` the pipeline attends 24/7. The ranges of a day cannot overlap.

A farewell `message` needs `inactivityMinutes` under 1,440, because after 24 hours the WhatsApp window is closed and the message cannot go out.

`closeStageId`, `stageOnTakeControl` and `stageOnHandoff` take the id of a stage of the pipeline, so they cannot be set on creation: create the pipeline with its stages and then send them with `PATCH`. Deleting a stage that a setting names sets that setting to `null`. The dashboard shows and changes the same settings.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/pipelines/$PIPELINE_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{
    "settings": {
      "timezone": "America/Santiago",
      "autoClose": { "enabled": true, "inactivityMinutes": 120, "message": "We close this chat for now. Write again when you want.", "closeStageId": "'"$LOST_STAGE_ID"'" },
      "businessHours": {
        "days": {
          "mon": [{ "from": "09:00", "to": "18:00" }],
          "tue": [{ "from": "09:00", "to": "18:00" }],
          "sat": [{ "from": "10:00", "to": "14:00" }]
        },
        "holidays": [{ "date": "2026-12-25" }]
      },
      "waitThresholds": { "yellowMinutes": 10, "redMinutes": 30 },
      "stageOnTakeControl": "'"$FOLLOW_UP_STAGE_ID"'"
    }
  }'
```

```ts title="Node.js"
await lelemon.pipelines.update(pipeline.id, {
  settings: {
    timezone: 'America/Santiago',
    autoClose: {
      enabled: true,
      inactivityMinutes: 120,
      message: 'We close this chat for now. Write again when you want.',
      closeStageId: lost!,
    },
    businessHours: {
      days: {
        mon: [{ from: '09:00', to: '18:00' }],
        tue: [{ from: '09:00', to: '18:00' }],
        sat: [{ from: '10:00', to: '14:00' }],
      },
      holidays: [{ date: '2026-12-25' }],
    },
    waitThresholds: { yellowMinutes: 10, redMinutes: 30 },
    stageOnTakeControl: quoteSent!,
  },
});
```


A setting that breaks a rule answers `400 invalid_request`, and `param` names it, for example `settings.autoClose` or `settings.stageOnHandoff`.

## Quick replies

`quickReplies` is a list of `{ shortcut, text }` that your team sends from the inbox. The `shortcut` is 1 to 30 lowercase letters, numbers, hyphens or underscores, without a slash; the `text` has up to 4,096 characters. `PATCH /v2/pipelines/{id}` with `quickReplies` replaces the whole list, so send every reply you want to keep.

```ts
await lelemon.pipelines.update(pipeline.id, {
  quickReplies: [
    { shortcut: 'hours', text: 'We visit Monday to Saturday, from 9:00 to 18:00.' },
    { shortcut: 'quote', text: 'Send us the address and a photo of the place and we reply with a quote today.' },
  ],
});
```

## Move and close cases

`POST /v2/cases/{id}/move` with a `stageId` moves a case. A stage of another pipeline moves the case to that pipeline. The move answers `422 required_fields_missing` when the stage requires a field the case lacks, and `409 conflict` for a closed case.

`POST /v2/cases/{id}/close` ends the case. The contact can then have a new case in the pipeline. `POST /v2/cases/{id}/reopen` brings it back, and answers `409 conflict` when the case is open or when the contact already opened another case in the pipeline.

```bash title="cURL"
curl https://api.lelemon.cl/v2/cases/$CASE_ID/move \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "stageId": "'"$QUOTE_SENT_STAGE_ID"'" }'

curl https://api.lelemon.cl/v2/cases/$CASE_ID/close \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "outcome": "won" }'

curl -X POST https://api.lelemon.cl/v2/cases/$CASE_ID/reopen \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
await lelemon.cases.move('cse_...', { stageId: quoteSent! });
await lelemon.cases.close('cse_...', { outcome: 'won' });
await lelemon.cases.reopen('cse_...');
```


Entering a stage fires `case.stage_entered`. A contact has one open case per pipeline: opening a second one answers `409 conflict`.

## Update and delete a pipeline

`PATCH /v2/pipelines/{id}` changes the `name`, the `description`, the `status` (`draft`, `active`, `paused` or `archived`), the `quickReplies` and the [settings](#pipeline-settings). Stages, fields and routing rules have their own routes.

`DELETE /v2/pipelines/{id}` answers `204`. It answers `409 resource_in_use` while the pipeline has open cases: close or move them first. The agents linked to its stages are not deleted.

```ts
await lelemon.pipelines.update(pipeline.id, { status: 'paused' });
await lelemon.pipelines.delete(pipeline.id);
```

## Errors you will meet

| Error | Cause |
| --- | --- |
| `400 invalid_request` | A key, shortcut, setting, length or target stage breaks a rule; `param` names it |
| `400 unknown_field` | A key of `config` or `settings` that the API does not have |
| `403 scope_missing` | The key lacks `pipelines:write` or `cases:write` |
| `404 not_found` | The pipeline, stage, field or rule does not exist in this mode |
| `409 conflict` | The entry stage, a closed case, a second open case for the contact, or a WhatsApp number that already feeds another pipeline |
| `409 resource_in_use` | Delete with cases in a stage, open cases in a pipeline, or a field that others depend on |
| `422 required_fields_missing` | A move into a stage whose required fields the case lacks |

All codes are in the [error catalog](/errors).

## Good practice

- Name stages after what the customer did, not after your tool: "Quote sent", not "Stage 2".
- Keep the entry stage for the agent and put the human work in later stages.
- Create fields before the agent that fills them, and write an `instruction` for each field with `source: "agent"`.
- Use `sensitive` for anything the agent should never see, such as a national ID.
- Put the fields a stage needs in `requiredFields`, so a case cannot reach "Visit booked" without an address.
- Close cases with an `outcome`; it is what your reports group by.

## What's next

- [Agents](/guides/agents) to build the agent that works a stage.
- [Build your own inbox](/guides/inbox) to show cases and let your team answer.
- [Measure how long cases take](/guides/usage-and-budgets#measure-how-long-cases-take) once cases move.
