Salesforce¶
Записывайте запись в Salesforce каждый раз, когда сессия проходит веху — обычно вердикт автоматических проверок, чтобы кейс или процесс в Salesforce показывал, прошла ли поставка, есть ли предупреждения или она упала, и никому не нужно было открывать Expecto.
Направление: только outbound. Expecto аутентифицируется по OAuth 2.0 Client Credentials flow connected app вашей организации и создаёт по одной записи sObject на событие ровно с теми полями, которые вы замапили.
событие сессии ──▶ исходящая привязка ──▶ подключение Salesforce ──▶ POST /services/data/vXX.X/sobjects/<Объект>
(какие события, (My Domain URL,
какой объект, Consumer Key/Secret,
маппинг полей) версия API)
1. Подготовьте организацию Salesforce¶
Всё ниже делается в Salesforce Setup и требует администратора Salesforce. Кроме доступа к API, Expecto ничего не требует от редакции организации.
Кастомный объект и поля¶
Создайте (или выберите) объект, в который Expecto будет писать, например
ExpectoWebhookEvent__c, и поля, которые нужно заполнять. Типичный набор:
| Поле | Тип | Заполняется из |
|---|---|---|
SessionId__c |
Text (36) | {{ session_id }} |
RunId__c |
Text (36) | {{ run_id }} |
Period__c |
Text | {{ period }} |
Status__c |
Picklist | {{ verdict }} через таблицу значений |
Phase__c |
Picklist | {{ phase }} через таблицу значений — technical (результат парсинга) или content (бизнес-проверка) |
ProcessId__c |
Text / ключ lookup | {{ context.process_id }} — ваш собственный корреляционный id, передаётся в контексте сессии |
Expecto отправляет только замапленные поля. Salesforce отклоняет запрос,
в котором названо поле, которого нет у объекта (HTTP 400, INVALID_FIELD),
поэтому объект со всеми замапленными полями должен существовать до первой
доставки.
Ограниченные picklist
Если Status__c — ограниченный picklist, его значения должны совпадать
с тем, что шлёт Expecto. Либо добавьте в список значения вердикта Expecto
(passed, warnings, failed), либо оставьте свои значения и переведите
их через value_maps (шаг 3). Несовпадение уводит доставку в dead-letter
с INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST.
Connected app с Client Credentials flow¶
- Setup → App Manager → New Connected App (или External Client App Manager в новых организациях). Включите настройки OAuth; callback URL любой — этот flow его не использует.
- Выберите scope Manage user data via APIs (
api). - Включите Client Credentials Flow и выберите пользователя Run As. Все записи создаются от его имени, поэтому дайте ему профиль или permission set с правом Create на объект и Edit на каждое замапленное поле.
- Откройте страницу Manage Consumer Details приложения и скопируйте
Consumer Key и Consumer Secret. Это OAuth
client_id/client_secret, которые будет использовать Expecto. - Запишите My Domain URL организации (Setup → My Domain), например
https://acme.my.salesforce.com. Эндпоинт токена —<My Domain URL>/services/oauth2/token; он должен быть доступен из интернета по https.
2. Создайте подключение в Expecto¶
Settings → Connections → Add Connection, тип Salesforce:

| Поле | Значение |
|---|---|
| Name | Любое, отличающее подключение, например Salesforce – production |
| My Domain URL | My Domain URL организации, только https://, без пути |
| API version (опционально) | Версия Salesforce REST API, по умолчанию 66.0 |
| Consumer Key / Consumer Secret | Из connected app. Хранятся зашифрованными, больше не показываются |
Нажмите Test connection: Expecto запрашивает токен и сообщает результат,
не создавая записей. auth_error означает, что пара Consumer Key/Secret
отклонена или для приложения не включён Client Credentials flow;
connection_error — что My Domain URL не ответил.
Через MCP-инструмент или API то же подключение выглядит так:
POST /api/integration-connections
{
"name": "Salesforce – production",
"type": "salesforce",
"base_url": "https://acme.my.salesforce.com",
"config": { "api_version": "66.0" },
"credentials": { "client_id": "<Consumer Key>", "client_secret": "<Consumer Secret>" }
}
3. Привяжите события сессии к объекту¶
Откройте проект, страницу Context, карточку Outbound Bindings. Проект может держать до 20 привязок; каждая называет подключение, события, на которые реагирует, и маппинг.

Edit открывает встроенный редактор:

