Keep team in sync
Every negotiation starts with the whole team reading from the same one-page brief, written the minute the stage changed.
- onattio deal.updated
- filterstage is Negotiation
- agentresearch the account
- slackpost the deal brief in #deal-desk
How it works
- Trigger — any deal record update in Attio fires the trigger; the filter closure lets only stage-changes-to-Negotiation through.
- Research — one agentic step does the work:
generateTextruns with the Attio skill (scoped to the Deals object) andSkill.web(), so the agent can read the record, its notes and meeting history, and check recent public signals. - 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, thenslack). terse generaterun after connecting, soAttioObject.*andSlackChannel.*exist as typed constants insrc/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.

Apollo
PostHog