The problem
Right now a matched comment gets its public reply and its DM as fast as the
queue can move, usually within a second or two of the comment being posted.
That timing is the giveaway: a human does not answer that fast, consistently,
at any hour.
What I'd propose
Two optional delays, both defaulting to 0 so nothing changes for existing
installs:
- seconds to wait before posting the public comment reply
- seconds to wait before sending the DM
Independent values, because they serve different purposes — a short pause on
the public reply reads as natural, while a long pause on the DM costs
conversion, since the commenter is still looking at their phone.
How it turned out to be cheap
processInstagramWebhook enqueues process-comment knowing only the account
and the comment; keyword matching against automations happens later, inside the
worker. So the webhook cannot read a per-campaign setting at enqueue time.
What made this easy is that processComment's two legs are already decoupled
and independently idempotent — publicReplySentAt for the public reply,
SENT / dmDeliveryUnconfirmed for the DM — and the job is already re-entrant
by design. So the webhook can simply enqueue the job twice, each with its own
delay and a leg: "reply" | "dm" discriminator, and each existing block is
gated on the leg. No new dedup, no restructuring, no sleep holding a worker
slot.
Two details worth flagging if you take this:
- The leg suffix on the job id is load-bearing. The base id is deterministic,
so without it BullMQ treats the second add as a duplicate and silently drops
one of the legs.
- The polling reconciler deliberately gets no delay. Everything it sweeps up is
a comment the webhook already missed, so it is late already; holding it back
further would only make a late reply later.
Omitting leg still runs both legs, so the reconciler and any job queued
before the change behave exactly as they do today.
The question before I open a PR
I put the setting on InstagramAccount rather than Automation, for the
constraint above: per-campaign delays would need an extra hop — run the job
immediately, match, then enqueue the delayed legs per matched automation. That
is doable but restructures the loop and changes the dedup surface, so I did not
want to guess.
There is also an argument that per-account is the better model regardless: how
fast you answer is a property of the account, not of one campaign. It would be
odd for one campaign to feel human and another robotic from the same handle.
Would you want this, and if so, per account or per campaign? Happy to redo
it the other way if you prefer — I'd rather ask than send a PR built on the
wrong assumption.
State
Implemented and running on my own instance: two commits, a migration adding two
INTEGER NOT NULL DEFAULT 0 columns, 11 tests covering the leg split and the
enqueue. Verified in production against a real comment — the queue shows two
delayed jobs firing at their separate times, and both messages arrive.
I can open the PR against a clean branch whenever you say.
The problem
Right now a matched comment gets its public reply and its DM as fast as the
queue can move, usually within a second or two of the comment being posted.
That timing is the giveaway: a human does not answer that fast, consistently,
at any hour.
What I'd propose
Two optional delays, both defaulting to
0so nothing changes for existinginstalls:
Independent values, because they serve different purposes — a short pause on
the public reply reads as natural, while a long pause on the DM costs
conversion, since the commenter is still looking at their phone.
How it turned out to be cheap
processInstagramWebhookenqueuesprocess-commentknowing only the accountand the comment; keyword matching against automations happens later, inside the
worker. So the webhook cannot read a per-campaign setting at enqueue time.
What made this easy is that
processComment's two legs are already decoupledand independently idempotent —
publicReplySentAtfor the public reply,SENT/dmDeliveryUnconfirmedfor the DM — and the job is already re-entrantby design. So the webhook can simply enqueue the job twice, each with its own
delay and a
leg: "reply" | "dm"discriminator, and each existing block isgated on the leg. No new dedup, no restructuring, no
sleepholding a workerslot.
Two details worth flagging if you take this:
so without it BullMQ treats the second add as a duplicate and silently drops
one of the legs.
a comment the webhook already missed, so it is late already; holding it back
further would only make a late reply later.
Omitting
legstill runs both legs, so the reconciler and any job queuedbefore the change behave exactly as they do today.
The question before I open a PR
I put the setting on
InstagramAccountrather thanAutomation, for theconstraint above: per-campaign delays would need an extra hop — run the job
immediately, match, then enqueue the delayed legs per matched automation. That
is doable but restructures the loop and changes the dedup surface, so I did not
want to guess.
There is also an argument that per-account is the better model regardless: how
fast you answer is a property of the account, not of one campaign. It would be
odd for one campaign to feel human and another robotic from the same handle.
Would you want this, and if so, per account or per campaign? Happy to redo
it the other way if you prefer — I'd rather ask than send a PR built on the
wrong assumption.
State
Implemented and running on my own instance: two commits, a migration adding two
INTEGER NOT NULL DEFAULT 0columns, 11 tests covering the leg split and theenqueue. Verified in production against a real comment — the queue shows two
delayed jobs firing at their separate times, and both messages arrive.
I can open the PR against a clean branch whenever you say.