Pipeline by script
Use this skill when a small firm runs its pipeline from its own folder with no CRM and an agent records what happens on each deal. Every movement (a send, a reply, a booking, a decision that changes the next step or whose move it is) goes in through one script command that writes the pipeline row, the activity log line, an archive copy and the rebuilt dashboard together, and a drift check at the start of every agent turn names any row left behind its log line. Produces one Markdown pipeline file (open deals, channels, clients won, closed, and a dated activity log), a generated dashboard whose weekly counts come from event tags, and the commands that keep them in step. Also use for "log that reply", "move the deal", "add this to the pipeline", "close it out", "update the client row", "track pipeline without a CRM", "why is this row out of date".
Paul MunroFounder, AppliedGTMApplies whenever something moves on a deal, a channel or a client at a small firm that keeps its pipeline in one Markdown file and runs no CRM. Produces one recorded movement: the row, the activity log line, an archive copy and the rebuilt dashboard, written together by one command.
The play
- Keep the whole pipeline in one Markdown file: open deals, channels (doors that could lead to deals, kept apart from deals), clients won, closed, and an activity log with the newest line first. A dashboard is generated from it. Tables, columns, IDs and archive copies are in references/file-layout.md.
- Treat the agent as the interface. The person says what happened in a sentence. The agent turns it into one command with every field explicit: row ID, Last activity, next step, next date, whose move, log text and event tags. The script reads no prose and guesses no field; the only date it supplies is the time of recording, in front of a Last activity entry that starts without one. When the sentence leaves a needed field open, the agent asks.
- Pick the subcommand by what happened:
- Something moved on an open deal or a channel:
move. - A new deal or a new door:
add, with every column given. The script assigns the next free ID. - A deal or a door ended:
close, with the outcome. The row moves to the closed table and its ID is retired. - Something changed on a won client:
client, found by account name. Each writes the row and a timestamped log line together, keeps a copy of the file as it stood, and rebuilds the dashboard. A scheduled daily run rebuilds it too. Flags, refusals and a worked example are in references/one-command.md.
- Something moved on an open deal or a channel:
- Tag every
move,addandclientwith what happened, from a fixed set of ten: sent, received, held, booked, ask, price, yes, post, intro, other. The flag repeats. The dashboard computes the week's counts from these tags. An untagged line counts only on an uppercase marker such as SENT, HELD or BOOKED, and asks, prices, yeses and posts come from tags alone. - Record in the same turn the movement is seen. A send, a reply, a booking, and a decision that changes the next step or whose move it is all count as movements.
- Let the drift check run at the start of every agent turn. A hook runs it and prints a line only when a row is stale: its Last activity is older than its newest log line. The check names the row and changes nothing. The fix is a
moveon that row. How each date is read is in references/drift-check.md. - When the script refuses, it writes nothing and says why. Correct the command and run it again.
- When a movement turns up that no subcommand can record, extend the script. New deals, closes and client rows each became a subcommand once hand edits to them were the last movements the script could not record. Test the change on a scratch copy of the file;
checkprints the parse counts.
What good looks like
- First thing I check: whether the turn opened with a drift line. The check is silent when the file is clean, so a drift line means someone wrote a row or a log line around the command. I fix it with a
moveon that row first. - The mediocre version is rows and log lines edited by hand. They drift apart, which is why I built the script.
- I know it is working when the week's counts come straight off the dashboard, because every line carries its event tags and nobody counts by hand.
- Every row says whose move it is. A change to that is a movement, and I record it like a send or a reply.
Rules
- MUST record every movement through the command, in the same turn it is seen.
- MUST name the events on every
move,addandclient, from the fixed set. - MUST pass every field explicitly, and ask the person for any field the sentence left open.
- MUST fix a drift line with a
moveon the named row. - MUST test any change to the script on a scratch copy of the pipeline file.
- NEVER edit a pipeline row or an activity log line by hand.
- NEVER edit the dashboard directly. Every write rebuilds it from the file.
- NEVER reuse a closed ID.
- NEVER let the drift check change the file or block a turn. It reports, and the agent acts.
references/drift-check.md
The drift check
One check. A row has drifted when its Last activity is older than its newest log line. That happens when a log line or a row is written around the command: the command writes both together, so movements recorded through it stay in step.
When it runs
- At the start of every agent turn. A hook runs the check in its quiet form before the agent reads the prompt. When the file is clean it prints nothing. When rows are stale it prints one line naming them and saying to fix them with a
move. It has a short timeout, it never blocks the turn, and any failure inside it is swallowed, so it can never break a prompt. - On demand. Run
driftat any time.
It reads the open deals and channels tables. It changes nothing.
What it prints
D4 <row name>: log <Day Mon D> vs row <Day Mon D>
One line per stale row, with the newest log date first and the row's date second.
How the log date is read
The newest dated activity log line whose text, right after the timestamp, opens with the row's ID. A row with no log line is skipped.
How the row date is read
The newest date that starts a sentence in the Last activity cell: at the start of the cell, or right after ". " or "; " (a closing quote or bracket before the space is allowed). A cell is a history, and its sentences are in no fixed order, so the leading date alone gives false alarms. --leading reads only the date at the very start of the cell, for when that literal reading is wanted.
A date is a month name and a day, with an optional weekday in front, or a full year-month-day. The year is the most recent one that puts the date on or before today. A stated weekday must match the date; if it matches in one nearby year, that year is used, and if it matches none, the date goes unread. A row with no readable date is skipped.
Fixing a drift line
Run a move on the named row with a --last that records the movement in the log line. If the movement happened on an earlier day, start --last with that date, since a --last with no date gets the time of recording.
references/file-layout.md
File layout
One Markdown file holds the whole pipeline. Each part sits under a level-two heading, with one table below it, and the script finds each part by the start of its heading text. Columns are found the same way, by the start of the header name, so a header can carry a short note in brackets, as in Stage (since). A generated dashboard and a folder of archive copies sit beside the file.
Open deals
Heading starts with Deals, open.
| ID | Account | Contact (function) | Size | Source | Offer | Value | Stage (since) | Last activity | Next step | Next date | Whose move |
|---|
- Last activity holds dated sentences. Each
moveputs the new one in front of the old, joined by ". ", so the cell reads as a short history. The drift check reads its dates. - Next step and Next date say what happens next and when.
- Whose move says who acts next. A change here is a movement, recorded like any other.
Channels
Heading starts with Channels. A channel is a door that could lead to deals. Doors live in their own table so the deals table holds deals only.
| ID | Door | Contact (type) | Source | Could lead to | Status | Last activity | Next step | Next date | Whose move |
|---|
Status takes the place of Size and Value. The dashboard reads four statuses: Active, Waiting, Dormant and Closed.
Clients (won)
| Account | Contact | What is live | Billing | Next |
|---|
This table has no ID column. The client subcommand finds a row by its Account cell. An account name ending in "(past)" marks an engagement that has ended. The cells are descriptions, so each update replaces a cell in full.
Closed
| Record | Closed | Outcome |
|---|
close writes the Record cell as the row's name with its ID in brackets, the Closed cell as the day it closed, and the Outcome in the person's words.
Activity log
Heading starts with Activity log. Newest line first, one line per movement:
- YYYY-MM-DD, H:MM AM TZ: D7 <what happened> [events: sent, price]
- YYYY-MM-DD, H:MM AM TZ: C3 NEW ROW, <why the door was added> [events: intro]
- YYYY-MM-DD, H:MM AM TZ: D5 CLOSED, <outcome and why>
- YYYY-MM-DD, H:MM AM TZ: <Account> <what changed> [events: yes]
The ID right after the timestamp is what ties a line to its row. Client lines carry the account name in its place. The events tag closes the line when the command names events.
IDs
An ID is one capital letter and a number. Open deals take D numbers and channels take C numbers. add assigns the next free number in that series unless an ID is given. A closed ID stays retired, so every log line keeps pointing at one record.
The dashboard
A builder script reads the pipeline file and the archive copies and writes a dashboard page into its own folder, and nothing outside it. Its parsing is deterministic: anything it cannot read shows as "not logged" or as the raw text. Every command rebuilds it, and a scheduled daily run rebuilds it as well. If the firm publishes the dashboard somewhere, the command's output carries a reminder to republish it.
Each render also writes a weekly record for the most recent Friday, covering Saturday to Friday. A record for that week that came from another source is left alone. What the record counts is in one-command.md.
Archive copies
Before every write, the script copies the file as it stood into an archive folder, named with the date and minute (a number is added when two writes share a minute). It then writes to a temporary file and swaps it in, so the pipeline file is always whole.
The copies give the weekly record its starting point. It compares the copy from just before the week began (taken within the seven days before the week) with the file at the week's end. That comparison yields the week's commitments due and kept, and its new clients. With no copy in that window, those counts stay empty.
references/one-command.md
One command
The script has six subcommands. Four of them record movements, and each of those writes the row and one timestamped log line together, keeps an archive copy of the file as it stood, writes the file, and rebuilds the dashboard. A refusal writes nothing.
From a sentence to a command
The person speaks in a sentence. The agent writes the command, with every field explicit and every date concrete.
"Sent the proposal to them this morning. It's on them now. I'll follow up Friday if I hear nothing."
move D4 --last "Proposal sent" --next "Follow up on the proposal" \
--date "<Mon D of that Friday>" --whose "Theirs" \
--log "Proposal sent; their move" --event sent --event price
The agent resolves "Friday" to a date before it runs the command. If the sentence did not say which row, or left the next step open, the agent asks.
move
move ID --log TEXT plus at least one cell: --last, --next, --date, --whose, --stage, --offer (Could lead to, on a channel), --value, --status. Optional: --replace-last, --event TYPE (repeatable).
- Finds the row by ID in the open deals or channels table.
- Replaces only the cells given.
--lastgoes in front of the existing Last activity, joined by ". ".--replace-lastoverwrites the cell instead.- A
--lastthat starts without a date gets the time of recording put in front, in the form "Mon D, H:MM AM TZ: ", so the drift check can read it. To record a movement from an earlier day, start--lastwith that date. - A
--logtext that opens with the row's ID has that copy trimmed, since the timestamp already carries the ID.
add
add --log TEXT plus every column: --account (the Door, on a channel), --contact, --source, --offer (Could lead to, on a channel), --stage, --last, --next, --date, --whose, and --size and --value for a deal or --status for a channel. Optional: --table deals|channels (deals by default), --id, --event TYPE.
- Every column needs a value. A missing one refuses and names it.
- The ID is the next free D or C number unless
--idis given. An ID that already exists refuses. - The log line reads "ID NEW ROW, <text>".
close
close ID --outcome TEXT --log TEXT, optional --closed TEXT (today's "Mon D" by default).
- Removes the row from open deals or channels and appends "<name> (<ID>)", the closed day and the outcome to the closed table.
- The log line reads "ID CLOSED, <text>".
- The ID is retired.
client
client ACCOUNT --log TEXT plus at least one of --contact, --live (What is live), --billing, --next. Optional: --event TYPE.
- Finds the row whose Account cell equals ACCOUNT, or starts with it followed by a space or " (", ignoring case.
- No match refuses and lists the accounts. More than one match refuses and asks for a longer name.
- Each cell given is replaced in full.
- The log line names the account, without any "(past)".
drift and check
drift prints one line per row whose Last activity is older than its newest log line, and nothing when the file is clean. --quiet is the form the hook runs. --leading reads only the date at the very start of the cell. Details in drift-check.md.
check prints, for each table, the rows, the columns and the rows it could not read, then the number of log lines, then OK or FAIL.
A workspace option points any subcommand at another folder. Tests run that way, on a scratch copy.
Refusals
The script refuses, and writes nothing, when:
- the ID is not in the open deals or channels table, or a new ID already exists or is not a letter and a number;
- no cell was given;
- a flag names a column the table lacks;
- a value holds a line break (a row stays on one line);
- the new row would have a different number of cells than the header (a pipe in a value is escaped first);
- a needed heading or table is missing from the file.
Event tags
--event works on move, add and client, and repeats. The fixed set is sent, received, held, booked, ask, price, yes, post, intro and other. The log line then ends " [events: sent, price]". Without --event the line carries no tag.
The weekly record computes its counts from dated log lines in the week:
| Tag | Counts toward |
|---|---|
| sent | notes sent |
| held | sessions held; on a deal row, buyer conversations held too |
| received, intro, booked | came in |
| ask | asks made |
| price | priced asks |
| yes | paid yeses |
| post | posts published |
| other | recorded, counted toward no total |
An untagged line counts only on an uppercase marker: SENT, HELD, BOOKED, or a reply marker such as REPLIED. "NOT sent" counts as nothing. Asks, priced asks, paid yeses and posts come from tags alone, so a week with no tagged lines leaves those counts empty. The record's own note says how many lines were counted by tag, how many by marker, and how many went uncounted.
Commitments due and kept, and new clients, come from comparing archive copies (see file-layout.md). A commitment is a row whose next date fell in the week. It counts as kept when the row's Last activity or a log line for it is dated on or after that next date, within the week. A new client is an account new to the clients won table that week. A row added already marked "(past)" records an old engagement and counts as no win.