# Долгий метод в бизнес-процессе

Рецепт для приложения, чья работа не укладывается в окно вызова: приложение принимает задачу от приостановленного действия бизнес-процесса Битрикс24, отвечает сразу и досылает результат отдельным запросом, когда закончит.

**Базовый URL:** `https://vibecode.bitrix24.tech/v1` | **Авторизация:** `X-Api-Key` | **Скоуп ответа:** `bizproc`

[Когда это нужно](#когда-это-нужно) | [Что понадобится](#что-понадобится) | [Как устроено решение](#как-устроено-решение) | [Шаг 1](#шаг-1-принять-вызов-и-ответить-сразу) | [Шаг 2](#шаг-2-записать-промежуточный-шаг-в-журнал-процесса) | [Шаг 3](#шаг-3-отправить-результат-в-процесс) | [Ключ для ответа](#ключ-для-ответа) | [Ошибки](#ошибки) | [Ограничения](#ограничения) | [Полный код](#полный-код)

Примеры шагов показывают отдельные вызовы и опираются на переменные, объявленные выше по сценарию. Готовый к запуску обработчик — в разделе [Полный код](#полный-код).

## Когда это нужно

Вызов через [внешний API приложения](/docs/applications/external-api) живёт 30 секунд — это общий предел, за которым вызывающий получает `503 APP_API_TIMEOUT`. Само приложение обязано начать отвечать раньше: платформа перестаёт ждать его ответ на двадцать пятой секунде и отдаёт вызывающему `503 APP_API_UNAVAILABLE`.

Работа, которая в эти окна не помещается, — расчёт по большой выборке, обращение к внешней системе, генерация документа, ожидание ответа человека — требует другой формы. Приложение принимает задачу, отвечает роботу сразу и досылает результат тогда, когда он готов. Бизнес-процесс всё это время спит на своём шаге, а его состояние держит портал.

Долгий метод по контракту решения устроен иначе — там адрес ответа и одноразовый токен приезжают заголовками вызова, а платформа доставляет исход на портал сама. Этот рецепт про бизнес-процесс, где ответом распоряжается само решение: [Отправить результат долгого метода](/docs/applications/solution-call-result).

## Что понадобится

1. **Приложение с включённым внешним API и выпущенным ключом.** Переключатель и выдача ключа — в карточке приложения, условия и отказы описаны на странице [Внешний API приложения](/docs/applications/external-api). Этим ключом бизнес-процесс вызывает приложение.
2. **Ключ платформы для ответа** — `vibe_api_…` со скоупом `bizproc`, в режиме чтения и записи, привязанный к тому же порталу. Его создаёте вы сами и сами передаёте приложению переменной окружения — платформа ключ не выдаёт. Требования разобраны в разделе [Ключ для ответа](#ключ-для-ответа).
3. **Приостанавливаемое действие бизнес-процесса на портале.** Оно вызывает приложение, кладёт в тело запроса свой `eventToken` и засыпает до события. Регистрация такого действия — [`/v1/bizproc-activities`](/docs/entities/bizproc-activities).
4. **Node.js 18 или новее** для примеров.

Во всех примерах `$VIBE_API_KEY` — ключ платформы для ответа из пункта 2, а `YOUR_APP_EXTERNAL_API_KEY` — ключ внешнего API из пункта 1.

## Как устроено решение

1. Действие бизнес-процесса вызывает приложение по адресу внешнего API и передаёт в теле запроса `eventToken` — одноразовый ключ своего шага.
2. Приложение сохраняет `eventToken` вместе с задачей и отвечает `202` в пределах окна вызова. Действие после этого спит.
3. Пока работа идёт, приложение пишет в журнал процесса через [`POST /v1/workflows/activity-log`](/docs/automation/workflows/activity-log). Состояние процесса это не меняет.
4. Закончив, приложение вызывает [`POST /v1/workflows/event`](/docs/automation/workflows/event) с тем же `eventToken` и значениями для выходных параметров действия. Процесс просыпается и идёт дальше.

Портальные учётные данные приложению не нужны ни на одном шаге: платформа обращается к порталу сохранёнными данными владельца ключа.

## Шаг 1. Принять вызов и ответить сразу

Робот бизнес-процесса отправляет запрос на адрес приложения:

```
Адрес:      https://vibecode.bitrix24.tech/v1/applications/:applicationId/api/<ваш-путь>
Метод:      POST
Заголовок:  X-Api-Key: YOUR_APP_EXTERNAL_API_KEY
```

В теле действие передаёт свой `eventToken` и всё, что нужно приложению для работы. Поле кладёт действие, а не платформа — имя поля выбирает автор действия, и в примерах ниже оно называется `eventToken`.

К телу платформа добавляет заголовки, которые вызывающий подделать не может — входящие имена с префиксом `X-Vibe-` срезаются:

| Заголовок | Что несёт |
|-----------|-----------|
| `X-Vibe-Request-Id` | Идентификатор вызова. Тот же идентификатор платформа пишет в свой журнал |
| `X-Vibe-Caller-Kind` | Значение `external-api` — вызов пришёл этим каналом, а не из интерфейса Битрикс24 |
| `X-Vibe-Caller-Portal-Id` | Портал, которому принадлежит ключ вызова |
| `X-Vibe-Caller-Key-Id` | Идентификатор ключа внешнего API, которым сделан вызов |

Заголовков с адресом ответа, токеном ответа и сроком на этом пути нет — они принадлежат вызову по контракту решения. Нет здесь и заголовков с личностью сотрудника: внешний вызов сессии не несёт. Полный разбор того, что доезжает до приложения, — [Что получает приложение](/docs/applications/external-api).

Ответ `202` говорит роботу, что задача принята. Тело ответа при этом остаётся за приложением: робот раскладывает по переменным бизнес-процесса значения верхнего уровня.

### JavaScript

```javascript
// Обработчик маршрута приложения, который вызывает действие бизнес-процесса.
app.post('/long-method', async (req, res) => {
  const { eventToken, dealId } = req.body;

  if (!eventToken) {
    res.status(400).json({ accepted: false, reason: 'EVENT_TOKEN_REQUIRED' });
    return;
  }

  // Задачу кладём в очередь и возвращаем управление СРАЗУ: ответить надо
  // в пределах окна вызова, а работа займёт больше.
  queue.push({ eventToken, dealId });

  res.status(202).json({ accepted: true });
});
```

## Шаг 2. Записать промежуточный шаг в журнал процесса

Пока работа идёт, приложение отчитывается в журнал выполнения бизнес-процесса. Запись видна тому, кто смотрит процесс на портале, и состояние процесса не меняет — действие остаётся приостановленным.

### cURL

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/workflows/activity-log \
  -H "X-Api-Key: $VIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eventToken":"xxx.yyy.zzz","logMessage":"Выборка собрана, считаем скидку"}'
```

### JavaScript

```javascript
await fetch('https://vibecode.bitrix24.tech/v1/workflows/activity-log', {
  method: 'POST',
  headers: {
    'X-Api-Key': process.env.VIBE_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    eventToken,
    logMessage: 'Выборка собрана, считаем скидку',
  }),
});
```

```json
{
  "success": true,
  "data": true
}
```

Полное описание полей и отказов — [Записать в журнал процесса](/docs/automation/workflows/activity-log).

## Шаг 3. Отправить результат в процесс

Работа закончена — приложение отправляет событие с тем же `eventToken`. Объект `returnValues` попадает в выходные параметры действия, и бизнес-процесс продолжает работу.

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `eventToken` | string | да | Тот самый токен, который приехал в теле вызова на шаге 1 |
| `returnValues` | object | нет | Значения выходных параметров действия. По умолчанию — пустой объект |
| `logMessage` | string | нет | Запись в журнал выполнения процесса вместе с событием |

### cURL

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/workflows/event \
  -H "X-Api-Key: $VIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eventToken":"xxx.yyy.zzz","returnValues":{"discount":12.5,"verdict":"approve"},"logMessage":"Скидка рассчитана"}'
```

### JavaScript

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/workflows/event', {
  method: 'POST',
  headers: {
    'X-Api-Key': process.env.VIBE_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    eventToken,
    returnValues: { discount: 12.5, verdict: 'approve' },
    logMessage: 'Скидка рассчитана',
  }),
});