- Binding name —
^[A-Za-z][A-Za-z0-9_]{0,63}$, уникально в проекте. Отображается в логе доставок. - Connection — предлагаются только подключения с возможностью outbound.
- Event types — см. какие события что несут.
- Salesforce object — API-имя объекта (
ExpectoWebhookEvent__c). - Fields — по строке на поле Salesforce: API-имя поля и шаблон значения.
Шаблон — литерал, плейсхолдер или текст со встроенными плейсхолдерами
(
FIBU {{ period }}). Отметьте Required у полей, которые не могут разрешиться в пустое значение. - Active — снимите, чтобы поставить привязку на паузу, не удаляя.
Та же привязка в JSON для PUT /api/projects/{id}/outbound-bindings или
MCP-инструмента set_project_outbound_bindings:
{
"bindings": [{
"name": "salesforce_check_verdict",
"connection_id": "<connection uuid>",
"events": ["session.parsed", "session.checked", "session.failed", "session.exported"],
"mapping": {
"sobject": "ExpectoWebhookEvent__c",
"fields": {
"Status__c": "{{ verdict }}",
"Phase__c": "{{ phase }}",
"SessionId__c": "{{ session_id }}",
"RunId__c": "{{ run_id }}",
"Period__c": "{{ period }}",
"ProcessId__c": "{{ context.process_id }}"
},
"required_fields": ["SessionId__c", "ProcessId__c"],
"value_maps": {
"Status__c": { "passed": "Passed", "warnings": "Warnings", "failed": "Error", "*": "Error" },
"Phase__c": { "technical": "Technical", "content": "Content", "*": "Context" }
}
},
"is_active": true
}]
}
PUT заменяет весь список — передавайте все привязки проекта,
{"bindings": []} удаляет все.
Маппинг подробно¶
| Ключ | Обязателен | Значение |
|---|---|---|
sobject |
да | API-имя объекта: буквы, цифры, подчёркивания, до 80 символов. |
fields |
да | API-имя поля → шаблон значения. Не пустой. Передаются ровно эти поля. |
required_fields |
нет | Подмножество fields. Если любое из них разрешается в null или "", доставка уходит в dead-letter до отправки с именем поля. |
value_maps |
нет | Поле → таблица {отрендеренное значение: замена}, применяется после рендеринга и до проверки обязательных полей. "*" — запасное значение для всего, чего нет в таблице (включая null). Ключи должны называть замапленные поля. |
Всё остальное отклоняется при сохранении как неизвестный ключ. Плейсхолдеры
тоже проверяются при сохранении: каждый {{ context.<name> }} должен быть
скаляром, объявленным в контекстной схеме проекта, а каждое поле envelope —
одним из известных имён.
Мапьте verdict, а не health
verdict — результат автоматических проверок: passed, warnings,
failed или null, пока результата нет; он стабилен на последующих
стадиях. health — чип дашборда (created, in_progress, blocked,
attention, ready, completed); он смешивает прогресс с результатом и
меняется по мере продвижения сессии.
Две фазы: сначала технический результат, потом содержательный¶
Salesforce обычно вызывает POST /sessions/{id}/run через несколько
секунд после создания сессии, пока Expecto ещё собирает источники контекста
проекта. Парсингу этот контекст не нужен, поэтому он идёт сразу: за секунды
привязка получает технический результат — session.parsed (phase =
technical, parse_error_count / parse_warning_count) либо
session.failed со stage = parse (битый файл, нет обязательной колонки,
не та кодировка). Затем Run ждёт сбор перед трансформацией: статус
waiting, waiting_for = context_fetch, тот же run_id. Со стороны
Salesforce повторять ничего не нужно. Когда сбор завершится, Run продолжится
сам, и придёт содержательный результат — session.checked или
session.failed (phase = content). Если сбор упал или ожидание превысило
серверный таймаут (по умолчанию 30 мин), Run закрывается как errors, и
приходит session.failed со stage = context_fetch, reason =
context_fetch_failed или context_fetch_timeout, phase = context и
message с именем источника и ошибкой.
Маппьте {{ phase }} на picklist (Phase__c выше), если Salesforce ведёт
оба результата раздельно; привязке, которой нужен только итоговый вердикт,
достаточно не подписываться на session.parsed и не использовать поле.
На какие события подписываться¶
| Событие | Когда срабатывает | run_id |
verdict |
|---|---|---|---|
session.parsed |
Все файлы разобраны — технический результат (phase = technical, счётчики парсинга) |
есть | null / warnings |
session.checked |
Бизнес-правила отработали без ошибок (phase = content) |
есть | passed / warnings |
session.failed |
Стадия пайплайна упала (stage — parse → phase = technical; transform, validation_gating, validate или check → phase = content) либо прогон остановлен на контекстном гейте после парсинга (context_fetch, context_validation → phase = context); reason — код, message объясняет |
есть | failed; null при остановке на гейте — маппить через значение по умолчанию * |
session.exported |
Пакет сформирован | — | на момент экспорта |
session.confirmed, session.approved |
Подтверждение человеком | — | на этот момент |
session.created, session.uploaded, session.transformed |
Остальные стадии пайплайна | только session.transformed |
null |
run_id есть только у событий прогона пайплайна
Маппинг, использующий {{ run_id }} на событии без прогона (например,
session.created), уходит в dead-letter с именем переменной. Либо
подпишите привязку только на события прогона, либо уберите RunId__c из
маппинга.
Для двухфазного потока практичный набор — session.parsed +
session.checked + session.failed; только для итогового вердикта —
session.checked + session.failed. Добавьте session.exported, если
Salesforce должен узнать и об отправке пакета.
4. Что отправляет Expecto¶
На каждое подходящее событие — одна доставка:
POST <My Domain URL>/services/oauth2/tokenсgrant_type=client_credentials(тело формы — учётные данные никогда не попадают в URL). Токен кэшируется в памяти процесса и переиспользуется; он никогда не сохраняется.-
POST <My Domain URL>/services/data/v66.0/sobjects/ExpectoWebhookEvent__cсAuthorization: Bearer …и JSON-телом ровно из замапленных полей после таблиц значений:{ "Status__c": "Passed", "SessionId__c": "7c3a76c0-6a00-44a5-a0ad-9093e6d219e1", "RunId__c": "0e1c1c3e-3a5c-4b39-9d24-6d4a3e6b9a1f", "Period__c": "2026-08", "ProcessId__c": "PRC-000123" } -
Ответ
201с{"id": "a0B…"}помечает доставку как succeeded; id записи сохраняется на доставке для аудита.
Тайм-аут запроса — 15 с на вызов. Редиректы не выполняются.
Обработка ошибок¶
Доставки едут на общей машинерии retry вебхуков (backoff от 30 с до 1 ч, 6 попыток, затем dead-letter), но ответы Salesforce сначала классифицируются:
| Salesforce отвечает | Expecto делает |
|---|---|
5xx, 429 (лимит API), тайм-аут, сетевая ошибка, ответ токен-эндпоинта 200 не в JSON |
Повтор с backoff — тот же запрос может пройти позже. |
| 401 на вызове записи | Обновить токен один раз и повторить в той же попытке; второй 401 уходит в dead-letter как auth_error. |
| 400 (неизвестное поле, плохое значение, picklist), 403 (нет прав), 404 (неверное имя объекта или версия API) | Сразу dead-letter — повтор не пройдёт без изменения запроса. Первые 2 KiB тела ответа сохраняются на доставке. |
| Токен-эндпоинт отклоняет учётные данные | Dead-letter как auth_error; тело ответа не сохраняется, потому что Salesforce эхом возвращает параметры запроса. |
После 10 dead-letter подряд привязка автоматически отключается
(disabled_reason: consecutive_failures) и перестаёт создавать доставки;
один успех сбрасывает счётчик. Устранив причину, включите её снова в
редакторе — при сохранении счётчик обнуляется.
5. Мониторинг и replay¶
Settings → Webhooks показывает каждую исходящую привязку организации рядом с webhook-эндпоинтами, с бейджем состояния — Active, Inactive или Auto-disabled с числом сбоев. Actions → Show deliveries открывает лог:

