Integrations / Cron pattern
Dynamic campaign pause via cron
Not every automation should be event-driven. Some checks are inherently periodic, like “is this campaign still healthy enough to keep sending invitations?” This recipe runs nightly, polls the analytics surface, and calls manage_outreach(pause) if the campaign is underperforming.
Why this is a poll, not a webhook
Aggregate metrics like “7-day acceptance rate” are not events; they are rolling windows. A webhook firing on every change to the rate would be loud and useless. A nightly poll is the right shape.
Crispy intentionally exposes the analytics surface for exactly this kind of cron-style logic. The webhook surface and the analytics surface compose: webhooks handle “something happened,” analytics handles “what's the current state of the world.”
The shell version
Drop this in any cron-friendly environment (a Linux box, fly machine run, GitHub Actions on a schedule). Requires curl and jq.
#!/usr/bin/env bash
# pause-cold-campaign.sh — pause a Crispy campaign if 7d acceptance rate < 10%.
# Run nightly via cron, GitHub Actions, or any scheduler.
set -euo pipefail
API_KEY="${CRISPY_API_KEY:?Set CRISPY_API_KEY}"
CAMPAIGN_ID="${CAMPAIGN_ID:?Set CAMPAIGN_ID}"
THRESHOLD="${THRESHOLD:-0.10}" # 10%
ANALYTICS=$(curl -sS \
-H "Authorization: Bearer $API_KEY" \
"https://crispy.sh/api/v1/analytics?dimension=invitations&campaign_id=$CAMPAIGN_ID&days=7")
SENT=$(echo "$ANALYTICS" | jq -r '.totals.sent // 0')
ACCEPTED=$(echo "$ANALYTICS" | jq -r '.totals.accepted // 0')
if [ "$SENT" -lt 30 ]; then
echo "Skipping: only $SENT invites in last 7 days (need >=30 for stable rate)"
exit 0
fi
# bash doesn't do floats; multiply by 100 and compare ints.
RATE_PCT=$(awk "BEGIN { printf \"%.0f\", ($ACCEPTED / $SENT) * 100 }")
THRESHOLD_PCT=$(awk "BEGIN { printf \"%.0f\", $THRESHOLD * 100 }")
echo "7d rate: ${RATE_PCT}% (threshold: ${THRESHOLD_PCT}%)"
if [ "$RATE_PCT" -lt "$THRESHOLD_PCT" ]; then
echo "Pausing campaign $CAMPAIGN_ID"
curl -sS -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
"https://crispy.sh/api/v1/manage_outreach" \
-d "{\"action\":\"pause\",\"campaign_id\":\"$CAMPAIGN_ID\",\"reason\":\"7d acceptance below ${THRESHOLD_PCT}%\"}"
fiAdd to crontab: 0 9 * * * /usr/local/bin/pause-cold-campaign.sh runs daily at 09:00.
The n8n version
Same logic, no shell box needed:
1. Schedule Trigger → daily at 09:00 UTC
2. HTTP Request (GET) → https://crispy.sh/api/v1/analytics?dimension=invitations&campaign_id={{$env.CAMPAIGN_ID}}&days=7
3. Code (JavaScript) → const { totals } = items[0].json; const rate = totals.sent > 0 ? totals.accepted / totals.sent : 0; return [{ json: { rate, sent: totals.sent } }];
4. IF → {{ $json.rate < 0.10 && $json.sent >= 30 }}
5. HTTP Request (POST) → https://crispy.sh/api/v1/manage_outreach with body { "action": "pause", "campaign_id": "{{$env.CAMPAIGN_ID}}" }Knobs to tune
- Sample size guard. The shell script bails if fewer than 30 invitations were sent in the window. Without it, a slow week can pause your campaign on noise. Tune the floor based on your daily volume.
- Threshold. 10% is a reasonable cold-outbound floor for senior ICPs. Tune up for warmer audiences, down for cold lists.
- Window size. 7 days catches recent reality; 30 days smooths noise but reacts slowly. Pick the one that fits your iteration speed.
- Resume. Pair this with a separate nightly job that calls
manage_outreach(resume)once the rate recovers, so you don't need a human to babysit the pause.
Related
For event-driven pauses (e.g. pause if campaign.guardian_paused fires), use the webhook surface directly. The cron pattern shown here is for thresholds that depend on rolling aggregates, not single events.