Frontrunner Open Frontrunner

Frontrunner CLI

fr runs your Frontrunner workspace from a terminal: agents, campaigns, runs, sessions and the lists they produce. It uses the same API as the app, so anything below also works from your own code over HTTPS.

Install

Node 20 or newer.

mkdir -p ~/.local/bin
curl -fsSL https://app.usefr.com/cli/fr -o ~/.local/bin/fr
chmod +x ~/.local/bin/fr
fr --version

If fr is not found, add ~/.local/bin to your PATH.

Create a key

In Frontrunner open Settings, then API keys, then Create key. The key is shown once. It acts as you: it reaches the workspaces you are a member of and nothing else, and it stops working the moment you revoke it on the same page.

Sign in

fr login --key fr_... --ws <workspace>
fr whoami

fr login writes ~/.frrc (readable only by you) and verifies the key. fr whoami prints the workspace ids your key can act in; pass one of them as --ws. If you have exactly one, fr login picks it for you.

Lists

Pull a list. --csv prints every column with a key column first; --json prints the raw API response, rows included.

fr list ls
fr list show <listId> --csv > list.csv
fr list show <listId> --json > list.json

Create a list:

fr list create --name "Design partners" --column company:Company --column why:"Why now":longtext

Writing to a list

The case this was built for: a tool of yours writes one line per lead (an opener), that line has to become a variable on the list a campaign targets, and it has to reach the leads already sitting in the Instantly campaign so the copy can use it.

1. Pull the list with everything your tool needs. Keep the key column: it is how rows are matched on the way back.

fr list show <listId> --json > leads.json

2. Add the field, once. Existing rows keep their cells; the new column is empty until you fill it.

fr list add-column <listId> --key opener --label "Opener" --type longtext

Types: text (default), longtext, number, date, url, email, phone.

3. Write the line per row and push it back. A row is matched by its key: an existing key updates that row's cells, a new key adds a row. Only the cells you send change; an empty CSV cell leaves the stored value alone. Columns in the file must exist on the list; --add-columns adds any missing ones as text.

[
  { "key": "jane-doe-1a2b3c", "cells": { "opener": "Saw the Series A note. Congrats on the growth hire." } },
  { "key": "acme.com", "cells": { "opener": "Your pricing page changed twice this month." } }
]
fr list push <listId> --file openers.json
fr list push <listId> --file list.csv --add-columns

The file is JSON as above, or CSV with a key column plus one column per field (what fr list show --csv prints, so pull, edit and push round-trips). One row without a file:

fr list set <listId> acme.com --field opener="Your pricing page changed twice this month."

4. Send the field to the leads already in the Instantly campaign, matched by the list's email column. =personalization fills Instantly's own {{personalization}} field; a bare column name makes a custom variable of that name, so --var opener gives the copy {{opener}}.

fr list sync <listId> --instantly <campaignId> --var opener=personalization
fr list sync <listId> --instantly <campaignId> --var opener --var segment

Leads the campaign does not hold are listed, not added, and no mail is sent. Rows marked not relevant are skipped. Run it again after you improve the line; the value is overwritten. The campaign id is the one in the Instantly URL, or fr campaign get <campaignId> shows it for a campaign Frontrunner runs.

Agents, campaigns and runs

fr agent list
fr agent get <agentId>
fr agent run <agentId> [-m "what you want"] [--wait]

fr campaign list
fr campaign get <campaignId>
fr campaign run <campaignId> [--wait]

fr run list [--campaign <campaignId>]
fr run get <runId>
fr run wait <runId>
fr run events <runId>

fr session new --agent <agentId> [-m "text"]
fr session send <sessionId> "text"
fr session watch <sessionId>

--json prints the raw API response. --wait blocks until the run finishes and exits non-zero if it failed. fr --help lists everything, including create and delete.

HTTP API

Every command is a request to https://app.usefr.com/api/v1/<workspace>/... with the header Authorization: Bearer fr_.... The three list calls:

# pull: { list, rows, suppressions }
curl -s https://app.usefr.com/api/v1/<workspace>/lists/<listId> -H "Authorization: Bearer $FR_API_KEY"

# add a field
curl -s -X POST https://app.usefr.com/api/v1/<workspace>/lists/<listId>/columns \
  -H "Authorization: Bearer $FR_API_KEY" -H "Content-Type: application/json" \
  -d '{"columns":[{"key":"priority","label":"Priority","type":"text"}]}'

# update or add rows (up to 2000 per call): { counts: { added, updated, reconfirmed, suppressed } }
curl -s -X POST https://app.usefr.com/api/v1/<workspace>/lists/<listId>/rows \
  -H "Authorization: Bearer $FR_API_KEY" -H "Content-Type: application/json" \
  -d '{"rows":[{"key":"acme.com","cells":{"opener":"Your pricing page changed twice this month."}}]}'

# carry a column onto the leads in an Instantly campaign: { updated, missing, failed, skipped }
curl -s -X POST https://app.usefr.com/api/v1/<workspace>/lists/<listId>/instantly-variables \
  -H "Authorization: Bearer $FR_API_KEY" -H "Content-Type: application/json" \
  -d '{"campaignId":"<instantly campaign id>","variables":{"opener":"personalization"}}'

GET /api/v1/whoami checks a key. GET /api/v1/<workspace>/lists lists the lists without their rows.

Configuration

WhereWhat
~/.frrc{"baseUrl","apiKey","workspace"}, written by fr login
FR_API_KEY, FR_WORKSPACE, FR_BASE_URLoverride the file
--key, --ws, --urloverride both, for one command

Errors

MessageMeaning
invalid_api_keythe key is wrong or revoked; create a new one in Settings
workspace_not_allowed, forbiddenyou are not a member of that workspace
no workspace setpass --ws <id> or run fr login --ws <id>
unauthorizedno key reached the server; check FR_API_KEY or ~/.frrc
list_not_foundthe list id is wrong or belongs to another workspace; fr list ls prints yours