Skip to content

Adding an integration

An “integration” in Workforce0 is a typed connector between the platform and an external system. It usually provides some mix of:

  • Outbound — Workforce0 posts to the external system (Slack messages, Jira tickets).
  • Inbound — External system posts to Workforce0 (Slack button clicks, Jira updates, GitHub pushes).

Most integrations do both.

Pick a slug (e.g. linear). Define the required credentials (API key, workspace URL, team id) and any optional settings (default project mapping).

The IntegrationConnection table is generic — credentials encrypt- blob, metadata JSON. Just add a case to the runtime decoder for your slug.

backend/src/services/integrations/linear.connector.ts:

export class LinearConnector {
constructor(private apiKey: string, private workspaceId: string) {}
async createIssue(title: string, description: string): Promise<string> {
const res = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: { 'Authorization': this.apiKey, … },
body: JSON.stringify({ query: `mutation { issueCreate(…) { issue { id } } }` }),
});
const { data } = await res.json();
return data.issueCreate.issue.id;
}
async testConnection(): Promise<{ ok: boolean; error?: string }> {
try {
await this.query(`{ viewer { id name } }`);
return { ok: true };
} catch (err) {
return { ok: false, error: (err as Error).message };
}
}
}

backend/src/services/integrations/integration-connection.service.ts:

async getConnector(tenantId: string, name: string) {
if (name === 'linear') {
const creds = await this.decrypt(tenantId, 'linear');
return new LinearConnector(creds.apiKey, creds.workspaceId);
}
// …existing branches
}

5. Add HTTP routes for connect / disconnect / test

Section titled “5. Add HTTP routes for connect / disconnect / test”

backend/src/routes/integrations/linear.routes.ts:

export async function linearIntegrationRoutes(fastify: FastifyInstance) {
fastify.post('/connect', async (request, reply) => {
const body = LinearConnectSchema.parse(request.body);
await connectionService.connect(tenantId, 'linear', body);
return reply.send({ success: true });
});
fastify.post('/test', …);
fastify.post('/disconnect', …);
}

Add to the routes index.

When a dev_agent ticket completes and you want to mirror to Linear, call the connector from the ticket-completion hook:

const connector = await connectionService.getConnector(tenantId, 'linear');
await connector.createIssue(ticket.title, ticket.description);

frontend/src/components/integration-wizard.tsx has a pattern for per-integration steps. Add:

  • Fields (API key, workspace id).
  • “Get API key” link to the provider’s docs.
  • Connect button → api.integrations.linear.connect(…).
  • Test button → api.integrations.linear.test().
  • Status badge in the Integrations page.

Add docs-site/src/content/docs/integrations/linear.md using the existing integration pages as a template.

Add to the sidebar in docs-site/astro.config.mjs.

Add:

backend/src/lib/webhook-verify.ts — add a function for your source’s signature scheme (HMAC, shared secret, etc).

backend/src/routes/webhooks/linear.routes.ts:

fastify.post('/', async (request, reply) => {
const verified = verifyLinearSignature(
request.headers['x-linear-signature'],
request.rawBody,
linearWebhookSecret,
);
if (!verified) return reply.status(401).send({ error: 'bad signature' });
const event = LinearWebhookSchema.parse(request.body);
await handleLinearEvent(event);
return reply.send({ ok: true });
});

3. A handler that maps events to your state

Section titled “3. A handler that maps events to your state”

Usually you map external events to existing models (Ticket, Meeting). The handler is the glue.

Both positive (valid signature + expected side effect) and negative (invalid signature → 401).

  • Verify signatures, always. Never trust an inbound webhook’s body without a signature check.
  • Use the Integration vault for credentials. Don’t read from env vars in services — the vault is the source of truth.
  • Return 200 even on “ignored” events. Providers retry on non-2xx; ignored events shouldn’t trigger retries.
  • Graceful degradation. If the integration is disconnected, dependent features should log a warning and proceed, not crash.
  • Unit tests — mock fetch; assert the connector sends the right request shape.
  • Integration tests — use a mock server (msw or similar).
  • Smoke test — real credentials on a staging environment.

External systems may retry webhooks. Match on provider-side delivery IDs (X-GitHub-Delivery, x-linear-event-id) and short-circuit duplicates.

  • Rate limits — respect Retry-After.
  • User-Agent — set Workforce0/<version> so the provider can identify your traffic.
  • Minimum scope — only request the OAuth scopes you use.
  • Connector class with testConnection().
  • Connect / disconnect / test routes.
  • UI wizard.
  • Signature verification (if inbound).
  • Unit tests + signature-failure tests.
  • Docs page under integrations/.
  • Sidebar entry in astro.config.mjs.
  • Example .env keys in .env.example.