Keep team in sync

Every negotiation starts with the whole team reading from the same one-page brief, written the minute the stage changed.

  1. onattio deal.updated
  2. filterstage is Negotiation
  3. agentresearch the account
  4. slackpost the deal brief in #deal-desk

Build it with your coding agent

Paste into any coding agent with the Terse skills installed. No skills yet? Install them in one command.

/terse-create build https://www.useterse.ai/use-cases/negotiation-deal-brief.md

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.