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

# SAP Field Service Management

> Client-mode REST API connector for SAP Field Service Management: create service calls and activities from alarms, upsert by alarm id, read and patch service calls, activities and equipment (run hours as UDFs), CoreSQL queries, and Service Management business actions (cancel, technically complete, release, close), with OAuth 2.0 client credentials

Technical Preview

## Overview

[SAP Field Service Management](https://www.sap.com/products/scm/field-service-management.html) (now documented as **SAP Field Service and Asset Management**) plans and runs field service: **service calls** (tickets), **activities** (the jobs technicians do) and **equipment** at customer sites. This connector lets the broker open a service call and an activity when a machine raises an alarm, put run hours and other machine data on the equipment, and close the loop with business actions, so planners and technicians see what the machine sees.

The template has two connectors:

* **SapFsmApi** (`SapFieldServiceManagementRestApi.lot`), a client connector on your cluster host with 24 events: 13 writes, 10 reads and 1 token event. It has no `DELETE` or `PUT` events; cancel actions are business actions, not deletes.
* **SapFsmBusinessRuleReceiver** (`cmms_sap_fsm_webhook.lot`), an optional server connector on port `9012` that receives calls from a Business Rule **Webhook** action and publishes them on MQTT.

> [!NOTE]
>
> SAP publishes the FSM API specs on the SAP Business Accelerator Hub, which needs an SAP login, so the events were picked by hand from the SAP Help Portal ([Data API v4](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_api/data-api-introduction.html), [Data Model](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_model/servicecalldto_v27.html), [Query API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_query_api/query-api-intro.html), [Service API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_service_api/supported-requests.html), [OAuth](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_access_api/get-access-token.html)) and the [SAP/fsm-sdk](https://github.com/SAP/fsm-sdk) source.

## Events

| Event | Topic | Request | Kind | What it does |
| - | - | - | - | - |
| `GetToken` | `cmms/sapfsm/token/create` | `POST /api/oauth2/v2/token` | Auth | Get an OAuth 2.0 access token (client credentials, HTTP Basic with the FSM OAuth client id:secret). Reply access\_token, token\_type, expires\_in; send access\_token on every other event |
| `ListServiceCalls` | `cmms/sapfsm/service-call/list` | `GET /api/data/v4/ServiceCall?dtos=ServiceCall.27&page={page}&pageSize={page_size}` | Read | Service calls, paged (default 20 per page, keep pageSize at 1000 or less). Reply data\[].serviceCall plus currentPage / lastPage |
| `FindServiceCalls` | `cmms/sapfsm/service-call/find` | `GET /api/data/v4/ServiceCall?dtos=ServiceCall.27&query={query}&pageSize={page_size}` | Read | Service calls matching a URL-encoded Data API query condition, e.g. lastChanged>=\<epoch ms> for incremental polls |
| `GetServiceCall` | `cmms/sapfsm/service-call/get` | `GET /api/data/v4/ServiceCall/{id}?dtos=ServiceCall.27` | Read | One service call by cloud id |
| `GetServiceCallByExternalId` | `cmms/sapfsm/service-call/get-by-external-id` | `GET /api/data/v4/ServiceCall/externalId/{external_id}?dtos=ServiceCall.27` | Read | One service call by your externalId (e.g. the alarm id) |
| `CreateServiceCall` | `cmms/sapfsm/service-call/create` | `POST /api/data/v4/ServiceCall?dtos=ServiceCall.27` | Write | Create a service call from an alarm; reply 201 \{data:\[\{serviceCall:\{id, code, lastChanged...}}]} |
| `UpdateServiceCall` | `cmms/sapfsm/service-call/update` | `PATCH /api/data/v4/ServiceCall/{id}?dtos=ServiceCall.27&forceUpdate={force_update}` | Write | Patch service call fields; send the current lastChanged (optimistic lock) or force\_update true. Reply 201 |
| `UpsertServiceCallByExternalId` | `cmms/sapfsm/service-call/upsert` | `PATCH /api/data/v4/ServiceCall/externalId/{external_id}?dtos=ServiceCall.27` | Write | Create or update the service call keyed by your externalId (one service call per alarm id; creating needs all mandatory fields) |
| `FindActivities` | `cmms/sapfsm/activity/find` | `GET /api/data/v4/Activity?dtos=Activity.43&query={query}&pageSize={page_size}` | Read | Activities matching a URL-encoded Data API query condition |
| `GetActivity` | `cmms/sapfsm/activity/get` | `GET /api/data/v4/Activity/{id}?dtos=Activity.43` | Read | One activity by cloud id (status, executionStage, responsibles, object -> service call) |
| `CreateActivity` | `cmms/sapfsm/activity/create` | `POST /api/data/v4/Activity?dtos=Activity.43` | Write | Create an activity under a service call (object = \{objectId, objectType SERVICECALL}) |
| `UpdateActivity` | `cmms/sapfsm/activity/update` | `PATCH /api/data/v4/Activity/{id}?dtos=Activity.43&forceUpdate={force_update}` | Write | Patch activity fields (send lastChanged or force\_update true). Use the Service Management actions to change planning state |
| `FindEquipment` | `cmms/sapfsm/equipment/find` | `GET /api/data/v4/Equipment?dtos=Equipment.24&query={query}&pageSize={page_size}` | Read | Equipment matching a URL-encoded query condition (e.g. serialNumber='...') |
| `GetEquipment` | `cmms/sapfsm/equipment/get` | `GET /api/data/v4/Equipment/{id}?dtos=Equipment.24` | Read | One equipment by cloud id (read lastChanged before a PATCH) |
| `GetEquipmentByExternalId` | `cmms/sapfsm/equipment/get-by-external-id` | `GET /api/data/v4/Equipment/externalId/{external_id}?dtos=Equipment.24` | Read | One equipment by externalId (usually the S/4HANA or ERP equipment number) |
| `UpdateEquipment` | `cmms/sapfsm/equipment/update` | `PATCH /api/data/v4/Equipment/{id}?dtos=Equipment.24&forceUpdate={force_update}` | Write | Patch equipment with machine data (remarks, status, UDF values). force\_update true skips the lastChanged lock |
| `UpsertEquipmentByExternalId` | `cmms/sapfsm/equipment/upsert` | `PATCH /api/data/v4/Equipment/externalId/{external_id}?dtos=Equipment.24` | Write | Patch equipment by externalId, e.g. a run-hours UDF referenced by its UDF meta externalId |
| `Query` | `cmms/sapfsm/query/run` | `POST /api/query/v1?dtos={dtos}&page={page}&pageSize={page_size}` | Read | Read-only CoreSQL query (POST with \{"query": ...}); dtos lists every DTO used, separated by ; |
| `CreateServiceCallWithActivities` | `cmms/sapfsm/service-call-tree/create` | `POST /service-management/api/v2/composite-tree/service-calls` | Write | Service Management API composite tree: create a service call and its activities in one request |
| `CancelServiceCall` | `cmms/sapfsm/service-call/cancel` | `POST /service-management/api/v2/servicecalls/{id}/actions/cancel` | Write | Business action: cancel a service call (not a delete) |
| `CompleteServiceCall` | `cmms/sapfsm/service-call/technically-complete` | `POST /service-management/api/v2/servicecalls/{id}/actions/technically-complete` | Write | Business action: mark a service call technically complete |
| `ReleaseActivity` | `cmms/sapfsm/activity/release` | `POST /service-management/api/v2/activities/{id}/actions/release` | Write | Business action: release a planned activity to the technician |
| `CloseActivity` | `cmms/sapfsm/activity/close` | `POST /service-management/api/v2/activities/{id}/actions/close` | Write | Business action: close an activity |
| `CancelActivity` | `cmms/sapfsm/activity/cancel` | `POST /service-management/api/v2/activities/{id}/actions/cancel` | Write | Business action: cancel an activity (not a delete) |

Every reply is published on the event topic plus `/response`. Data API replies wrap objects as `{"data": [{"serviceCall": {...}}]}` (lower-case DTO name).

## Prerequisites

* An SAP FSM company and its **cluster**: `eu`, `de`, `us` or `au` (China uses `cn.fsm.sapcloud.cn`). Change `BASE_ADDRESS` to `https://<cluster>.fsm.cloud.sap`. The older `coresuite.com` / `coresystems.net` hosts are being migrated to `fsm.cloud.sap`.
* An **OAuth client** created in Account Admin (client id + secret) with a permission group that can read and write service calls, activities and equipment and run the Service Management actions.
* The numeric **account id** and **company id** (Account Admin).
* Outbound HTTPS from the broker to the cluster host.
* For Business Rule webhooks only: a public HTTPS URL that forwards to the broker's port `9012`.

## Authentication

> [!WARNING]
>
> The client secret and every access token act with the client's permission group. Store `base64(client_id:client_secret)` in the Hub secret `SAP_FSM_BASIC_AUTH` (and the ids in `SAP_FSM_ACCOUNT_ID` / `SAP_FSM_COMPANY_ID`), read them with `GET SECRET`, and never put them or a token in a connector file.

`GetToken` posts `grant_type=client_credentials` to `/api/oauth2/v2/token` with HTTP Basic:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "basic": "<base64 of client_id:client_secret (Hub secret SAP_FSM_BASIC_AUTH)>",
  "form": "grant_type=client_credentials"
}
```

The reply carries `access_token`, `token_type` and `expires_in`. Every other event sends `Authorization: Bearer {value.json.access_token}`, `X-Account-ID` and `X-Company-ID` from the payload, and the fixed `X-Client-ID: custom-coreflux-broker` / `X-Client-Version: 1.0` (SAP asks custom clients to use a `custom-` prefix). Call `GetToken` again before `expires_in` runs out.

Answers seen from the real hosts without valid credentials: a fake client returns `401 {"error":"unauthorized","error_description":"Bad credentials"}`, a Data API call without a token `401 INVALID_LOGIN`, and any call with a fake token `401 {"error":"invalid_token"}`.

## Payload conventions

Each payload carries the context and the event's own fields; the request body comes from `body`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "id": "<ServiceCall id>",
  "force_update": "false",
  "body": {
    "lastChanged": 1759737600000,
    "priority": "LOW",
    "remarks": "Alarm cleared (Coreflux)"
  }
}
```

* `id` is the 32-character FSM id; `external_id` is your id (alarm id, ERP equipment number). Both go into the path, so pass only values you trust.
* `query` is a URL-encoded Data API condition, for example `lastChanged%3E%3D1759737600000` (`lastChanged>=1759737600000`) for incremental polls.
* Updates use **optimistic locking**: send the current `lastChanged` from a GET, or set `force_update` to `"true"`.
* `PATCH .../externalId/<id>` creates the object when it does not exist yet (send all mandatory fields).
* Pages: default 20, `page_size` 1000 at most; replies carry `currentPage`, `lastPage`, `totalObjectCount`.
* DTO versions are pinned: `ServiceCall.27`, `Activity.43`, `Equipment.24`.

### Alarm to service call

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "body": {
    "subject": "Alarm TT-101 = 92.4 C on compressor C-7 (Coreflux)",
    "priority": "HIGH",
    "businessPartner": "<BusinessPartner id>",
    "equipments": [
      "<Equipment id>"
    ],
    "startDateTime": "2026-10-06T08:00:00Z",
    "dueDateTime": "2026-10-07T17:00:00Z",
    "typeCode": "<service call type code>",
    "statusCode": "<service call status code>",
    "originCode": "<origin code>",
    "remarks": "Opened by Coreflux broker"
  }
}
```

Then attach the job (`object` points to the service call):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "body": {
    "subject": "Inspect compressor C-7",
    "type": "ASSIGNMENT",
    "object": {
      "objectId": "<ServiceCall id>",
      "objectType": "SERVICECALL"
    },
    "businessPartner": "<BusinessPartner id>",
    "equipment": "<Equipment id>",
    "startDateTime": "2026-10-06T09:00:00Z",
    "endDateTime": "2026-10-06T10:00:00Z",
    "durationInMinutes": 60,
    "remarks": "Alarm TT-101 (Coreflux)"
  }
}
```

