# Jobs (jobs)

> Die jobs-Tabelle definiert, welche Daten wann und in welche Richtung synchronisiert werden.

---

Site [llms.txt](/llms.txt)

---

Ein **Job** beschreibt genau eine Synchronisationsrichtung zwischen zwei Systemen:
er verbindet eine Quelltabelle mit einer Zieltabelle, legt den Zeitplan fest und
verweist auf das zugehörige Mapping.

---

## Tabellenstruktur

| Spalte | Pflicht | Beschreibung |
|---|:---:|---|
| **PartitionKey** | ✅ | Muss immer `job` lauten. |
| **RowKey** | ✅ | Eindeutiger Jobname. Wird im Logging und als Referenz für ResponseJobs verwendet. |
| **IsActive** | ✅ | `true` = Job wird von `GetChanges` ausgeführt. `false` = Job wird nur indirekt (als ResponseJob) aufgerufen. |
| **SourceConnection** | ✅ | RowKey-Referenz auf einen Eintrag in der `connections`-Tabelle (Quelldaten). |
| **TargetConnection** | ✅ | RowKey-Referenz auf einen Eintrag in der `connections`-Tabelle (Zieldaten). |
| **Mapping** | ✅ | PartitionKey-Referenz auf eine Gruppe von Einträgen in der `mappings`-Tabelle. |
| **Source** | — | Quellressource (Tabelle, Entität, Pfad oder Aktion). Überschreibt die `SourceTable` der Mapping-Zeilen für diesen Job (siehe [unten](#source-und-target--job-eigene-ressourcenpfade)). |
| **Target** | — | Zielressource (Tabelle, Entität, Pfad oder Aktion). Überschreibt die `TargetTable` der Mapping-Zeilen für diesen Job (siehe [unten](#source-und-target--job-eigene-ressourcenpfade)). |
| **Order** | ✅ | Ganzzahl. Legt die Ausführungsreihenfolge fest — muss über **alle** Jobs eindeutig sein. |
| **WatermarkField** | — | Feldname im **Quellsystem**, der als Änderungsindikator genutzt wird (meistens ein Datums-/Zeitstempelfeld). Nur bei `WatermarkType` = `date` oder `datetime` erforderlich; bei `string` (Dataverse Change Tracking) bleibt das Feld leer. |
| **WatermarkType** | ✅ | Typ des Watermark-Wertes: `date`, `datetime` oder `string` (Details: [Watermarks](../watermarks/)). |
| **Filter** | — | Filterausdruck für die Quellanfrage. Format ist systemabhängig (siehe unten). |
| **Schedule** | — | CRON-Ausdruck (6-stellig, NCrontab). Wenn leer, läuft der Job bei jedem Timer-Aufruf. |
| **Action** | — | Steuert, welche Änderungsarten gelesen werden, oder benennt eine Aktion. Leer = `NewUpdate`. **Wirkt bei den CRUD-Werten (`Delete`, `DeleteOnUpdate`) nur bei D365CE als Quellsystem** (siehe unten); ein nicht erkannter Wert schaltet den Job auf `InvokeAction` (siehe unten). |
| **Priority** | — | `Standard` (Vorgabe), `High` oder `Low`. Steuert die Service-Bus-Queue. |
| **ResponseJob** | — | RowKey eines anderen Jobs, der nach erfolgreichem Schreiben automatisch ausgeführt wird. |
| **IsValid** | — | Reserviert. Die Spalte kommt in bestehenden Tabellen vor, wird von der DataBridge aber nicht gelesen und hat **keine Wirkung**. |

> **PartitionKey ist nicht frei wählbar**
>
> Die DataBridge liest die Tabelle ausschließlich mit dem Filter `PartitionKey eq 'job'`.
> Zeilen mit einem abweichenden PartitionKey werden **ohne Fehlermeldung und ohne Warnung
> ignoriert** — der Job existiert dann für die DataBridge schlicht nicht.
> 
> Dasselbe gilt für die `connections`-Tabelle (dort `connection`). Nur bei `mappings` ist
> der PartitionKey frei wählbar — er dient dort der Gruppierung.



---

## Ausführungsreihenfolge

Das Feld `Order` bestimmt, in welcher Reihenfolge die fälligen Jobs bei jedem Timer-Aufruf
abgearbeitet werden. Empfehlung: BC-Jobs vor CE-Jobs ausführen, um sicherzustellen,
dass IDs aus BC schon in Dataverse existieren, bevor sie referenziert werden.

```
Order 10: BC → CE (Stammdaten, z. B. Kunden)
Order 20: BC → CE (Bewegungsdaten, z. B. Angebote)
Order 30: CE → BC (Rückmeldungen)
```

Der `Order`-Wert muss über **alle** Jobs eindeutig sein — auch über inaktive ResponseJobs
hinweg. Ein doppelter Wert führt dazu, dass die betroffenen Jobs als unvollständig
markiert und nicht ausgeführt werden.

---

## Filter-Syntax

Der Filter bestimmt, welche Datensätze aus der Quelle abgerufen werden.

### D365BC, D365NAV und SQL — Filter mit Watermark-Platzhalter

Der Filterausdruck wird als Bedingung in die Quellanfrage eingesetzt. Zwei Elemente
steuern das Verhalten:

- `%watermark%` wird zur Laufzeit durch den gespeicherten Watermark-Wert ersetzt
- `{{ … }}` klammert einen Block, der **komplett entfällt**, wenn die Watermark leer ist

```
{{lastModifiedDateTime gt %watermark%}}
```

Ist die Watermark leer (Erstlauf oder manuell geleert), verschwindet der gesamte Block
und der Job liest den vollständigen Bestand. Genau dafür ist die Klammerung gedacht.

> **Die Position im Filter ist egal — die Klammerung entscheidet**
>
> Es kommt **nicht** darauf an, dass der Watermark-Vergleich zuerst steht. Ausgewertet wird
> allein die `{{ }}`-Klammerung: Jeder Block, der `%watermark%` enthält, wird bei leerer
> Watermark ersatzlos entfernt. Mehrere Blöcke sind erlaubt.
> 
> Daraus folgt eine Falle beim Kombinieren von Bedingungen:
> 
> ```
> {{lastModifiedDateTime gt %watermark%}} and {{blocked eq false}}
> ```
> 
> Bei leerer Watermark entsteht daraus `" and blocked eq false"` — ein **ungültiger
> Filterausdruck**, der die Abfrage gegen das Quellsystem scheitern lässt.



**Richtig kombiniert** wird, indem die Zusatzbedingung außerhalb der Klammer steht und
das `and` mit in den Watermark-Block wandert:

```
blocked eq false {{and lastModifiedDateTime gt %watermark%}}
```

| Watermark | Ergebnis |
|---|---|
| gesetzt | `blocked eq false and lastModifiedDateTime gt 2026-08-01T10:00:00.000Z` |
| leer | `blocked eq false` |

Beide Varianten sind gültig — der Erstlauf liest den vollen Bestand, gefiltert auf
`blocked eq false`.

### D365CE / Dataverse — JSON-Operationen

Dataverse nutzt die Change Tracking API; ein OData-Filter kommt dort nicht zum Einsatz.
Stattdessen erwartet das `Filter`-Feld ein **JSON-Objekt**, das nach dem Abruf
clientseitig ausgewertet wird — passende Datensätze werden verarbeitet, alle anderen
verworfen.

```json
{ "Operations": [ { "Name": "NotNull", "Parameters": { "SourceField": "wysa_number" } } ] }
```

```json
{ "Operations": [ { "Name": "StatusCode", "Parameters": { "Value": 3 } } ] }
```

| Operation | Parameter | Wirkung |
|---|---|---|
| `Equals` | `SourceField`, `Value` | Feldwert entspricht dem angegebenen Wert |
| `Boolean` | `SourceField`, `Value` | Boolesches Feld entspricht dem angegebenen Wert |
| `OptionSetValue` | `SourceField`, `Value` | Optionsset-Feld entspricht dem angegebenen Wert |
| `NotNull` | `SourceField` | Feld ist gefüllt |
| `IsNull` | `SourceField` | Feld ist leer |
| `StatusCode` | `Value` | Datensatz hat den angegebenen Statuscode |

Ein nicht aufgeführter Operationsname führt zu einem Fehler
(`Undefined FilterOperation`). Die unter `SourceField` genannten Felder werden
automatisch mit abgefragt — sie müssen nicht zusätzlich im Mapping stehen.

---

## Schedule — jobbezogener CRON-Ausdruck

Wenn ein Job seltener als der globale Timer laufen soll, kann ein eigener
CRON-Ausdruck eingetragen werden. Die DataBridge prüft anhand von `LastCronRun` im
Watermark, ob das Intervall bereits abgelaufen ist.

Beispiele:

| Schedule | Bedeutung |
|---|---|
| `0 */2 * * * *` | Alle 2 Minuten |
| `0 */15 * * * *` | Alle 15 Minuten |
| `0 0 * * * *` | Stündlich |
| `0 0 6 * * *` | Täglich um 06:00 Uhr |
| *(leer)* | Greift bei jedem Timer-Aufruf |

Der globale Timer (`GetChangesTimerExpression`) steht bei einer Neuinstallation auf
`*/15 * * * * *` — also alle 15 Sekunden. Ein Job ohne eigenen `Schedule` wird damit bei
jedem dieser Läufe geprüft.

---

## Priorität

| Wert | Service-Bus-Queue | Typischer Einsatz |
|---|---|---|
| `High` | `sbqhighexport` | Zeitkritische Stammdaten (Preise, Kundenstatus) |
| `Standard` | `sbqexport` | Normalbetrieb (Vorgabe, auch bei leerem oder unbekanntem Wert) |
| `Low` | `sbqlowexport` | Große Historien-Importe, Bulk-Daten |

Jede der drei Queues hat einen eigenen, gleichrangigen Verarbeiter. Sie laufen unabhängig
und parallel — dadurch bleibt eine hochpriore Nachricht nicht hinter einem Rückstau
niedrigerer Priorität stehen. Eine echte Vorrang-Reihenfolge zwischen den Queues gibt es
nicht.

> **Weitere Queues sind rein intern**
>
> Der Service Bus enthält vier weitere Queues (`sbqignored`, `sbqtransitivehigh`,
> `sbqtransitivestandart`, `sbqtransitivelow`). Sie sind Ziele der internen
> Fehlerbehandlung — für übersprungene bzw. bei Konflikten im Zielsystem zurückgestellte
> Nachrichten — und **nicht** als Wert für `Priority` gedacht. Details:
> [Architektur → Queue-Struktur](../../ueberblick/architektur/#queue-struktur-im-service-bus).



---

## Action — welche Änderungsarten gelesen werden

| Wert | Wirkung |
|---|---|
| *(leer)* / `NewUpdate` | Nur Neuanlagen und Änderungen (Vorgabe, auch bei unbekanntem Wert) |
| `Delete` | Nur Löschungen |
| `DeleteOnUpdate` | Löschungen, die im Quellsystem als Änderung auftreten |
| `All` | Neuanlagen, Änderungen und Löschungen in einem Job |
| *ein beliebiger anderer Wert* | Der Job wird als **Aktions-Job** (`InvokeAction`) behandelt — siehe unten |

> **CRUD-Werte wirken nur bei D365CE als Quellsystem**
>
> Die Werte `Delete`, `DeleteOnUpdate` und `All` werden ausschließlich vom Dataverse-Connector
> ausgewertet. Steht als Quelle ein D365BC-, D365NAV- oder SQL-System, haben sie **keine
> Wirkung** — der Job liest dort unabhängig vom gesetzten Wert immer Neuanlagen und
> Änderungen.



### InvokeAction — Aktions-Jobs

*(neu)* Enthält `Action` weder einen leeren Wert noch den Namen eines bekannten
`JobActionType` (`NewUpdate`, `Delete`, `TimeStamp`, `DeleteOnUpdate`), wird der Job zu
einem **Aktions-Job**: Statt eines CRUD-Zyklus ruft er einen benannten, nicht-CRUD-Endpunkt
auf. Der Wert von `Action` wird dabei unverändert als Aktionsname übernommen — z. B. ein
OpenAPI-`operationId` oder der Name eines CE-`OrganizationRequest`.

```
Action = SendByEInvoice
```

Aktuell implementiert der [OpenAPI-Konnektor](../../konnektoren/openapi/) `InvokeAction`
(`ResourcePath` aus `Source`/`Target` bzw. `TargetPath` + `TargetTable` der Mapping-Zeilen).
Details und Konfigurationsbeispiel: [Konnektoren → OpenAPI → InvokeAction](../../konnektoren/openapi/#invokeaction--generische-aktionsaufrufe).

---

## Source und Target — job-eigene Ressourcenpfade {#source-und-target--job-eigene-ressourcenpfade}

*(neu)* Bislang bestimmten ausschließlich die Mapping-Zeilen (`SourceTable`/`TargetTable`),
welche Ressource ein Job liest bzw. beschreibt. Ein Job kann jetzt zusätzlich eigene Felder
`Source` und `Target` tragen, die diese Werte **überschreiben**:

- Sind `Source`/`Target` am Job leer, gilt weiterhin das bisherige Verhalten — die
  Mapping-Zeilen bestimmen die Ressource (mit einer einmaligen Warnung im Log, die auf die
  ältere Konfigurationsform hinweist).
- Ist mindestens eines der beiden Felder gesetzt, wird nur dieses überschrieben; das jeweils
  andere fällt weiterhin auf den Mapping-Wert zurück.
- Fehlt sogar das `Mapping`-Feld am Job komplett, wird aus `Source`/`Target` ein
  **synthetisches Mapping** ohne `TargetKey` erzeugt (`JOB-<Jobname>`) — nützlich für reine
  Aktions-Jobs ohne Feld-Mapping.
- Enthält `Source`/`Target` einen Schrägstrich (`pfad/ressource`), wird der Teil vor dem
  letzten Schrägstrich als Pfad (`SourcePath`/`TargetPath`), der Teil danach als
  Ressourcenname (`SourceTable`/`TargetTable`) interpretiert.

```
Source = v2/clients
Target = v2/client_invoices/{id}/send_by_einvoice
```

Damit lässt sich ein und dieselbe Mapping-Definition für mehrere Ressourcen bzw.
Aktionspfade wiederverwenden, statt für jede Kombination eine eigene Mapping-Gruppe
anzulegen.

---

## ResponseJob — Rückschreiben von IDs

Ein ResponseJob löst das Problem, dass Business Central beim Anlegen eines Datensatzes
eine neue interne ID vergibt, die anschließend in Dataverse gespeichert werden soll.

**Ablauf:**

1. Job A schreibt einen Datensatz von CE nach BC
2. BC antwortet mit dem neu erzeugten Datensatz inkl. der neuen BC-ID
3. Die DataBridge führt automatisch **Job B** (den ResponseJob) aus
4. Job B liest die BC-ID aus der Antwort (`ReadFromMessage`-Operation) und schreibt
   sie zurück nach CE

**Konfiguration:**

```
Job A:  IsActive = true
        ResponseJob = "JobB-RowKey"

Job B:  IsActive = false   ← wird nur durch Job A ausgelöst, nie direkt
        SourceConnection = BC-Verbindung
        TargetConnection = CE-Verbindung
```

Felder, die aus der BC-Antwort gelesen werden, werden im Mapping mit einem
Unterstrich-Präfix im `SourceField` markiert und die Operation `ReadFromMessage`
eingetragen (Details: [Mappings → ReadFromMessage](../mappings/#readfrommessage)).
