Перейти к содержанию

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

  1. Setup → App Manager → New Connected App (или External Client App Manager в новых организациях). Включите настройки OAuth; callback URL любой — этот flow его не использует.
  2. Выберите scope Manage user data via APIs (api).
  3. Включите Client Credentials Flow и выберите пользователя Run As. Все записи создаются от его имени, поэтому дайте ему профиль или permission set с правом Create на объект и Edit на каждое замапленное поле.
  4. Откройте страницу Manage Consumer Details приложения и скопируйте Consumer Key и Consumer Secret. Это OAuth client_id / client_secret, которые будет использовать Expecto.
  5. Запишите 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:

Диалог 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 привязок; каждая называет подключение, события, на которые реагирует, и маппинг.

Карточка Outbound Bindings на странице Context проекта

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

Редактор исходящей привязки с маппингом полей Salesforce

  • 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

На каждое подходящее событие — одна доставка:

  1. POST <My Domain URL>/services/oauth2/token с grant_type=client_credentials (тело формы — учётные данные никогда не попадают в URL). Токен кэшируется в памяти процесса и переиспользуется; он никогда не сохраняется.
  2. 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"
    }
    
  3. Ответ 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 открывает лог:

Лог доставок привязки Salesforce с успешной, упавшей и ожидающей доставкой

  • 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 подряд Устранить причину, включить привязку, повторить упавшие доставки.