Integrations
Wayd talks to external systems through two layers:
- Connectors are customer-configured at runtime via Settings → System → Connections. Each connection stores its own credentials (encrypted at rest) and runs against the target system on the customer's behalf.
- Integration libraries (
Wayd.Integrations.*) are the low-level clients those connectors use. There are no always-on, configuration-driven integrations — every external system Wayd pulls data from is a connector you create in the UI.
Connectors
Connectors are managed from Settings → System → Connections. The set of supported connector types is defined in code; adding one is described in the Architecture guide.
| Connector | Category | Status | Purpose |
|---|---|---|---|
| Azure DevOps | Work sync | GA | Syncs work items, processes, workspaces, and teams from Azure DevOps into Wayd. |
| Entra | People sync | GA | Syncs people (employees, contingent workers) from Microsoft Entra ID via Microsoft Graph into Wayd's Employee records. |
| Workday | People sync | GA | Syncs workers (employees, contingent workers) from a Workday tenant via the Staffing SOAP web service into Wayd's Employee records. |
| Azure OpenAI | AI provider | Preview | Stores an Azure OpenAI endpoint, deployment, and API key. No data is synced, and no Wayd feature calls it yet. |
The Connector enum's value 2 belonged to an OpenAI connector that was removed before it shipped; the value is retired and must not be reused.
Cross-cutting concerns
These behaviors apply to every connector — the per-connector pages link back here rather than repeat them.
Capabilities drive orchestration
Each connector declares the ConnectorCapability values it supports (a multi-surface connector like GitHub can support several through one connection). The capability — not the specific connector type — determines which background runner picks the connection up:
WorkItems("Work Items") →WorkSyncRunner(background jobs Work Full Sync / Work Differential Sync,BackgroundJobType.WorkFullSync/WorkDiffSync).People→PeopleSyncRunner(background jobs People Full Sync / People Differential Sync,BackgroundJobType.PeopleFullSync/PeopleDiffSync). Connectors that don't support incremental fall back to Full transparently.AiProvider("AI Provider") → not synced.
The global jobs run every active connection with the capability. No sync job is scheduled by default: an admin creates the recurring schedule under Settings → System → Background Jobs, or runs a job on demand from there.
The detail page's Sync History section dispatches by capability too: its Diff Sync and Full Sync buttons enqueue a sync of that one connection (POST /api/app-integrations/connections/{id}/run?syncType=…), routed by the connector's capabilities. The connection must be active. A future BambooHR / Okta / Jira connector just declares the right ConnectorCapability values and gets the same plumbing. Each capability also carries a display category (the enum's Display(GroupName): "Work Management", "People", "AI Provider") that groups the connector picker.
Only one People Sync connection may be active at a time
Across all PeopleSync connectors (Entra, Workday, future BambooHR/etc.), at most one connection can be IsActive=true simultaneously. Activating a People Sync connection while another is already active is rejected by ActivateConnectionCommand ("Another PeopleSync connection (…) is already active. Deactivate it before activating this one."). This is a guardrail against the same person becoming two Employee rows.
Multiple People Sync connections may exist — admins can stage one, configure it fully, and only flip the active flag during the cutover. The single-active rule is checked only at activation time, and creation does not check it: a new connection starts active, so creating a second People Sync connection while one is active leaves two active. Deactivate the new connection straight after creating it when staging a cutover. While two are active, the global People sync jobs refuse to run (PeopleSyncRunner fails the run with "Multiple active PeopleSync connections found"); a per-connection sync from the detail page still runs.
Work Sync has no equivalent limit; multiple Azure DevOps / future Jira connections can be active concurrently. Each Azure DevOps organization can be linked by only one connection.
Credentials are encrypted at rest
Every credential field on a connector (PAT, API key, client secret, ISU password, future OAuth refresh tokens) is marked [Encrypted] and round-tripped through AES-256-GCM via the EncryptingJsonValueConverter before being written to the Connections table. The master key is configured via SecuritySettings:DataProtection:MasterKey — see the Configuration guide for setup and the key rotation caveat.
API responses never return a stored secret: ConnectionSecret.Masked replaces it with the fixed placeholder ******** (or an empty string when none is stored), so the response discloses neither the value nor its length. On edit, secret inputs start blank, and a blank or masked value submitted back means "keep the stored secret" (ConnectionSecret.Resolve) — a credential cannot be cleared, only replaced.
Activation
New connections are active by default. Admins toggle this with Activate / Deactivate in the detail page's actions menu — IsActive is the single switch that controls whether a connection participates in sync runs; there is no separate "sync enabled" flag. A connection syncs only when it is active, its configuration is valid (IsValidConfiguration), and — for Azure DevOps — at least one work process or workspace integration is active (ISyncableConnection.CanSync). Activation transitions raise ConnectionActivatedEvent / ConnectionDeactivatedEvent, so the deliberate state change is recorded on its own rather than bundled into a config edit.
Connection events
Every connector shares one family of events on Connection, all filed under the Connection aggregate: ConnectionCreatedEvent, ConnectionDetailsUpdatedEvent (name, description), ConnectionConfigurationChangedEvent, ConnectionCredentialsChangedEvent, ConnectionActivatedEvent, ConnectionDeactivatedEvent and ConnectionDeletedEvent, plus ConnectionBaselinedEvent for a connection that existed before creation was recorded. A connector describes its non-secret settings by overriding DescribeSettings(), as name/value pairs formatted invariantly in a fixed order; that list is what the created and configuration-changed events carry, before and after. It lists its credentials in Credentials(), which is compared in memory only — the credentials event names which ones were replaced and nothing else. A new connector implements both, and a setting it adds belongs in DescribeSettings() or it changes without a record.
State a connector syncs from its external system raises no event and is listed in EventCoverageTests.UneventedMutators: the Azure DevOps system id, workspaces, processes, teams and integration state, pending #956, and the Workday init-probe result (WorkdayConnection.RecordInitResult), which records a probe of the configuration rather than a change to it.
Sync history
Every sync run — manual or scheduled, per-connection or global — produces a SyncRun row with status, timestamps, and a per-run DetailsJson blob. SyncRuns has no foreign key to Connections, so history survives deleting the connection. The Sync History section on each connection's detail page lists runs from the last 24 hours, 7 days or 30 days and renders the JSON with a connector-aware schema when a run is expanded (People sync shows employees fetched, upserted and excluded, the per-rule exclusion breakdown, and errors[]; Work sync shows per-workspace counts of work items, parent and dependency link changes, deletions, revisions read and history periods written).
Individual records can be skipped without failing the run. People sync reconciles data from a system we don't control, so a record that can't be applied — an ambiguous identity match, a value longer than its column allows — is skipped and reported rather than aborting the whole reconciliation. The rest of the payload still syncs. Check the run's errors[] for the per-record reason; each names the offending employee so it can be corrected at the source.
People sync does not automatically retry. Its failures are overwhelmingly deterministic — constraint violations, malformed source data, revoked credentials — so a retry would rebuild the identical change set and fail identically while consuming upstream API quota. A failed People sync goes straight to the Failed state, where it stays visible under Settings → System → Background Jobs; fix the cause and trigger the sync again.
Work sync does still retry (3 attempts, backing off 30s/60s/120s), because Azure DevOps REST produces genuinely transient failures where a later attempt succeeds. Transient transport faults are handled lower down for Azure DevOps and Workday, by an HTTP resilience pipeline (retries, a 90-second per-attempt timeout and a 5-minute total), and never reach the job layer. Azure DevOps uses the host's default pipeline (ConfigureHttpClientDefaults); Workday registers its own in WorkdayConnectorModule, which skips retrying SOAP faults that can never succeed. Entra calls go through a GraphServiceClient built per sync, outside IHttpClientFactory, so neither pipeline applies.
Integration Architecture
Azure OpenAI is absent from the diagram because nothing calls it: Wayd.Integrations.AzureOpenAI contains no code yet. See Azure OpenAI.
Adding a New Connector
For sync-shaped or AI-provider connectors that customers configure in Settings → System → Connections:
- Add the connector type to
Connectorenum inWayd.Common.Domain.Enums.AppIntegrations - Declare its
ConnectorCapabilityvalues inConnectorExtensions.GetCapabilities()— the capabilities determine which runners pick it up (work-sync, people-sync, AI-provider); a multi-surface connector declares several - Create a domain configuration class (e.g.
JiraConnectionConfiguration) and connection aggregate (JiraConnection : Connection<JiraConnectionConfiguration>). Every sync connector also implementsISyncableConnection— both runners pick up only connections that implement it and reportCanSync— and itsHasActiveIntegrationObjectsdecides whether per-target integration state (workspaces, projects) must be active before it syncs. ImplementDescribeSettings()andCredentials()(see Connection events). Mark credential fields with[Encrypted]for at-rest encryption — see Architecture → Connector framework - Create command/query handlers under
Wayd.AppIntegration.Application/Connections/Commands/{Connector}/following the AzDO, Entra, Workday, and AOAI templates - Register the EF entity configuration in
Wayd.Infrastructure/src/Wayd.Infrastructure/Persistence/Configuration/AppIntegrationConfiguration.cs(add aHasValue<…>(Connector.X)to theConnectionsdiscriminator) and add a migration (the migration is typically a no-op when the connector reuses the sharedConnectionsTPH table — it just adds a discriminator value) - Add a
Wayd.Integrations.{SystemName}project for the low-level client (only if the connector actually talks to a remote system — pure AI providers can use an existing SDK directly) - For work-sync connectors: implement
IWorkItemSource(the port is inWayd.Integrations.Abstractions; the implementation goes inWayd.AppIntegration.Application/Connections/Managers/{Connector}WorkItemSource.cs) and anISyncableConnectionDescriptorBuilder. The genericWorkSyncRunnerpicks up every registered source automatically.GetWorkItemHistorymaps the system's revision history onto the connector-neutral contract; a connector without one returnsWorkItemHistoryBatch.Empty(watermark)— see Work item history. - For people-sync connectors: implement
IEmployeeSource(an adapter inWayd.AppIntegration.Application/Connections/Managers/{Connector}EmployeeSource.cswrapping your low-level service) and anISyncableConnectionDescriptorBuilder. DeclareSupportsIncrementalandMatchByon the source. The genericPeopleSyncRunnerpicks it up automatically — no runner edits. - Add the connector's module: one
IConnectorModuleclass inWayd.Infrastructure/src/Wayd.Infrastructure/ConnectorModules/declaring the connector's capabilities and registering everything from steps 6–8 (keyed sources, descriptor builder, optionalIConnectionInitProbe, HTTP clients). Modules are assembly-scanned at startup (ConnectorModuleExtensions.DiscoverModules);ConnectorModulesTestsinWayd.Infrastructure.Testsfails if the module is missing, its capabilities disagree withGetCapabilities(), or a declared sync capability lacks its keyed source. - On the frontend (
Wayd.Web/src/wayd.web.reactclient/src/), register the connector inapp/settings/connections/_components/connector-registry.ts(create/edit form),app/settings/connections/[id]/_components/detail-registry.tsx(detail page, including itsNAME_TO_TYPEentry), andtypes/connectors.ts(enum + display names). See Architecture → Connector framework for the full checklist.