Or create both in one call with the Service Management API:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "body": {
    "subject": "Alarm TT-101 on compressor C-7 (Coreflux)",
    "priority": "HIGH",
    "businessPartner": {
      "id": "<BusinessPartner id>"
    },
    "activities": [
      {
        "subject": "Inspect compressor C-7",
        "durationInMinutes": 60
      }
    ]
  }
}
```

### Machine data on equipment

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "external_id": "<ERP equipment number>",
  "body": {
    "udfValues": [
      {
        "meta": {
          "externalId": "<UDF meta externalId, e.g. RUN_HOURS>"
        },
        "value": "1287.5"
      }
    ]
  }
}
```

`udfValues` reference a user-defined field you create for the equipment in FSM (here by its UDF meta `externalId`). `UpdateEquipment` patches standard fields such as `remarks` by FSM id.

### Query API

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access_token from GetToken>",
  "account_id": "<X-Account-ID>",
  "company_id": "<X-Company-ID>",
  "dtos": "ServiceCall.27;Equipment.24",
  "page": "1",
  "page_size": "100",
  "body": {
    "query": "SELECT sc.id, sc.code, sc.subject, sc.priority FROM ServiceCall sc WHERE sc.priority = 'HIGH'"
  }
}
```

`dtos` must list every DTO the CoreSQL statement uses, separated by `;`.

## Business Rule receiver

FSM has no webhook subscription API. Instead, a **Business Rule** (Admin > Company > Business Rules) with a **Webhook** action calls any URL with your headers and body when a service call, activity or other object is created or changed. Point it at:

| Event | Path | Publishes on |
| - | - | - |
| `NotifyServiceCalls` | `POST /sapfsm/notify/service-calls` | `cmms/sapfsm/service-call-notification/received` |
| `NotifyActivities` | `POST /sapfsm/notify/activities` | `cmms/sapfsm/activity-notification/received` |
| `NotifyEvents` | `POST /sapfsm/notify/events` | `cmms/sapfsm/notification/received` |

> [!WARNING]
>
> Deploying the receiver opens port `9012`. FSM calls out from the SAP cloud, so put a reverse proxy or tunnel with a public TLS certificate in front of it, allow SAP's FSM egress addresses and check a shared secret header there: the connector does not authenticate callers.

## Connector

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Coreflux REST_API template — SAP Field Service Management (SAP Field Service and Asset Management) (beta): get an
#   OAuth 2.0 token, create service calls from alarms (Data API v4 ServiceCall.27), upsert them by your own alarm id,
#   find / read / patch service calls, activities (Activity.43) and equipment (Equipment.24, e.g. run hours as a UDF),
#   run read-only CoreSQL queries (Query API v1), create a service call with activities in one call and run the
#   Service Management API v2 business actions (cancel / technically complete service calls, release / close / cancel
#   activities).
# Hugo catalog #172, Tier 3 (cmms). Sources: SAP Help Portal FSM Data API, Data Model, Query API, Service API, OAuth
#   and API Quick Start Guide pages plus the SAP/fsm-sdk source (cached in openapi-cache/sap-field-service-management/;
#   the Business Accelerator Hub specs need an SAP login). Credentials are placeholders only.
# Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-field-service-management
#
# Host: your cluster, https://YOUR_CLUSTER.fsm.cloud.sap with YOUR_CLUSTER = eu | de | us | au (China:
#   https://cn.fsm.sapcloud.cn). The older eu.coresuite.com / de.coresystems.net hosts are being migrated to
#   fsm.cloud.sap. Use the cluster_url your account shows in Account Admin.
# Auth: OAuth 2.0 client credentials. GetToken posts grant_type=client_credentials ({value.json.form}) to
#   /api/oauth2/v2/token with Authorization: Basic {value.json.basic} (base64 of client_id:client_secret; keep it
#   in the Hub secret SAP_FSM_BASIC_AUTH). Every other event sends Authorization: Bearer {value.json.access_token}
#   plus X-Account-ID {value.json.account_id} and X-Company-ID {value.json.company_id} (numeric, from Account Admin)
#   and X-Client-ID custom-coreflux-broker / X-Client-Version 1.0. Tokens expire (expires_in): call GetToken again.
#   Live (2026-10-06, eu): fake Basic -> 401 {"error":"unauthorized","error_description":"Bad credentials"};
#   Data API without a token -> 401 INVALID_LOGIN; fake Bearer -> 401 invalid_token.
# Objects: Data API v4 DTO versions pinned to the newest on the SAP Help Portal on 2026-10-06 (ServiceCall.27,
#   Activity.43, Equipment.24; SAP/fsm-sdk 4.0.1 still ships 26 / 39 / 23). Status, type, origin and priority codes
#   are company-configurable: read them from an existing service call first.
# Payload fields (control fields stay out of bodies; request bodies come from {value.json.body}):
#   access_token, account_id, company_id, id, external_id, query, page, page_size, force_update, dtos, basic, form, body
#   query is a URL-encoded Data API condition (lastChanged%3E%3D1759737600000 = lastChanged>=...). id / external_id
#   go into the path: pass FSM ids or external ids you trust, never raw user input.
#     CreateServiceCall    body = {"subject": "...", "priority": "HIGH", "businessPartner": "<id>", "equipments": ["<id>"]}
#     UpdateServiceCall    id = <ServiceCall id>, force_update = "false", body = {"lastChanged": <from GET>, "priority": "LOW"}
#     UpsertEquipmentByExternalId  external_id = <ERP equipment no>, body = {"udfValues": [{"meta": {"externalId": "RUN_HOURS"}, "value": "1287.5"}]}
#     Query                dtos = "ServiceCall.27", body = {"query": "SELECT sc.id, sc.subject FROM ServiceCall sc"}
# Errors: 401 (token), 403 (permission group), 404, 409 / CA-* codes (e.g. stale lastChanged); Data API pages are
#   limited to 1000 objects and 100,000 objects per pagination. Writes run the company's Business Rules and sync to ERP.
#

DEFINE ROUTE SapFsmApi WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://YOUR_CLUSTER.fsm.cloud.sap"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '30'
        WITH DEFAULT_CONTENT_TYPE "application/json"
        # Smoke: GetToken -> FindEquipment -> CreateServiceCall -> GetServiceCall -> CreateActivity -> UpdateEquipment -> CompleteServiceCall. Never commit secrets or tokens.
    # Get an OAuth 2.0 access token (client credentials, HTTP Basic with the FSM OAuth client id:secret). Reply access_token, token_type, expires_in; send access_token on every other event
    ADD EVENT GetToken
        WITH SOURCE_TOPIC "cmms/sapfsm/token/create"
        WITH DESTINATION_TOPIC "cmms/sapfsm/token/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/oauth2/v2/token"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Basic {value.json.basic}"
            ADD "Content-Type" "application/x-www-form-urlencoded"
        WITH BODY "{value.json.form}"
    # Service calls, paged (default 20 per page, keep pageSize at 1000 or less). Reply data[].serviceCall plus currentPage / lastPage
    ADD EVENT ListServiceCalls
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/list"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/list/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27&page={value.json.page}&pageSize={value.json.page_size}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # Service calls matching a URL-encoded Data API query condition, e.g. lastChanged>=<epoch ms> for incremental polls
    ADD EVENT FindServiceCalls
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/find"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/find/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27&query={value.json.query}&pageSize={value.json.page_size}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # One service call by cloud id
    ADD EVENT GetServiceCall
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/get"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/ServiceCall/{value.json.id}?dtos=ServiceCall.27"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # One service call by your externalId (e.g. the alarm id)
    ADD EVENT GetServiceCallByExternalId
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/get-by-external-id"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/get-by-external-id/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/ServiceCall/externalId/{value.json.external_id}?dtos=ServiceCall.27"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # WRITE: Create a service call from an alarm; reply 201 {data:[{serviceCall:{id, code, lastChanged...}}]}
    ADD EVENT CreateServiceCall
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/create"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Patch service call fields; send the current lastChanged (optimistic lock) or force_update true. Reply 201
    ADD EVENT UpdateServiceCall
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/update"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/api/data/v4/ServiceCall/{value.json.id}?dtos=ServiceCall.27&forceUpdate={value.json.force_update}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Create or update the service call keyed by your externalId (one service call per alarm id; creating needs all mandatory fields)
    ADD EVENT UpsertServiceCallByExternalId
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/upsert"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/upsert/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/api/data/v4/ServiceCall/externalId/{value.json.external_id}?dtos=ServiceCall.27"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # Activities matching a URL-encoded Data API query condition
    ADD EVENT FindActivities
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/find"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/find/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/Activity?dtos=Activity.43&query={value.json.query}&pageSize={value.json.page_size}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # One activity by cloud id (status, executionStage, responsibles, object -> service call)
    ADD EVENT GetActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/get"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/Activity/{value.json.id}?dtos=Activity.43"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # WRITE: Create an activity under a service call (object = {objectId, objectType SERVICECALL})
    ADD EVENT CreateActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/create"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/data/v4/Activity?dtos=Activity.43"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Patch activity fields (send lastChanged or force_update true). Use the Service Management actions to change planning state
    ADD EVENT UpdateActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/update"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/api/data/v4/Activity/{value.json.id}?dtos=Activity.43&forceUpdate={value.json.force_update}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # Equipment matching a URL-encoded query condition (e.g. serialNumber='...')
    ADD EVENT FindEquipment
        WITH SOURCE_TOPIC "cmms/sapfsm/equipment/find"
        WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/find/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/Equipment?dtos=Equipment.24&query={value.json.query}&pageSize={value.json.page_size}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # One equipment by cloud id (read lastChanged before a PATCH)
    ADD EVENT GetEquipment
        WITH SOURCE_TOPIC "cmms/sapfsm/equipment/get"
        WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/Equipment/{value.json.id}?dtos=Equipment.24"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # One equipment by externalId (usually the S/4HANA or ERP equipment number)
    ADD EVENT GetEquipmentByExternalId
        WITH SOURCE_TOPIC "cmms/sapfsm/equipment/get-by-external-id"
        WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/get-by-external-id/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/data/v4/Equipment/externalId/{value.json.external_id}?dtos=Equipment.24"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
    # WRITE: Patch equipment with machine data (remarks, status, UDF values). force_update true skips the lastChanged lock
    ADD EVENT UpdateEquipment
        WITH SOURCE_TOPIC "cmms/sapfsm/equipment/update"
        WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/update/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/api/data/v4/Equipment/{value.json.id}?dtos=Equipment.24&forceUpdate={value.json.force_update}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Patch equipment by externalId, e.g. a run-hours UDF referenced by its UDF meta externalId
    ADD EVENT UpsertEquipmentByExternalId
        WITH SOURCE_TOPIC "cmms/sapfsm/equipment/upsert"
        WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/upsert/response"
        WITH METHOD "PATCH"
        WITH ENDPOINT "/api/data/v4/Equipment/externalId/{value.json.external_id}?dtos=Equipment.24"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # Read-only CoreSQL query (POST with {"query": ...}); dtos lists every DTO used, separated by ;
    ADD EVENT Query
        WITH SOURCE_TOPIC "cmms/sapfsm/query/run"
        WITH DESTINATION_TOPIC "cmms/sapfsm/query/run/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/query/v1?dtos={value.json.dtos}&page={value.json.page}&pageSize={value.json.page_size}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Service Management API composite tree: create a service call and its activities in one request
    ADD EVENT CreateServiceCallWithActivities
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call-tree/create"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call-tree/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/composite-tree/service-calls"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Business action: cancel a service call (not a delete)
    ADD EVENT CancelServiceCall
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/cancel"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/cancel/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/servicecalls/{value.json.id}/actions/cancel"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Business action: mark a service call technically complete
    ADD EVENT CompleteServiceCall
        WITH SOURCE_TOPIC "cmms/sapfsm/service-call/technically-complete"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/technically-complete/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/servicecalls/{value.json.id}/actions/technically-complete"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Business action: release a planned activity to the technician
    ADD EVENT ReleaseActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/release"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/release/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/release"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Business action: close an activity
    ADD EVENT CloseActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/close"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/close/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/close"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    # WRITE: Business action: cancel an activity (not a delete)
    ADD EVENT CancelActivity
        WITH SOURCE_TOPIC "cmms/sapfsm/activity/cancel"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity/cancel/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/cancel"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
            ADD "X-Account-ID" "{value.json.account_id}"
            ADD "X-Company-ID" "{value.json.company_id}"
            ADD "X-Client-ID" "custom-coreflux-broker"
            ADD "X-Client-Version" "1.0"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
```

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# SAP FSM Business Rule receiver — REST_API route (server mode, beta, companion of SapFieldServiceManagementRestApi.lot)
#   Receives the HTTP calls that an SAP Field Service Management Business Rule makes with its 'Webhook' action
#   (Admin > Company > Business Rules: trigger on create / update of a ServiceCall, Activity, Equipment ..., action
#   Webhook, method POST, your URL, header + JSON body you define) and publishes the body on MQTT, so changes made
#   in FSM reach the broker without polling. Opt-in: deploying it opens an HTTP port.
# Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-field-service-management
#
# FSM calls out from the SAP cloud: put a reverse proxy or tunnel with a public TLS certificate in front of
#   SERVER_PORT, allow the SAP FSM cloud egress addresses ('Incoming firewall configuration from SAP Field Service
#   Cloud') and check a shared secret header there: this route does not authenticate callers. The body is
#   whatever your Business Rule sends (e.g. ${serviceCall} variables); treat it as JSON you defined.

