email column) or a Pylon issue id (a column like pylon_issue_id).
All three plugins require the Pylon datasource to be registered on your back-end: they need the Datasource instance to reach the Pylon API client.
Usage
Nothing is registered automatically. Opt each plugin in per collection:Datasource instance you registered earlier as the required datasource: option, and all refuse to register without a collection — they only work at the collection level. You can attach the same plugin to several collections with different option sets.
Create an issue and notify
ASingle-scope action that opens a Pylon issue from the selected host record and delivers its first message to the requester. The host record does not need to be related to Pylon: the requester is identified by an email entered (or pre-filled) in the form, and Pylon creates the contact on the fly when it does not already exist (the action derives the contact’s name from the email’s local part, e.g. john.doe@acme.com → john.doe).
Where a Zendesk notification is a side effect of a public comment, Pylon says it outright: destination_metadata.destination names the channel the issue’s body_html is delivered through, and no metadata at all is what leaves the issue internal.
The form exposes the following fields:
There is no “Type” field, unlike the Zendesk form:
POST /issues does not take one, Pylon accepting type on an update only.
Pylon never returns the priority of an issue, so no column carries it: what the operator picks in the form is applied on creation and shown nowhere in Forest afterwards.
Email-templates wizard
Whenemail_templates is set, the form becomes a two-page wizard:
- Page 1 — Template. A
Templatefield lists each template’stitleplus a sentinel"No template"entry. - Page 2 — Body. The same fields as above, with the Message recomputed from the page 1 selection.
content; taking it back ("No template") restores default_message rather than emptying a required field. Typing into Message in between is preserved across re-renders of the same selection.
"No template": the title is what the enum carries and what the content is looked up by, so a duplicate would send one template’s content under another’s name. Both are refused at registration rather than discovered by whoever sends the wrong message.
Outcome
- A Pylon
4xxreaches the operator as the action’s own error, with Pylon’s message intact — it names what they filled in. Anything else stays a 500. - On success the message names the issue by its Pylon number, and says whether the requester was notified and through which channel, or that the issue is internal.
- A failed
issue_id_fieldwriteback is appended to that success message as a warning; the issue exists either way.
Reply to an issue
ASingle-scope action that answers one Pylon issue on the channel the customer used, or posts an internal note on it. A Slack issue gets a Slack reply and an email issue gets an email: Pylon delivers the reply into the conversation of the message the action names, and the action picks that message for you.
The form exposes the following fields:
Which channel
The channel is thesource of the first customer-visible message of the issue, which means any message that is not an internal note. The reply answers the latest customer-visible message on that channel.
- An issue that started on one channel and moved to another is answered on the first one.
- An issue with no customer-visible message, such as one created as an internal note, cannot get a public reply. Only the internal note is offered. With
allow_internal_note: false, the Channel field shows the reason and the action refuses to run.
Email recipients
Pylon does not work out the recipients of an email reply, so the form pre-fills To and CC from the latest email on the channel:- When the customer wrote that email, To is its sender only. The addresses the customer wrote to include your support inbox. The other addresses in its To are not carried over, not even to CC: add them by hand if they should get the reply.
- When your team wrote it, To is the addresses it was sent to.
support_emails and every address your team wrote from in the thread are left out of To and CC. That applies to the pre-fill and to what the operator types: the success message names any typed address that was left out. A reply with no To is refused.
Who signs the reply
By default, the account that owns the API token signs every reply, and the success message says so. Withpost_as_operator: true, a public reply is signed by the active Pylon user whose primary or secondary email matches the email of the Forest user who runs the action. If no active user matches, several do, the Forest user has no email, or the Pylon users cannot be read, the reply is refused and nothing is sent. The action never falls back to signing with the token. Internal notes are always signed by the token.
Cost in Pylon requests
- Every form render reads the thread once, with one
GET /issues/{id}/messages(120 requests per minute). The form renders when it opens and each time the Reply type changes. Editing the other fields does not render it again. - A submit adds one read for an internal note and two for a public reply: the agent renders the form again before running the action, and a public reply reads the thread once more so it answers the latest state of the conversation.
- When the action sits on
PylonIssueitself, every render and every submit also resolves the selected issue with oneGET /issues/{id}(300 requests per minute). - With
post_as_operator, a public reply adds oneGET /users. - The reply itself is one
POST /issues/{id}/reply, and an internal note is onePOST /issues/{id}/note. Each endpoint allows 30 requests per minute.
Outcome
- A thread that cannot be read is shown as an error in the Channel field, never as an issue without a customer message. Both reply types stay offered, and an internal note does not depend on the thread.
- A Pylon
4xxreaches the operator with Pylon’s own message. - Any other failure, a timeout included, may happen after Pylon delivered the message. The action says so and asks the operator to check the issue in Pylon before sending it again.
- On success, the message names the issue, the channel or the email recipients, and who signed the reply.
Close an issue
Registers actions that move the selected Pylon issues to a state —closed unless told otherwise. The ids are read either from the primary keys of the selected records (when the action sits on PylonIssue itself) or from a configurable column of the host record, so you can close a Pylon issue straight from a business row that stores pylon_issue_id.
One action is registered per requested scope:
Pick a subset to register fewer variants, e.g.
scopes: %i[bulk] registers a single bulk action. Registering both under the same name is refused at registration: a collection keys its actions by name, so the second would overwrite the first and answer with the wrong scope.
Registering the plugin twice with two different state values on the same collection is how you offer several transitions — give each variant its own action_name / bulk_action_name.
Which scope bounds it
The state is written straight through the Pylon client rather than throughPylonIssue, because the action is registered on the host collection. But the ids were read before that write, through the collection the action sits on, and the agent intersects the operator’s scope into the filter that read them. So what bounds this action is the scope of the host collection:
- Mounted on
PylonIssue, a scope or a segment onPylonIssuebounds exactly what it closes. - Mounted on a business collection with
issue_id_field, what bounds it is the records the operator may see there, and the issue ids those records carry. In that form the column is the authority — an operator who can write it can name any Pylon issue — so treat it as one.
Batch behaviour
The batch is capped at 20 issues (MAX_TARGETS), the same budget a filter-driven write gets: Pylon takes one request per issue, so a wider selection is a long run of sequential writes the request may time out on, leaving the issues closed up to that point closed and reporting which ones to nobody. Past the cap the run is refused before its first write, with a message naming the count and the cap — with one exception today. Mounted on PylonIssue with a selection carrying no scope and no segment, the ids are read through the datasource’s own primary-key lookup, which truncates a pageless read at twenty before the cap can see the rest: fifty selected issues close the first twenty and report “20 issues closed”. The two other forms are exact — issue_id_field on a host collection, and any selection a scope or a segment narrows.
The cap counts the issues named, not the records selected: a column of issue ids is not a key, so a hundred host records naming ten issues is a batch of ten (duplicates are collapsed, so no issue is written — or counted — twice).
Within the batch, each id is processed independently: a single issue Pylon refuses (deleted, or outside the token’s scope) does not abort the rest, and the success message names how many were moved and which ones failed. If every id fails, the action surfaces as an error rather than as a partial success.
If no usable id can be read from the selection — an empty issue_id_field, a renamed column, a record the scope hides — the action answers No Pylon issue id found in '<field>'. rather than calling Pylon. A refusal coming from the datasource itself (a scoped selection on PylonIssue naming more issues by id than one page of lookups covers, for instance) travels to the operator with its own message instead: a selection they can see they made must never be reported as “nothing selected”.