---
title: Keep team in sync
description: The moment a deal moves to Negotiation in Attio, an agent researches the account and posts a structured brief to your deal channel.
slug: negotiation-deal-brief
group: gtm
trigger: attio deal.updated
steps:
  - "filter: stage is Negotiation"
  - "agent: research the account"
  - "slack: post the deal brief in #deal-desk"
integrations:
  - attio
  - slack
outcome: Every negotiation starts with the whole team reading from the same one-page brief, written the minute the stage changed.
publishedAt: "2026-07-14"
updatedAt: "2026-07-14"
featured: true
---

## How it works

1. **Trigger** — any deal record update in Attio fires the trigger; the
   filter closure lets only stage-changes-to-Negotiation through.
2. **Research** — one agentic step does the work: `generateText` runs with
   the Attio skill (scoped to the Deals object) and `Skill.web()`, so the
   agent can read the record, its notes and meeting history, and check recent
   public signals.
3. **Brief** — the result posts to the deal channel with a fixed structure
   (deal, stakeholders, history, signals, risks, the ask), so every brief
   reads the same way.

## Requirements

- A Terse project (`terse init`) with the **Attio** and **Slack**
  integrations connected (`terse integrate connect attio`, then `slack`).
- `terse generate` run after connecting, so `AttioObject.*` and
  `SlackChannel.*` exist as typed constants in `src/terse.generated.ts`.
- A deals pipeline in Attio with a stage attribute, and a channel where the
  deal team lives.

## The job

```typescript
import { createJob, generateText } from "terse-sdk";
import { z } from "zod";
import {
  AttioObject,
  SlackChannel,
  Skills,
  Triggers,
  toolbox,
  type AttioDealRecordUpdatedTrigger,
} from "../terse.generated";

createJob({
  name: "Brief the team when a deal hits Negotiation",
  triggers: [Triggers.attio.onRecordUpdated({ object: AttioObject.Deal })],
  filter: async (event: AttioDealRecordUpdatedTrigger) => {
    // "Deal stage" attribute id — record.updated fires once per changed attribute,
    // so this keeps other deal edits from re-triggering the brief.
    return (
      event.resourceIds.attribute_id === "<STAGE_ATTRIBUTE_ID>" &&
      event.record.values.stage?.title === "Negotiation"
    );
  },
  onTrigger: async (event: AttioDealRecordUpdatedTrigger) => {
    const brief = await generateText({
      prompt:
        `A deal just moved to Negotiation. Research it and fill in the brief. ` +
        `Use the CRM (records, notes, meetings) as the source of truth for the deal, ` +
        `stakeholders, history, and risks. Use the web only for signals — recent public ` +
        `news about the company: funding, launches, hiring. If a section has nothing ` +
        `substantive, return an empty list rather than padding it. List stakeholders ` +
        `as "Name — role"; use "Unknown" for value or owner when not set. ` +
        `Context: ${event.formatForAgentRunner()}`,
      skills: [Skills.attio({ object: AttioObject.Deal }), Skills.web()],
      outputSchema: DealBrief,
    });

    await toolbox.slack.sendMessage({
      channelId: SlackChannel.DealDesk.channelId,
      message: renderBrief(brief),
      thread_ts: null,
      blocks: null,
    });
  },
});

function renderBrief(brief: DealBrief): string {
  return [
    `:handshake: *Deal moved to Negotiation: ${brief.dealName}*`,
    `*Deal*\n• Value: ${brief.dealValue}\n• Owner: ${brief.owner}`,
    `*Stakeholders*\n_Who we know:_\n${bullets(brief.knownStakeholders)}\n_Who's missing:_\n${bullets(brief.missingStakeholders)}`,
    `*History*\n${bullets(brief.history)}`,
    `*Signals*\n${bullets(brief.signals)}`,
    `*Risks*\n${bullets(brief.risks)}`,
    `*The ask this week*\n${brief.askThisWeek}`,
  ].join("\n\n");
}

function bullets(items: string[]): string {
  return items.length > 0
    ? items.map((item) => `• ${item}`).join("\n")
    : "• None found";
}

const DealBrief = z.object({
  dealName: z.string(),
  dealValue: z.string(),
  owner: z.string(),
  knownStakeholders: z.array(z.string()),
  missingStakeholders: z.array(z.string()),
  history: z.array(z.string()),
  signals: z.array(z.string()),
  risks: z.array(z.string()),
  askThisWeek: z.string(),
});
type DealBrief = z.infer<typeof DealBrief>;
```

The prompt pins the brief's structure so output stays scannable across deals;
the agent's judgment goes into the content, not the format. Scoping
`Skills.attio` to the Deals object keeps the agent's tool surface

## Make it yours

- **Brief on more stages.** A shorter "heads-up" brief when deals enter
  Proposal, the full version at Negotiation.
- **Thread the updates.** Post follow-up briefs as thread replies on the
  original message, so each deal keeps one running thread.
- **Add the paper trail.** Have the agent write the brief back to the Attio
  record as a note, so the CRM keeps what Slack scrolls away.
- **Flag missing stakeholders.** If no economic buyer is on the contact list,
  have the brief open with that gap instead of burying it.