DEFINE ROUTE SapFsmBusinessRuleReceiver WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH ENABLE_SERVER "true"
        WITH SERVER_PORT '9012'
        WITH DEFAULT_CONTENT_TYPE "application/json"
    # Business Rule Webhook action (POST https://<public host>/sapfsm/notify/service-calls) when a service call is created or changes
    ADD EVENT NotifyServiceCalls
        WITH ENDPOINT "/sapfsm/notify/service-calls"
        WITH METHOD "POST"
        WITH DESTINATION_TOPIC "cmms/sapfsm/service-call-notification/received"
    # Same for activities (assignment, release, checkout, close)
    ADD EVENT NotifyActivities
        WITH ENDPOINT "/sapfsm/notify/activities"
        WITH METHOD "POST"
        WITH DESTINATION_TOPIC "cmms/sapfsm/activity-notification/received"
    # Any other Business Rule webhook body (equipment, checklists). Filter on the body in a LoT action
    ADD EVENT NotifyEvents
        WITH ENDPOINT "/sapfsm/notify/events"
        WITH METHOD "POST"
        WITH DESTINATION_TOPIC "cmms/sapfsm/notification/received"
```

## Example actions

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Example actions for the SapFsmApi route (docs page). Not part of the route template.
# Secrets: SAP_FSM_BASIC_AUTH (base64 of <OAuth client id>:<client secret>, from Account Admin > Clients),
#   SAP_FSM_ACCOUNT_ID and SAP_FSM_COMPANY_ID (numeric ids from Account Admin). The access token is kept on the
#   internal topic internal/sapfsm/access_token. Replace the <...> placeholders (business partner, equipment ids).

DEFINE ACTION SapFsmGetToken
ON EVERY 30 MINUTES DO
    SET "basic" WITH GET SECRET "SAP_FSM_BASIC_AUTH"
    PUBLISH TOPIC "cmms/sapfsm/token/create" WITH { "basic": {basic}, "form": "grant_type=client_credentials" }

DEFINE ACTION SapFsmKeepToken
ON TOPIC "cmms/sapfsm/token/create/response" DO
    SET "tok" WITH (GET JSON "access_token" IN PAYLOAD AS STRING)
    KEEP TOPIC "internal/sapfsm/access_token" WITH {tok}

DEFINE ACTION SapFsmServiceCallFromAlarm
ON TOPIC "porto/line1/compressor/alarm/raised" DO
    SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING
    SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID"
    SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID"
    SET "alarm" WITH (GET JSON "alarm" IN PAYLOAD AS STRING)
    SET "value" WITH (GET JSON "value" IN PAYLOAD AS STRING)
    SET "subj" WITH "Coreflux alarm " + {alarm} + " (value " + {value} + ")"
    PUBLISH TOPIC "cmms/sapfsm/service-call/create" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "body": { "subject": {subj}, "priority": "HIGH", "businessPartner": "<BusinessPartner id>", "equipments": ["<Equipment id>"] } }

DEFINE ACTION SapFsmRunHoursToEquipment
ON TOPIC "porto/line1/press01/run-hours" DO
    SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING
    SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID"
    SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID"
    SET "hours" WITH PAYLOAD AS STRING
    PUBLISH TOPIC "cmms/sapfsm/equipment/upsert" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "external_id": "<ERP equipment number>", "body": { "udfValues": [ { "meta": { "externalId": "RUN_HOURS" }, "value": {hours} } ] } }

DEFINE ACTION SapFsmPollChangedServiceCalls
ON EVERY 15 MINUTES DO
    SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING
    SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID"
    SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID"
    PUBLISH TOPIC "cmms/sapfsm/query/run" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "dtos": "ServiceCall.27", "page": "1", "page_size": "100", "body": { "query": "SELECT sc.id, sc.code, sc.subject, sc.priority, sc.statusName FROM ServiceCall sc WHERE sc.priority = 'HIGH'" } }
```

