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¶
- Create an app at api.slack.com/apps → From scratch.
- 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-...) intoFACTORYAL_SLACK_BOT_TOKEN. - Basic Information → copy the Signing Secret into
FACTORYAL_SLACK_SIGNING_SECRET. - Event Subscriptions → on, Request URL
https://<your-factoryal-api>/v1/integrations/slack/events(Factoryal answers Slack's verification challenge). Subscribe to the bot eventapp_mention. - Slash Commands → create
/factoryalwith Request URLhttps://<your-factoryal-api>/v1/integrations/slack/commands. - Map Slack users to Factoryal identities, for approvals:
FACTORYAL_SLACK_USER_MAP={"U012ABC": "ana", "U034DEF": "bob"}. The identities are the names you list asapproverson 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¶
- 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. - Settings → API → Webhooks → new webhook, URL
https://<your-factoryal-api>/v1/integrations/linear/webhook, resource type Issues. Copy its signing secret intoFACTORYAL_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.