const body = await res.json();
if (!body.success) {
  console.error(`событие не принято: ${body.error.code}`);
}
```

```json
{
  "success": true,
  "data": true
}
```

Поле `data` со значением `true` означает, что портал принял событие и выполнение процесса возобновлено. Полное описание полей и отказов — [Отправить событие в процесс](/docs/automation/workflows/event).

## Ключ для ответа

Отвечать надо **своим ключом платформы, а не тем ключом, которым приложение вызвали**. Ключ внешнего API заперт перечнем маршрутов: ему открыт ровно один адрес платформы — проброс в приложение. На `POST /v1/workflows/event` и `POST /v1/workflows/activity-log` он получает `403 APP_API_KEY_OUT_OF_SCOPE` ещё до обработчика, и никакая настройка этого не меняет.

Годится ключ платформы `vibe_api_…` — тот же, которым решение уже обращается к API Вайбкод. **Платформа такой ключ не выдаёт и сама в приложение не подставляет.** Его создаёт человек в разделе [Ключи API](/keys) и передаёт приложению переменной окружения при развёртывании. Имя переменной выбираете вы, по соглашению — `VIBE_API_KEY`: приложение читает её из своего окружения и отправляет значение заголовком `X-Api-Key`. Автоматически заведённого ключа «на сервер» не существует, и `401 MISSING_API_KEY` от приложения означает, что переменная не доехала. Три требования к ключу:

| Требование | Что будет иначе |
|------------|-----------------|
| Скоуп `bizproc` | `403 SCOPE_DENIED` |
| Режим чтения и записи | `403 WRITE_BLOCKED_READONLY_KEY`. Ответ несёт адрес страницы, где режим переключается |
| Привязка к порталу | `401 TOKEN_MISSING`. Так же отвечает ключ авторизации приложения `vibe_app_…`, предъявленный без токена сессии сотрудника: у него портальные данные живут в сессии, а не на ключе |

**Ответ уходит на портал правами владельца ключа, а не того сотрудника, который запустил бизнес-процесс.** Событие и запись в журнал платформа выполняет сохранёнными учётными данными владельца, поэтому выбирайте владельца ключа осознанно: у него должны быть права на те процессы, которым решение отвечает.

Текущий набор скоупов ключа и состояние его привязки к порталу показывает [`GET /v1/me`](/docs/keys-auth/me). Где переключается режим ключа — [Ключи и авторизация](/docs/keys-auth).

## Ошибки

Таблица собрана по двум адресам ответа — `POST /v1/workflows/event` и `POST /v1/workflows/activity-log`. Отказы самого проксирующего вызова на шаге 1 — в таблице [Внешний API приложения](/docs/applications/external-api).

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `MISSING_PARAMS` | Не передан `eventToken`. На записи в журнал так же отвечает запрос без `logMessage` |
| 401 | `MISSING_API_KEY` | Заголовок с ключом не передан |
| 401 | `INVALID_API_KEY` | Ключ не опознан: такой строки на платформе нет |
| 401 | `TOKEN_MISSING` | У ключа нет привязки к порталу. Текст ответа называет причину и то, что с ней делать |
| 403 | `SCOPE_DENIED` | Ключу не хватает скоупа `bizproc`. Проверяется первым — раньше режима ключа |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ переведён в режим чтения. Поле `details.switchUrl` называет страницу, где режим меняется |
| 403 | `APP_API_KEY_OUT_OF_SCOPE` | Ответ отправлен ключом внешнего API приложения. Этому ключу открыт только проброс в приложение |
| 403 | `BITRIX_ACCESS_DENIED` | Битрикс24 отклонил запрос: токен недействителен, истёк или уже использован |
| 422 | `BITRIX_ERROR` | Ошибка на стороне Битрикс24 |
| 429 | `RATE_LIMITED` | Превышен лимит запросов. Повторите через 1–2 секунды |
| 502 | `BITRIX_UNAVAILABLE` | Битрикс24 недоступен |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Пример ответа при ошибке

403 — ключ переведён в режим чтения:

```json
{
  "success": false,
  "error": {
    "code": "WRITE_BLOCKED_READONLY_KEY",
    "message": "Key is in read-only mode. Switch to read+write in /keys to enable writes.",
    "details": {
      "method": "bizproc.event.send",
      "keyName": "Solution deploy key",
      "currentMode": "READONLY",
      "switchUrl": "/keys"
    }
  }
}
```

## Ограничения

| Ограничение | Значение |
|-------------|----------|
| Окно ответа приложения на шаге 1 | 25 секунд на ответ приложения внутри общего предела вызова в 30 секунд |
| Срок жизни `eventToken` | Платформа его не ограничивает и передаёт токен в Битрикс24 как есть. Недействительный токен отклоняет Битрикс24 — `403 BITRIX_ACCESS_DENIED` |
| Повторное использование `eventToken` | Токен одноразовый: каждая приостановка процесса выдаёт новый, и после `POST /v1/workflows/event` прежний недействителен |
| Записей в журнал на один шаг | Платформа их не ограничивает. Каждая запись — отдельный вызов API и расходуется по общим лимитам |

## Полный код

Обработчик принимает вызов действия, отвечает `202` и доводит задачу в фоне, отчитываясь в журнал процесса. Все переменные объявлены внутри, запускается как есть на Node.js 18 и новее.

```javascript
import express from 'express';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // робот шлёт поля формы, не JSON