- Result — id записи Salesforce при успехе, класс и текст ошибки при сбое (по наведению — сохранённая выдержка ответа) или время следующей попытки для ожидающей доставки.
- Replay повторяет доставку через адаптер с текущими привязкой и подключением — используйте после исправления маппинга или объекта Salesforce. Replay — новая доставка, связанная с исходной.
Программно: GET /api/webhook-deliveries?connection_id=<uuid> и
POST /api/webhook-deliveries/{id}/replay либо MCP-инструменты
list_webhook_deliveries / replay_webhook_delivery.
Диагностика¶
| Симптом в логе доставок | Причина | Что делать |
|---|---|---|
delivery_rejected … INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST |
Отрендеренного значения нет в picklist | Добавить запись value_maps для поля или расширить picklist. |
delivery_rejected … INVALID_FIELD / No such column |
Замапленного поля нет у объекта | Исправить API-имя поля (кастомные поля оканчиваются на __c). |
delivery_rejected: Salesforce rejected the record (HTTP 404) |
Неверное имя sobject или неподдерживаемая версия API |
Проверить API-имя объекта и api_version подключения. |
delivery_rejected: Salesforce rejected the record (HTTP 403) |
У пользователя Run As нет Create на объект или Edit на поле | Поправить permission set пользователя. |
auth_error: Token endpoint rejected the credentials |
Неверные Consumer Key/Secret, flow выключен или нет пользователя Run As | Перепроверить connected app; ротировать секрет в Edit. |
delivery_rejected: Query placeholder 'run_id' has no value |
Привязка подписана на событие без прогона | Ограничить события session.checked / session.failed или убрать поле. |
delivery_rejected: Required mapping field(s) resolved to an empty value: … |
Обязательное поле отрендерилось в null — обычно скаляр контекста, который сессия не задала |
Передать значение в контексте сессии или снять Required. |
connection_inactive / binding_inactive |
Подключение или привязка деактивированы | Включить снова; затем доставку можно повторить через Replay. |
| Привязка показывает Auto-disabled | 10 dead-letter подряд | Устранить причину, включить привязку, повторить упавшие доставки. |