@t4h.framework/amber
v0.2.0
Published
Amber module for the T4H Framework
Keywords
Readme
@t4h.framework/amber
Amber module for the T4H Framework. It drives an Amber webhook channel from a workflow: create tickets, act on them, and wait for what happens on the Amber side (an agent tabulating a ticket) without polling.
Installation
yarn add @t4h.framework/amberPeer dependencies: @t4h.framework/core, @t4h.framework/http, @t4h.framework/fs, @t4h.framework/cache and typebox.
Configuration
import { Amber } from '@t4h.framework/amber'
const amber = new Amber({
url: 'https://amber.example.com', // Amber API root
tenant: 'your-tenant-id',
channel: 'your-webhook-channel-id',
clientId: 'channel-client-id',
clientSecret: 'channel-client-secret',
})Every call goes to {url}/v1/tenants/{tenant}/channels/{channel} with the channel credentials as basic auth.
Point the channel webhook at the workflow
Amber emits its events (service:tabulate, …) to the URL configured on the webhook channel. Set it to the events route of the workflow that uses this package, on the host that runs it:
POST {runtime}/tenants/{tenant}/projects/{project}/workflows/{workflow}/eventsThe host asks the waiting activities which event is theirs (toCorrelationKey, keyed by the Amber service id), verifies the signature Amber puts on every event (detached signature over timestamp.body, key resolved by kid at {url}/tenants/{tenant}/.well-known/jwks.json) and advances the right wait. Nothing is stored on the Amber side to find the run back: no metadata, no ids in the ticket.
One webhook channel serves one workflow. A project with several Amber workflows needs one channel per workflow.
Tickets
const ticket = await amber.createTicket({
protocol: 'P-123',
skill: 'skill-id',
tabulationOption: 'option-id',
customers: [{ isOwner: true, type: 'customer' }],
fields: { customer: [{ name: 'email', value: '[email protected]' }] },
})A ticket is born locked by the workflow (lock: { ref: <workflow id> }), so no agent picks it up while the workflow still works on it. Pass lock: false to create it unlocked, or lock: { expiresAt, metadata } to customize the lock.
The lock ref is the workflow id the host reports through GetWorkflowUniqueIdentifierClaim (amber-ticket for a workflow declared with id: 'amber-ticket'), not the id of the run. lock(), releaseLock() and the lock release of transfer() use the same ref, so every run of a workflow can release a lock another run of it took.
A locked ticket still takes comments, interactions and field updates from the workflow, but Amber answers 423 to a lock() while a lock exists (release first, lock again) and to a tabulate() while the ticket is locked, even by the workflow itself. Release the lock before tabulating or finishing the ticket. Transfers release it for you with status: 'in-queue' or lockRelease: true.
await ticket.comment({ type: 'public', content: 'Looking into it' })
await ticket.interact({ content: 'Hello', contentType: 'text/plain' })
await ticket.updateField({ field: 'field-id', value: 'new value' })
const fields = await ticket.fields()
await ticket.lock({ expiresAt: new Date(Date.now() + 3600_000) })
await ticket.releaseLock()Handing the ticket to a human and waiting
const transfer = await ticket.transfer({
skill: 'human-skill-id',
status: 'in-queue',
})
const { tabulation } = await transfer.wait()transfer returns an awaitable. Transferring to in-queue releases the workflow lock, and wait() suspends the workflow until an agent tabulates the ticket, resolving with the tabulation the agent chose (tabulation.tabulationOption, tabulation.notes). Informative tabulations are ignored. A transfer to a user or without status: 'in-queue' settles at once and wait() resolves without tabulation; add lockRelease: true to release the lock on those too.
The wait has no deadline by default. Pass timeoutInMs in the second argument, next to retry, to bound it; wait() then rejects with { reason: 'timeout', service, serviceTransfer } when the agent has not tabulated in time:
const transfer = await ticket.transfer(
{ skill: 'human-skill-id', status: 'in-queue' },
{ timeoutInMs: 24 * 60 * 60 * 1000, retry: true },
)When the transfer released the lock (status: 'in-queue' or lockRelease: true), wait() on a ticket locks it back for the workflow before resolving, with no expiration, so no agent picks it up again while the workflow finishes it. Pass lock: false in the second argument to skip that, or lock: { expiresAt, metadata } to customize the lock. The lock is a separate AmberLockTicketActivity recorded after the transfer, and a refusal by Amber rejects wait() with its AmberRequestError.
const transfer = await ticket.transfer(
{ skill: 'human-skill-id', status: 'in-queue' },
{ lock: { expiresAt: new Date(Date.now() + 3600_000) } },
)
const { tabulation } = await transfer.wait() // ticket locked again hereawait ticket.tabulate({
tabulationOption: 'option-id',
notes: [{ type: 'public', content: 'ok' }],
})
await ticket.finish()Errors
Every call that Amber answers with a status other than 200 rejects with an AmberRequestError. Its message carries the status and the response body, which is where Amber says what it refused, so the reason shows up in any log that prints only the message:
Failed to create ticket: Amber responded 400 {"errors":{"$.tabulationOption":["Not found tabulation option"]}}The same data is available as error.status, error.body and error.cause ({ httpStatusCode, body }). Bodies longer than 2000 characters are truncated. The activity that waits on Amber events, transfer(), rejects its awaitable with that message as the payload instead.
Activities
Every method above is an activity, recorded in the workflow history and replayable. Hosts register them by class: AmberCreateTicketActivity, AmberGetTicketActivity, AmberCommentTicketActivity, AmberInteractTicketActivity, AmberTabulateServiceActivity, AmberTransferServiceActivity, AmberUpdateServiceActivity, AmberLockTicketActivity, AmberReleaseTicketLockActivity, AmberFindTicketFieldsActivity, AmberUpdateTicketFieldValueActivity.
AmberTransferServiceActivity is the one that waits on Amber events; the schema it validates the events against (AmberServiceTabulateEventSchema) and the auth it declares (AmberWebhook.authorization(channelUrl)) are exported by the package.
