Integration Guide
This document explains how external systems integrate with Xema Workflows, including webhook triggers, trigger payloads, external actions, and best practices.
Table of Contents
- Overview
- Single Ingress Edge Architecture
- Webhook Triggers
- Supported Providers
- Trigger Payloads
- Workflow Dispatch API
- External Actions
- Integration Patterns
- Security & Authentication
- Idempotency & Retry
Overview
How Integration Works
External systems trigger and interact with Xema Workflows through:
- Webhook triggers — GitHub, GitLab, Jira, Slack, custom webhooks
- API dispatch — Manual workflow triggers via REST API
- Schedule triggers — Cron-based automation
- Workflow calls — Workflows calling other workflows
Architecture Principle
Single Ingress Edge: All external provider webhooks flow through a single normalization layer (connector-gateway-api), which standardizes events into canonical envelope format before routing to domain services.
This ensures:
- ✅ Consistent webhook handling across providers
- ✅ Single security boundary
- ✅ Deterministic idempotency keys
- ✅ No direct webhook access to domain services
Single Ingress Edge Architecture
Flow Diagram
1External Provider Integration Adapter Workflow Engine
2(GitHub, GitLab, → (Normalize Event) → (Trigger Workflow)
3 Jira, Slack, ...)
4 ↓
5 Canonical Envelope
6 (provider, event, payload)
7 ↓
8 Idempotency Check
9 (provider:deliveryId)
10 ↓
11 POST /webhooks/provider-data
12 (Domain Service)
13 ↓
14 Workflow Match & Trigger
15 (Filter by projectId, branch, etc.)Benefits
- Normalization — All providers speak the same language
- Idempotency — Deterministic keys prevent duplicate execution
- Security — Single validation point
- Extensibility — Add new providers without touching domain services
- Auditability — Complete webhook history
Webhook Triggers
Declaring Webhook Triggers
Workflows declare which events they respond to:
1on:
2 webhook:
3 - event: scm.push
4 filters:
5 projectId: proj_123
6 branch: main
7 - event: scm.pull_request
8 filters:
9 projectId: proj_123
10 state: opened
11 - event: tracker.issue
12 filters:
13 projectId: proj_123
14 type: feature
15 - event: tracker.issue
16 filters:
17 projectId: proj_123
18 type: bug
19 priority: highEvent Filters
Webhooks are only triggered if all specified filters match:
1filters:
2 projectId: proj_123 # Project ID
3 branch: main # Branch name (SCM only)
4 state: opened # State (PR/issue)
5 type: feature # Issue type
6 priority: high # Priority levelAccessing Webhook Data
In workflow jobs, access the incoming webhook:
1jobs:
2 process-push:
3 uses: xema/agent
4 with:
5 provider: ${{ trigger.provider }} # e.g., "github"
6 event: ${{ trigger.event }} # e.g., "scm.push"
7 repo: ${{ trigger.payload.repo.name }}
8 branch: ${{ trigger.payload.branch }}
9 commit_sha: ${{ trigger.payload.commit.sha }}
10 commit_message: ${{ trigger.payload.commit.message }}
11 author: ${{ trigger.payload.commit.author }}Supported Providers
SCM Providers
GitHub
Events:
scm.push— Code pushed to repositoryscm.pull_request— Pull request opened/updated/closedscm.repository— Repository created/deletedscm.installation— GitHub App installed/uninstalled
Payload:
1{
2 "provider": "GITHUB",
3 "adapterKey": "SCM",
4 "event": "scm.push",
5 "payload": {
6 "repo": {
7 "name": "myrepo",
8 "owner": "acme",
9 "url": "https://github.com/acme/myrepo",
10 "isPrivate": false
11 },
12 "branch": "main",
13 "before": "abc123...",
14 "after": "def456...",
15 "commits": [...],
16 "pusher": "alice"
17 }
18}GitLab
Connector biome @xemahq/biomes-platform-gitlab. Covers GitLab-hosted repositories and their issues.
Events (canonical names the connector emits):
scm.push.received— a push. The payload carries the repository clone URL and full name,ref,branch(empty for non-branch refs),beforeSha,afterSha,commitCountandcommits.scm.change_request.opened/.updated/.merged/.closed— merge request events. The payload carries the title, description, state, source and target branch, head SHA and merge commit SHA when present.tracker.item.created— an issue is opened.tracker.item.updated— an issue is closed, reopened or updated.tracker.comment.added— a comment on an issue. Comments on merge requests, commits and snippets are not forwarded.
Other GitLab event kinds are received and dropped without starting a run.
Verification: GitLab sends the webhook secret you configure in the X-Gitlab-Token header; Xema compares it. There is no body signature. X-Gitlab-Event-UUID is the delivery id used for idempotency.
Install: the connector uses oauth_user with the read_api and read_repository scopes. An admin supplies the GitLab OAuth application ID and secret (an org can override the platform app). When installing the connector, pick the connected GitLab instance and at least one repository (project).
Actions: the connector also exposes repository operations as capabilities: connector:scm.get-repository@1, list-branches, list-files, get-file, get-diff, list-commits, get-checks, list-pull-requests, get-pull-request and list-pull-request-comments (all low risk), and create-branch, create-pull-request, update-pull-request and post-pull-request-comment, which are high risk and require approval before they run.
Gitea
Connector biome @xemahq/biomes-platform-gitea, for self-hosted Gitea instances.
Events:
scm.push.received— a push (same payload shape as GitLab above).scm.change_request.opened/.updated/.merged/.closed— pull request events. A pull request whose state is merged is reported asmerged; anopenedaction asopened;closedasclosed; any other action asupdated.
Other Gitea events are received and dropped. Issues and comments are not ingested.
Verification: Gitea signs the body with HMAC-SHA256 and sends the raw hex digest (no sha256= prefix) in X-Gitea-Signature. The event kind comes from X-Gitea-Event, and X-Gitea-Delivery is the delivery id.
Install: credential kind pat. An admin supplies the instance base URL (no trailing slash) and a personal access token with repo and write:repository scope. When installing the connector, pick the connected Gitea instance and at least one repository.
Azure DevOps
Connector biome @xemahq/biomes-platform-azure-devops, for Azure DevOps Services and Server.
Events (from service hooks):
scm.push.received—git.push(same payload shape as GitLab above).scm.change_request.opened—git.pullrequest.created;scm.change_request.merged—git.pullrequest.merged; forgit.pullrequest.updatedthe result ismerged,closedorupdateddepending on the pull request state.
Other service-hook event types are received and dropped.
Verification: the service hook uses HTTP Basic authentication. Xema compares the Authorization header with the credential pair stored on the connection, so configure the hook with the same username and token.
Install: credential kind basic. An admin supplies the organization URL (for example https://dev.azure.com/your-organization), a username, and a personal access token with at least Code (Read) scope. When installing the connector, pick the connected organization and at least one repository.
Issue Tracker Providers
Jira
Events:
tracker.issue— Issue created/updated/transitioned
Payload:
1{
2 "provider": "ATLASSIAN",
3 "adapterKey": "TRACKER",
4 "event": "tracker.issue",
5 "payload": {
6 "issue": {
7 "key": "PROJ-123",
8 "title": "Add user authentication",
9 "description": "...",
10 "type": "feature",
11 "status": "in-progress",
12 "priority": "high",
13 "assignee": "[email protected]",
14 "reporter": "[email protected]",
15 "labels": ["backend", "urgent"]
16 }
17 }
18}Confluence
Events:
doc.page.created,doc.page.updated,doc.page.deleted— page events. Confluencepage_createdmaps to created;page_trashedandpage_removedto deleted;page_updated,page_restoredand any other page or blog event to updated.doc.space.created,doc.space.updated— space events.
Atlassian connector setup
Jira and Confluence share one connector biome, @xemahq/biomes-platform-atlassian (connector key ATLASSIAN), and one webhook URL. Confluence pages and spaces are delivered as documentation events and Jira issues and comments as tracker events.
Jira events (beyond the issue event above): tracker.item.created, tracker.item.updated, tracker.item.deleted for jira:issue_created, jira:issue_updated and jira:issue_deleted, and tracker.comment.added for comment events. Other event names are received and dropped.
Verification: Atlassian signs the body with HMAC-SHA256 and sends X-Hub-Signature: sha256=<hex>. Atlassian sends no delivery id, so Xema derives the idempotency key from a hash of the body.
Install: credential kind basic. An admin supplies the Atlassian account email and an API token (created under Security, API tokens in the Atlassian account settings). When installing the connector, pick the connected Atlassian Cloud site and optionally restrict it to named Confluence space keys and Jira project keys.
Linear
Connector biome @xemahq/biomes-platform-linear, for Linear workspaces.
Events:
tracker.issue.created— an issue is created.tracker.issue.updated— an issue is updated.
The payload carries the Linear issue id, its identifier (for example ENG-123), the title and the workflow state name. Linear comment events and other entity types are received and dropped, and Linear does not emit a separate "transitioned" event: a state change arrives as tracker.issue.updated, and the new state is in the payload.
Verification: Linear signs each delivery and sends the signature in the linear-signature header. The linear-delivery header is the delivery id.
Install: credential kind oauth_user with read and write scopes. An admin supplies the Linear OAuth client ID and secret. When installing the connector, pick the connected workspace and at least one Linear project, and optionally limit it to named state transitions (the default is any).
Actions: the connector declares a create-issue-comment action (inputs issueId and body). In the current release it is a reference skeleton that does not yet write a comment in Linear, so do not rely on it in production workflows.
Communication Providers
Slack
Connector biome @xemahq/biomes-platform-slack. It feeds Slack messages into conversation channels, where agent sessions answer them, and delivers the replies back.
Events:
channel.message.received— a new message.channel.message.edited— an edited message.
The payload carries senderId, conversationId (the Slack channel), text, externalMessageId (the Slack timestamp) and, for thread replies, threadId. Reactions, channel joins, deleted messages, bot messages and other event types are received and dropped.
1{
2 "provider": "SLACK",
3 "event": "channel.message.received",
4 "payload": {
5 "senderId": "U12345678",
6 "conversationId": "C12345678",
7 "text": "Hey, start a workflow",
8 "externalMessageId": "1234567890.123456"
9 }
10}Verification: Slack signs each request with the app's signing secret (X-Slack-Signature with X-Slack-Request-Timestamp). A request whose timestamp is more than 5 minutes off is rejected. Slack's one-time URL verification handshake is answered by the gateway and does not start a run.
Outbound: connector:channel.send-message@1 with conversationId and block posts one reply to the channel.
Install: credential kind bot_token, with the channels:history, groups:history, im:history and chat:write scopes. An admin supplies the Slack app's client ID, client secret and signing secret, and installs the app to the workspace through Slack's OAuth flow. When installing the connector, pick the connected Slack workspace.
Trigger Payloads
Canonical Envelope Format
All webhooks are normalized to this format:
The base class is ConnectorWebhookEnvelopeDto<TPayload> in @xemahq/platform-common. Each owning domain service declares one concrete subclass per WebhookEntityKind.
1class ConnectorWebhookEnvelopeDto<TPayload = unknown> {
2 provider: string
3 // Curated providers use the ConnectorKind vocabulary — GITHUB, GITLAB,
4 // GITEA, XEMA_GITEA, AZURE_DEVOPS, ATLASSIAN, SLACK, TELEGRAM, ...
5 // The column is open so an installed biome can contribute its own.
6
7 adapterKey: AdapterKey
8 // SCM | TRACKER | DOCUMENTATION | ...
9
10 event: string
11 // Canonical event name (e.g. "scm.push")
12
13 entityKind: WebhookEntityKind
14 // Discriminator: scm.push, scm.pull_request, tracker.issue, ...
15
16 deliveryId: string
17 // Unique per webhook delivery — the idempotency input
18
19 occurredAt: string
20 correlationId: string
21 externalId?: string
22 externalSpaceKey?: string
23 connectionId?: string
24 // Which named connection this delivery arrived on
25
26 payload: TPayload
27 // Typed per entityKind
28}Idempotency Key
The workflow engine computes:
Idempotency-Key = {provider}:{deliveryId}
This ensures:
- Each webhook is processed exactly once
- Duplicate deliveries are detected and skipped
- Safe retry without side effects
Payload Examples
GitHub Push Event
1on:
2 webhook:
3 - event: scm.push
4
5jobs:
6 on-push:
7 uses: xema/agent
8 with:
9 repo_url: ${{ trigger.payload.repo.url }}
10 branch: ${{ trigger.payload.branch }}
11 commits: ${{ trigger.payload.commits }}
12 pusher: ${{ trigger.payload.pusher }}Jira Issue Event
1on:
2 webhook:
3 - event: tracker.issue
4 filters:
5 type: feature
6
7jobs:
8 on-issue:
9 uses: xema/agent
10 with:
11 issue_key: ${{ trigger.payload.issue.key }}
12 title: ${{ trigger.payload.issue.title }}
13 priority: ${{ trigger.payload.issue.priority }}
14 assignee: ${{ trigger.payload.issue.assignee }}Workflow Dispatch API
Manual Trigger via REST API
Trigger a workflow manually:
1curl -X POST https://workflow-engine-api.xema.dev/workflows/{slug}/dispatch \
2 -H "Authorization: Bearer $TOKEN" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "workflowId": "wf_123",
6 "inputs": {
7 "project_name": "Acme Portal",
8 "scope": "full",
9 "budget": 500000
10 }
11 }'Response
1{
2 "runId": "run_abc123",
3 "status": "pending",
4 "createdAt": "2026-04-27T14:30:00Z"
5}Polling for Results
1curl -X GET https://workflow-engine-api.xema.dev/runs/run_abc123 \
2 -H "Authorization: Bearer $TOKEN"Response:
1{
2 "runId": "run_abc123",
3 "workflowId": "wf_123",
4 "status": "running",
5 "jobs": [
6 {
7 "jobId": "job_1",
8 "status": "running"
9 }
10 ],
11 "startedAt": "2026-04-27T14:30:05Z"
12}External Actions
Custom Webhooks
Workflows can send webhooks to external systems:
1jobs:
2 send-notification:
3 needs: analysis
4 uses: xema/webhook
5 with:
6 url: https://hooks.acme.com/deployments
7 method: POST
8 headers:
9 Authorization: "Bearer ${{ secrets.WEBHOOK_TOKEN }}"
10 Content-Type: application/json
11 payload:
12 status: ${{ needs.analysis.outputs.status }}
13 result: ${{ needs.analysis.outputs.result }}HTTP Requests
Make arbitrary HTTP calls:
1jobs:
2 call-external-api:
3 uses: xema/http
4 with:
5 url: https://api.external.com/process
6 method: POST
7 headers:
8 Authorization: "Bearer ${{ secrets.API_KEY }}"
9 body:
10 data: ${{ needs.previous.outputs.data }}
11 outputs:
12 response: ${{ result.body }}Integration with External Tools
Example: Trigger AWS Lambda
1jobs:
2 trigger-lambda:
3 uses: xema/http
4 with:
5 url: https://lambda.amazonaws.com/invoke
6 method: POST
7 headers:
8 Authorization: "Bearer ${{ secrets.AWS_TOKEN }}"
9 body:
10 functionName: my-function
11 payload:
12 input: ${{ inputs.data }}Example: Post to Slack
1jobs:
2 notify-slack:
3 uses: xema/webhook
4 with:
5 url: ${{ secrets.SLACK_WEBHOOK_URL }}
6 payload:
7 text: "Workflow completed: ${{ needs.final.outputs.status }}"
8 uploads:
9 - title: "Summary"
10 text: ${{ needs.final.outputs.summary }}Integration Patterns
Pattern 1: Git-Based Triggers
Trigger workflows on code changes:
1on:
2 webhook:
3 - event: scm.push
4 filters:
5 branch: main
6
7jobs:
8 build:
9 uses: xema/agent
10 with:
11 task: Build and test
12 repo_url: ${{ trigger.payload.repo.url }}
13 branch: ${{ trigger.payload.branch }}
14 commit_sha: ${{ trigger.payload.commit.sha }}Use case: CI/CD pipeline on every push
Pattern 2: Issue-Driven Workflows
Trigger workflows from issue trackers:
1on:
2 webhook:
3 - event: tracker.issue
4 filters:
5 type: feature
6 priority: high
7
8jobs:
9 plan-feature:
10 uses: xema/agent
11 with:
12 task: Create feature delivery plan
13 issue_key: ${{ trigger.payload.issue.key }}
14 title: ${{ trigger.payload.issue.title }}
15 requirements: ${{ trigger.payload.issue.description }}Use case: Automated planning from feature requests
Pattern 3: Multi-Tool Orchestration
Coordinate workflows across multiple external systems:
1on:
2 webhook:
3 - event: scm.pull_request
4 filters:
5 state: opened
6
7jobs:
8 review:
9 uses: xema/agent
10 with:
11 pr_url: ${{ trigger.payload.pull_request.url }}
12 pr_number: ${{ trigger.payload.pull_request.number }}
13 code_diff: ${{ trigger.payload.pull_request.diff }}
14 outputs:
15 review: ${{ result.review }}
16
17 post-comment:
18 needs: review
19 uses: xema/webhook
20 with:
21 url: ${{ trigger.payload.pull_request.comments_url }}
22 method: POST
23 body:
24 body: ${{ needs.review.outputs.review }}
25
26 update-status:
27 needs: review
28 uses: xema/http
29 with:
30 url: ${{ trigger.payload.pull_request.status_url }}
31 method: POST
32 body:
33 state: "success"
34 description: ${{ needs.review.outputs.status }}Security & Authentication
API Tokens
All API calls require authentication:
1curl -H "Authorization: Bearer $XEMA_TOKEN" \
2 https://workflow-engine-api.xema.dev/workflows/...Webhook Verification
External providers send signed webhooks to prevent spoofing:
Header: X-Signature-256 = HMAC-SHA256(secret, body)
Xema verifies the signature before processing.
Secrets in Workflows
Reference secrets securely:
1jobs:
2 deploy:
3 uses: xema/http
4 with:
5 url: https://api.deployment.com/deploy
6 headers:
7 Authorization: "Bearer ${{ secrets.DEPLOYMENT_TOKEN }}"Secrets are:
- ✅ Encrypted at rest
- ✅ Never logged
- ✅ Audited for access
- ✅ Rotatable
Permission Scopes
Workflows declare access requirements:
1permissions:
2 repos: read # Read SCM
3 kb: limited # Limited KB access
4 backlog: write # Full issue tracking
5 integrations: read # Read integrationsIdempotency & Retry
Idempotent Webhook Processing
Each webhook has a deterministic idempotency key:
Key = {provider}:{deliveryId}
This ensures:
- First delivery: Processed normally
- Duplicate deliveries: Skipped (already processed)
- Failed deliveries: Retried with same key
Workflow Retry Policy
Configure how failed workflows are retried:
1defaults:
2 retry:
3 maxAttempts: 3
4 backoffCoefficient: 2.0
5 initialInterval: 1s
6 maxInterval: 60sExample: Safe External API Calls
1jobs:
2 call-external:
3 retry:
4 maxAttempts: 5
5 uses: xema/http
6 with:
7 url: https://api.external.com/process
8 body:
9 idempotency_key: ${{ trigger.payload.deliveryId }}External API also receives idempotency key, enabling end-to-end safety.
Integration Checklist
When integrating an external system:
- Declare webhook events in
on.webhook - Add appropriate filters to avoid unwanted triggers
- Access payload via
${{ trigger.payload.* }} - Use
${{ secrets.* }}for authentication - Include idempotency keys in external calls
- Handle timeouts gracefully (retry policy)
- Test with webhook simulation tools
- Monitor webhook delivery in audit logs
- Document expected behavior and filters
- Set up alerts for failed workflows
Examples
Example 1: GitHub → Code Review
1name: automatic-code-review
2on:
3 webhook:
4 - event: scm.pull_request
5 filters:
6 state: opened
7
8jobs:
9 analyze:
10 uses: xema/agent
11 with:
12 task: Review pull request code for quality and security
13 repo_url: ${{ trigger.payload.repo.url }}
14 pr_number: ${{ trigger.payload.pull_request.number }}
15 code_diff: ${{ trigger.payload.pull_request.diff }}
16 outputs:
17 review_comment: ${{ result.review }}
18
19 post-review:
20 needs: analyze
21 uses: xema/webhook
22 with:
23 url: ${{ trigger.payload.pull_request.comments_url }}
24 method: POST
25 body:
26 body: ${{ needs.analyze.outputs.review_comment }}Example 2: Jira → Feature Specification
1name: jira-to-spec
2on:
3 webhook:
4 - event: tracker.issue
5 filters:
6 type: feature
7
8jobs:
9 generate-spec:
10 uses: xema/agent
11 with:
12 task: Create detailed feature specification
13 issue_title: ${{ trigger.payload.issue.title }}
14 requirements: ${{ trigger.payload.issue.description }}
15 priority: ${{ trigger.payload.issue.priority }}
16 outputs:
17 spec_id: ${{ result.artifact_id }}
18
19 notify:
20 needs: generate-spec
21 uses: xema/webhook
22 with:
23 url: https://notifications.acme.com/webhook
24 payload:
25 issue_key: ${{ trigger.payload.issue.key }}
26 spec_generated: true
27 spec_id: ${{ needs.generate-spec.outputs.spec_id }}Next: Read Expressions & Data Flow for details on data passing between jobs.