Skip to content

Connecting Slack and Linear

Both are optional. Their credentials live only in the control plane (the API), like the GitHub App's key: workers and agents never see them. Inbound requests are authenticated by each service's request signature, not by a Factoryal token.

Slack

  1. Create an app at api.slack.com/apps → From scratch.
  2. OAuth & Permissions → Bot Token Scopes: app_mentions:read, chat:write, commands. Install the app to the workspace and copy the Bot User OAuth Token (xoxb-...) into FACTORYAL_SLACK_BOT_TOKEN.
  3. Basic Information → copy the Signing Secret into FACTORYAL_SLACK_SIGNING_SECRET.
  4. Event Subscriptions → on, Request URL https://<your-factoryal-api>/v1/integrations/slack/events (Factoryal answers Slack's verification challenge). Subscribe to the bot event app_mention.
  5. Slash Commands → create /factoryal with Request URL https://<your-factoryal-api>/v1/integrations/slack/commands.
  6. Map Slack users to Factoryal identities, for approvals: FACTORYAL_SLACK_USER_MAP={"U012ABC": "ana", "U034DEF": "bob"}. The identities are the names you list as approvers on approval steps.

What arrives where:

In Slack In Factoryal
@factoryal please triage the billing outage a slack_message entity, kind: mention; thread_ts is where to reply
/factoryal fix the flaky ledger test a slack_message entity, kind: command
/factoryal approve <run id> [step] [comment] decides the run's pending approval as the mapped user
/factoryal reject <run id> [step] [comment] same, rejecting

The slack_message blueprint is created on first use. Start runs from it with an entity_event trigger and reply with a notify step; see examples/demo/projects/support/workflows/slack-triage.yaml. Slack retries a delivery it thinks failed; a message already stored is not stored (or triggered) again. The app's own messages are ignored.

Linear

  1. Settings → API → Personal API keys (or an OAuth app): create a key with write access and set FACTORYAL_LINEAR_API_KEY. Factoryal uses it to comment on issues.
  2. Settings → API → Webhooks → new webhook, URL https://<your-factoryal-api>/v1/integrations/linear/webhook, resource type Issues. Copy its signing secret into FACTORYAL_LINEAR_WEBHOOK_SECRET.

Each issue becomes a linear_issue entity keyed by its identifier (ENG-42) with title, description, state, labels, assignee, priority, team and url; updates merge into it and removed issues are deleted. Deliveries older than 5 minutes are rejected. examples/demo/projects/delivery/workflows/linear-agent-fix.yaml fixes issues labelled agent after an approval and comments the PR on the issue.

Flow

sequenceDiagram
    autonumber
    actor Dev as Developer
    participant S as Slack
    participant API
    participant D as Dispatcher
    participant R as Run

    Dev->>S: @factoryal please triage the billing outage
    S->>API: event (signed)
    API->>API: verify signature, store slack_message entity
    D->>R: entity_event trigger starts a run
    R->>R: agent / code steps
    R->>API: notify step (slack_thread)
    API->>S: chat.postMessage in the thread
    Dev->>S: /factoryal approve <run id>
    S->>API: command (signed)
    API->>R: approve signal, as the mapped identity

Replying: the notify step

- id: reply
  type: notify
  target: slack_thread           # or linear_comment, github_comment, github_review
  channel: "{{ .inputs.channel }}"
  thread_ts: "{{ .inputs.thread }}"
  text: "Done: {{ .steps.fix.output.pr_url }}"
# linear_comment: issue: ENG-42        github_comment: repo: billing-api, number: 12 (a PR)

github_review posts a comment-only review on a PR: text is the summary and comments (a list, or a template that yields one) are inline comments {path, line, body}, where line is a line number in the new file. A comment with kind (issue, suggestion or nitpick) gets that kind's header, and one with a suggestion (the corrected line) a block GitHub applies in one click, so a model's output can go in as it is. Set commit_id to the head commit you reviewed, or set pull: <step>, an earlier github_pull step: it gives repo, number and commit_id, and the summary then ends with how many comments there are and what wasn't reviewed (text is optional with it). GitHub rejects a whole review if one comment points outside the diff, so those comments go into the summary under Outside the diff instead. The output has the review's url and how many comments went inline or outside_diff. With findings_of: <step> (the earlier step that wrote the comments), each inline comment becomes a finding of that step, and the output says how many.

github_reply answers in an existing review comment thread: repo, number, thread (a review comment id) and text. Set concedes (a template works) when the reply agrees the comment was wrong: its finding then counts as disputed. To answer several threads at once, set replies (a list or a template yielding one) of {thread, text, concedes} instead of thread and text: empty texts are skipped, and with pull: <step> only the threads that github_pull step read are answered, since a model's thread ids could point anywhere on the PR. One that fails doesn't stop the others: the output lists the replies sent and the ones that failed, and the step fails only if none was sent.

The step's output is what was posted (ts, or the comment's id and url). It is retried like other steps, so a message could be posted twice if the service accepted it but the response was lost.