## Notes

* Writes run the company's Business Rules and, if connected, sync to SAP S/4HANA or ECC.
* Status, type, origin and priority codes and UDFs are company-specific: read an existing service call first and copy its codes.
* SAP's own `fsm-sdk` 4.0.1 still pins `ServiceCall.26`, `Activity.39`, `Equipment.23`. If your company rejects the newer versions, change the `dtos=` values in the endpoints.
* The Service API pages also list `X-Account-Name` and `X-Company-Name`. The connector sends only the ids, like SAP's SDK; add the name headers if a business action answers `400`.
* For Service Management calls the SAP Help pages use `/service-management/api/v2/...`; the SDK calls `/service-management/v2/...`. Both reach the API gateway; the connector uses the documented form.

## Not covered

Deletes, full PUT replacement, the Data API batch endpoint, attachments, planning actions that need technician or team ids (plan, replan, reschedule, unassign), bulk actions, approvals and other DTOs (business partners, persons, service assignments, time efforts, materials, checklists). All use the same headers; add events in the same style.

## Status

Technical preview, **blocked**. The template passes 357 of 357 smoke checks: static checks of both connectors against the SAP Help Portal pages and SAP's `fsm-sdk`, every event against a local mock of the FSM APIs, negative controls, and calls to the real SAP FSM hosts with fake credentials (all rejected with `401`). No call has run against a real SAP FSM company.

To finish testing, use an SAP FSM test company: set `SAP_FSM_CLUSTER`, `SAP_FSM_BASIC_AUTH`, `SAP_FSM_ACCOUNT_ID` and `SAP_FSM_COMPANY_ID`, run `python3 smoke_sap_fsm.py` (and `SAP_FSM_ALLOW_WRITE=1` to create one service call named "Coreflux smoke test (safe to delete)"), then run an MQTT round trip through the broker.

## Related

* [REST API connectors](/latest/lot-language/routes/rest-api-routes)
* [REST API app templates](/latest/lot-language/routes/rest-api-apps/index)
* [SAP FSM Data API v4](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_api/data-api-introduction.html)
* [SAP FSM Service API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_service_api/supported-requests.html)
* [SAP/fsm-sdk on GitHub](https://github.com/SAP/fsm-sdk)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.