const VIBE_URL = 'https://vibecode.bitrix24.tech/v1';
const VIBE_API_KEY = process.env.VIBE_API_KEY;

async function callVibe(path, body) {
  const res = await fetch(`${VIBE_URL}${path}`, {
    method: 'POST',
    headers: {
      'X-Api-Key': VIBE_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const payload = await res.json();
  if (!payload.success) {
    throw new Error(`${path}: ${payload.error.code} — ${payload.error.message}`);
  }
  return payload.data;
}

async function runLongWork(eventToken, dealId) {
  try {
    await callVibe('/workflows/activity-log', {
      eventToken,
      logMessage: `Считаем скидку по сделке ${dealId}`,
    });

    const discount = await computeDiscount(dealId); // ваша долгая работа

    await callVibe('/workflows/event', {
      eventToken,
      returnValues: { discount, verdict: discount > 0 ? 'approve' : 'reject' },
      logMessage: 'Скидка рассчитана',
    });
  } catch (err) {
    // Процесс спит и ждёт события. Ошибку тоже надо вернуть событием,
    // иначе шаг останется приостановленным до срока, заданного на портале.
    await callVibe('/workflows/event', {
      eventToken,
      returnValues: { discount: 0, verdict: 'error' },
      logMessage: `Расчёт не удался: ${err.message}`,
    }).catch(() => {});
  }
}

app.post('/long-method', (req, res) => {
  const { eventToken, dealId } = req.body;

  if (!eventToken || !dealId) {
    res.status(400).json({ accepted: false, reason: 'EVENT_TOKEN_AND_DEAL_REQUIRED' });
    return;
  }

  // Ответ уходит СРАЗУ, работа идёт после него.
  res.status(202).json({ accepted: true });
  void runLongWork(eventToken, String(dealId));
});

async function computeDiscount(dealId) {
  await new Promise((resolve) => setTimeout(resolve, 60_000));
  return 12.5;
}

app.listen(process.env.PORT || 3000);
```

## Известные особенности

- **Ошибку тоже возвращают событием.** Приостановленное действие ждёт `POST /v1/workflows/event` и ничего не знает о том, что приложение упало. Пока события нет, шаг остаётся приостановленным. Поэтому неудачный расчёт отвечает событием с признаком отказа в `returnValues`, а не молчанием.
- **Ответ `202` на шаге 1 не гарантирует, что работа началась.** Робот получает подтверждение приёма, и дальше единственный сигнал о ходе работы — записи в журнале процесса. Заводите запись на каждой значимой границе: разбирать застрявший шаг по журналу дешевле, чем по логам приложения.
- **Ключ ответа не хранится рядом с ключом вызова.** Ключ внешнего API лежит в настройках робота на портале, ключ ответа — в переменных окружения приложения. Отзыв одного другого не трогает, и перевыпуск ключа внешнего API идущие ответы не ломает.
- **Один `eventToken` закрывает один шаг.** Токен становится недействительным сразу после `POST /v1/workflows/event`, и уже использованный Битрикс24 отклоняет кодом `BITRIX_ACCESS_DENIED`. Поэтому повтор после сетевого обрыва небезопасен вслепую: тот же код придёт и когда первый запрос дошёл, и когда токен был испорчен, а различить эти два случая по ответу нечем.
- **Скоуп проверяется раньше режима ключа.** Ключ без скоупа `bizproc` и в режиме чтения получит `403 SCOPE_DENIED`, а не отказ по режиму. Сначала откройте скоуп, потом проверяйте режим.

## Смотрите также

- [Внешний API приложения](/docs/applications/external-api)
- [Отправить результат долгого метода](/docs/applications/solution-call-result)
- [Отправить событие в процесс](/docs/automation/workflows/event)
- [Записать в журнал процесса](/docs/automation/workflows/activity-log)
- [Бизнес-процессы](/docs/automation/workflows)
- [Ключи и авторизация](/docs/keys-auth)
- [Ошибки](/docs/errors)
