Building a Team Agent That Fits the Way Engineers Work
What we learned building an approval-first engineering agent around Slack, GitHub and AWS.
Software delivery rarely gets stuck because nobody can write code.
It gets stuck because the work is unclear, the acceptance criteria are implied, the useful context is spread across Slack threads and pull requests, and the next person has to reconstruct the story from fragments. That is a very human problem. It is also exactly the kind of problem that makes AI engineering tools both exciting and dangerous.
The exciting part is obvious: modern AI tools can read, summarise, inspect, suggest and draft at a speed that changes the shape of everyday engineering work.
The dangerous part is less flashy: if we point a powerful model at a messy workflow, it can make the mess faster. It can generate a plausible issue from an incomplete Slack message. It can review a pull request without understanding the local standards. It can post a confident answer into a team channel when the safer thing was to ask one more question.
That tension led us to a simple design goal:
Build a team agent that improves the quality of engineering work before it tries to automate the work itself.
The shape of the experiment was an internal Sage prototype for Slack-to-GitHub engineering workflows: a shared control plane for squad workflows, built around the tools engineers already use.
The problem we started with
Most teams already have the raw ingredients for good delivery:
- a place where work is discussed, often Slack
- a place where engineering context lives, often GitHub issues and pull requests
- a delivery tracker, often Jira
- CI, reviews, architecture notes, repo-specific conventions and team habits
The challenge is that these things do not automatically become a coherent brief.
A typical piece of work may start as a short Slack message, gain constraints in a thread, become an issue later, pick up implementation detail in a pull request, and then be reviewed by someone who has to infer what was agreed. Every hand-off costs attention.
Our hypothesis was that a team agent could help most by tightening those hand-offs:
- turn rough requests into structured work items
- ask clarifying questions before drafting
- review pull requests against repo-local context
- generate implementation investigation and planning artefacts
- keep write actions explicit and human-approved
- provide a lightweight audit trail of what happened and why
That last point mattered. We were not trying to build an autonomous delivery system. We were trying to build a useful colleague-shaped tool: one that can read, draft, inspect and propose, but knows when to stop.
What we built
At a high level, the prototype is a TypeScript and AWS serverless control plane. Slack is the interaction layer. GitHub is the engineering context and review surface. AWS Bedrock provides model access. The control plane owns routing, policy, state, approvals, audit records and external writes.
flowchart TD
Slack[Slack interactions] --> API[API Gateway and Lambda]
API --> Queue[SQS job queue]
Queue --> Worker[Worker Lambda]
Worker --> Policy[Control plane]
Policy --> Context[State, artefacts and context]
Policy --> Runtime{Runtime choice}
Runtime --> Strands[Strands for lightweight flows]
Runtime --> Goose[Goose on ECS/Fargate for repo-heavy flows]
Policy --> Outputs[Slack prompts and approval-gated GitHub writes]
The important part is not the diagram. The important part is the separation of responsibilities.
The API layer verifies the Slack request and gets out of the way quickly. Long-running work happens asynchronously through a queue. The worker owns workflow state, policy checks and human-facing output. Model runtimes sit behind a boundary rather than owning the product workflow.
We used two different runtime styles:
- Strands Agents, an agent SDK, for lightweight, Lambda-compatible, persona-led workflows such as work-item refinement and fast pull request review.
- Goose, an open-source coding-agent runtime, on ECS/Fargate for repo-heavy workflows that need a checked-out repository, deeper inspection and longer execution time.
That split was one of the more useful architectural decisions. We did not want every workflow to pay the operational cost of a containerised coding agent. We also did not want to force code-heavy investigation into a short-lived Lambda call. The control plane chooses the runtime per workflow.
A Slack-first work item flow
The first useful path was work-item refinement.
An engineer, product manager or business analyst can start with a rough request in Slack. The agent validates the team and repository context, loads bounded repo-local guidance, and then runs a staged pipeline:
collect context -> classify the route -> ask clarifying questions or draft -> produce a structured work item -> validate the draft -> post a Slack card with actions
If the request is underspecified, the agent should ask questions. That sounds obvious, but it was a meaningful product decision. A fast but poorly shaped issue creates work for everyone downstream. A slower flow that asks one good question can save a pull request from becoming a requirements negotiation.
For example, a deliberately generic Slack request might be:
"Can we make the nightly report failure less noisy? It alerts even when the upstream file is only delayed."
A useful draft is not just a longer version of that message. It might turn the request into a structured work item like this:
Problem: nightly report alerts are firing when an upstream file is delayed, even though no manual action is needed. Acceptance criteria: - distinguish delayed input from a failed report run - alert only when intervention is required - include the expected recovery behaviour in the runbook Open question: how long should the workflow wait before treating the delay as actionable?
The important change is that the agent separates the problem from the proposed solution, identifies the decision still needed, and makes the next human review easier.
When a draft is ready, it is still only a draft. A human can refine it, approve it, discard it, or create the GitHub issue through an explicit approval step. The worker owns the GitHub write. The model does not.
That draft, review, approval and write pattern repeats throughout the system. It is not just a UX preference. It is a safety property.
Pull request review: depth matters
Pull request review exposed another useful distinction: not every review needs the same runtime.
For many pull requests, a fast review pack based on metadata, diff and repo guidance is enough. It can highlight risks, suggest missing tests and give the reviewer a better starting point.
For deeper work, the agent may need to inspect the repository around the diff, follow imports and compare changed code with nearby tests or conventions. That is where the Goose-backed path fits. The worker prepares a bounded brief, launches a read-only repo-heavy workflow, validates the returned artefacts, and posts a concise digest back to Slack. If the team wants to publish the review to GitHub, that is another approval-gated action.
One design choice here was deliberately conservative: a deep review is not a GitHub approval, not a requested-changes review, and not a merge gate. It is advisory evidence for a human reviewer.
That framing helped keep the tool useful without pretending it could own accountability.
Five lessons from the build
The prototype taught us that useful engineering agents are less about autonomy first, and more about context, constraints and feedback loops.
1. Start with intake quality before autonomous code changes
It is tempting to begin with code generation because that is the most visible demo. We found more immediate value earlier in the lifecycle.
Taking a rough Slack message and shaping it into a work item forces useful questions:
- What problem are we solving?
- Who is affected?
- What would make this ready to implement?
- What is out of scope?
- What evidence or reproduction detail is missing?
- Which repository or service does this belong to?
An agent can help here without needing permission to change code. It can ask questions, draft acceptance criteria, highlight gaps and make the work easier for a human to approve.
That is a low-risk, high-leverage slice. It improves the input to every later stage.
2. Slack is a good front door, but a poor source of truth
Slack is where a lot of real work starts. That makes it a natural interaction layer. It is immediate, familiar and close to the people making decisions.
But Slack is not enough by itself.
Threads are noisy. Messages are edited, deleted, retried and forwarded. Useful context sits beside jokes, shorthand and assumptions. If an agent treats the raw thread as durable truth, it will eventually make a bad decision confidently.
The pattern that worked better was:
- use Slack for interaction and review
- store durable workflow state elsewhere
- keep raw conversational source bounded and treated as untrusted
- turn useful output into explicit artefacts
- write approved engineering context back to GitHub
That lets Slack stay the front door without becoming the database.
3. Approval-first writes need real state
Almost every AI product says a human is in the loop. The useful question is: which loop?
For the prototype, we found it helpful to be precise:
- A human reviews a work-item draft before issue creation.
- A human approves each external write.
- A human decides whether an advisory PR review should be published.
- A human owns merge decisions.
- A human reruns assessment when the source issue changes.
Those are different controls. They need different UI, state and failure handling.
The implementation detail matters too. Approval-first writes need conditional claims, expiry windows, idempotency keys and stale checks. Otherwise "approval" is just a button in front of an unreliable side effect.
This was one of the strongest findings from the build: safety is mostly ordinary distributed-systems engineering. Prompt wording helps, but it is not a transaction boundary.
4. Choose the runtime per workflow
We ended up with two runtime paths because the workflows had different needs.
For lightweight reasoning, drafting and review, an in-process agent runtime inside a worker Lambda was enough. It kept latency and operations simpler.
For deeper repository inspection, a containerised runtime was a better fit. It could handle a checked-out repository, longer execution and richer artefact generation.
The lesson was to choose the smallest runtime that can honestly do the job.
Not every agent workflow needs shell access. Not every workflow belongs in a Lambda. Not every code-heavy workflow should be allowed to run installs, tests or arbitrary commands by default. The right boundary depends on the risk and evidence required.
5. Measure useful accepted output, not generated volume
An agent can generate a lot of text. That is not success.
The signals we wanted to measure were closer to:
- Did the draft reduce clarification loops?
- Did the review point to a real risk?
- Did the plan help someone start implementation?
- Did humans accept, refine or reject the output?
- How often did the agent ask a good clarifying question?
- How often did it fail because context was missing?
- What did each useful workflow cost to run?
Feedback buttons and run metrics are not just reporting. They are product input. If a workflow is noisy, expensive or frequently ignored, the answer is not a bigger prompt. It may be a smaller workflow.
Why not just put an agent in GitHub Actions?
We considered simpler shapes. A GitHub Action can read a pull request. A Slack bot can call a model. A script can run Goose. Each of those is useful in isolation.
The problem is that the hard part is not calling a model. The hard part is productising the boundaries:
- Which Slack channels and repositories are allowed?
- What context is trusted, and what is untrusted prompt input?
- Who can approve a write?
- What happens if Slack retries an interaction?
- How do we avoid duplicate GitHub issues?
- How do we know whether the issue changed after a readiness assessment?
- How do we present a useful result without flooding a channel?
- Where do run records, artefacts and failure states live?
Those questions pushed us towards a control-plane architecture. The agent runtime is a component. The workflow product is the system around it.
What we deliberately did not do
The prototype avoided a few tempting shortcuts:
- no autonomous merges
- no broad automatic comments into shared channels
- no model-owned external writes
- no treating Slack text as trusted source material
- no assuming a pull request review from an agent is equivalent to human approval
- no one-size-fits-all runtime for every workflow
That made the system less flashy, but more usable. In practice, a team agent earns trust by being predictable before it becomes powerful.
Where this leaves us
The strongest lesson from the prototype is that an engineering agent is not one thing.
It is a set of small workflow contracts:
- intake
- context gathering
- drafting
- validation
- approval
- execution
- publication
- feedback
Each contract needs an owner, state, limits and a failure mode.
The model is important. The system around the model is what makes it usable.
The best internal agents will not be the ones that appear most autonomous in a demo. They will be the ones that fit naturally into the team's operating model, make useful work easier, and stop before they cross a boundary they do not own.
What's next
The next phase is to move from a useful prototype to careful squad trials.
That means putting the agent in front of the people who feel the workflow from different angles: product managers shaping the original request, business analysts turning ambiguity into acceptance criteria, developers picking up the implementation, and QA engineers looking for evidence that the right thing was built.
Each persona will notice different failure modes. A product manager may care most about whether the agent preserves intent. A business analyst may spot missing constraints. A developer may notice whether repo context is actually useful. A QA engineer may care about whether the output makes testing easier rather than just longer.
Those trials should treat feedback as product input, not a post-launch survey. The useful signals are which drafts get accepted, which questions improve the brief, which reviews are ignored, and where the agent slows a team down. That evidence can make the workflows smaller, sharper and more useful.
That is the real path to trust: test the agent against real squad habits, listen to the people using it, and keep iterating on the contracts until the tool fits the work.
References
- Spotify Engineering: Spotify's background coding agent, Part 1
- Spotify Engineering: Context engineering for background coding agents, Part 2
- Spotify Engineering: Predictable results through strong feedback loops, Part 3
- Spotify Engineering: Dataset migrations with Honk, Part 4
- Cloudflare: The AI engineering stack we built internally
- monday.com Engineering: I Am Morphex
- AWS Bedrock
- Strands Agents
- Goose