@gotohuman/n8n-nodes-gotohuman
Version:
n8n node to request human reviews in AI workflows with gotoHuman
851 lines • 40.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GotoHuman = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const helpers_1 = require("./helpers");
class GotoHuman {
constructor() {
this.description = {
displayName: 'gotoHuman',
name: 'gotoHuman',
icon: 'file:gotohuman.svg',
group: ['transform'],
version: [1, 2],
features: {
sendRawReviewData: { '@version': [{ _cnd: { gte: 2 } }] },
},
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Request human reviews with gotoHuman',
defaults: {
name: 'gotoHuman',
},
usableAsTool: true,
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
credentials: [
{
name: 'gotoHumanApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
responseData: '',
path: '={{ $nodeId }}',
restartWebhook: true,
isFullPath: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Review Request',
value: 'reviewRequest',
description: 'Request for Human Review',
},
{
name: 'Message',
value: 'message',
description: 'Send a message to a human',
},
],
default: 'reviewRequest',
required: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Send',
value: 'send',
action: 'Send review request',
description: 'Request a human review',
},
{
name: 'Send and Wait for Response',
value: 'sendAndWait',
action: 'Send review request and wait for response',
description: 'Request a human review and wait for the response',
},
{
name: 'Delete',
value: 'delete',
action: 'Delete review request',
description: 'Delete an existing review request',
},
],
default: 'sendAndWait',
displayOptions: {
show: {
resource: [
'reviewRequest',
],
},
},
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Send Message to Human',
value: 'sendMessage',
action: 'Send message to human',
description: 'Send a message to a human',
},
],
default: 'sendMessage',
displayOptions: {
show: {
resource: ['message'],
},
},
},
{
displayName: 'Agent',
name: 'agentID',
type: 'resourceLocator',
description: 'Choose the agent you are acting as from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: { mode: 'list', value: '' },
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select the agent you are requesting as...',
typeOptions: {
searchListMethod: 'searchAgents',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
hint: 'Enter an ID',
placeholder: 'e.g. Y5XrpfmvpKcdJzKYLX2M',
}
],
displayOptions: {
show: {
resource: [
'reviewRequest',
],
operation: [
'send',
'sendAndWait',
],
},
},
},
{
displayName: 'Agent',
name: 'agentID',
type: 'resourceLocator',
required: true,
description: 'Choose the agent you are messaging as from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: { mode: 'list', value: '' },
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select the agent you are messaging as...',
typeOptions: {
searchListMethod: 'searchAgents',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
hint: 'Enter an ID',
placeholder: 'e.g. Y5XrpfmvpKcdJzKYLX2M',
}
],
displayOptions: {
show: {
resource: ['message'],
operation: ['sendMessage'],
},
},
},
{
displayName: 'Session ID',
name: 'sessionId',
type: 'string',
default: '',
description: 'The session ID to associate the message with',
displayOptions: {
show: {
resource: ['message'],
operation: ['sendMessage'],
},
},
},
{
displayName: 'Message',
name: 'message',
type: 'string',
required: true,
default: '',
description: 'The message to send to the human',
displayOptions: {
show: {
resource: ['message'],
operation: ['sendMessage'],
},
},
},
{
displayName: 'Review Template',
name: 'reviewTemplateID',
type: 'resourceLocator',
required: true,
description: 'Choose a review template from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: { mode: 'list', value: '' },
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a Review Template...',
typeOptions: {
searchListMethod: 'searchReviewTemplates',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
hint: 'Enter an ID',
placeholder: 'e.g. FjbxGtNfPIuDdRm55eqK',
}
],
displayOptions: {
show: {
resource: [
'reviewRequest',
],
operation: [
'send',
'sendAndWait',
],
},
},
},
{
displayName: 'Fields',
name: 'fields',
type: 'resourceMapper',
noDataExpression: true,
default: {
mappingMode: 'defineBelow',
value: null,
},
required: true,
typeOptions: {
loadOptionsDependsOn: ['reviewTemplateID.value'],
resourceMapper: {
resourceMapperMethod: 'getMappingFields',
mode: 'add',
valuesLabel: 'Fields',
fieldWords: {
singular: 'field',
plural: 'fields',
},
supportAutoMap: false,
},
},
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
'@feature': [{ _cnd: { not: 'sendRawReviewData' } }],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Review Data',
name: 'reviewData',
type: 'json',
required: true,
default: '',
placeholder: 'e.g. {"property1": "value1"}',
description: 'The data to populate the review template',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
'@feature': ['sendRawReviewData'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Review Config',
name: 'reviewConfig',
type: 'json',
default: '{}',
placeholder: 'e.g. {"fields": { "myField1": { ... } }}',
description: "Can optionally be used to dynamically configure your review and its fields",
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
'@feature': ['sendRawReviewData'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Session ID',
name: 'sessionId',
type: 'string',
default: '',
description: 'Send to connect various events and reviews of the same agent session/execution',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
},
},
},
{
displayName: 'Meta Data',
name: 'metaSelect',
description: 'Select if you want to add meta data that you want to receive back in the response webhook',
required: true,
type: 'options',
options: [
{
name: 'No Meta Data',
value: 'no',
},
{
name: 'Add as JSON',
value: 'json',
},
{
name: 'Add as Key-Value Pairs',
value: 'keyValue',
},
],
default: 'no',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Meta Data JSON',
name: 'metaJson',
type: 'json',
default: '',
placeholder: 'e.g. {"myKey": "myValue"}',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
metaSelect: ['json'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Meta Data Values',
name: 'metaKeyValues',
type: 'fixedCollection',
placeholder: 'Add Key-Value Pair',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'metaArray',
displayName: 'Metadata',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
placeholder: 'myKey',
default: '',
description: 'Name of the metadata key to add',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
placeholder: 'myValue',
default: '',
description: 'Value to set for the metadata key',
},
],
},
],
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
metaSelect: ['keyValue'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Assigned Users',
name: 'assignToSelect',
description: 'Change if you want to assign the review to a specific user',
required: true,
type: 'options',
options: [
{
name: 'Default / All',
value: 'all',
},
{
name: 'Only Selected Users',
value: 'selectByEmail',
},
],
default: 'all',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Selected Users',
name: 'assignTo',
description: 'List the email addresses of the users you want to assign the review to',
type: 'fixedCollection',
placeholder: 'Add User',
typeOptions: {
multipleValues: true,
},
default: [],
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Email Address',
name: 'email',
description: 'The email address the user used when signing up for gotoHuman',
type: 'string',
required: true,
placeholder: 'e.g. nathan@example.com',
default: '',
hint: 'Only emails from registered users will be accepted',
},
],
},
],
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
assignToSelect: ['selectByEmail'],
},
hide: { reviewTemplateID: [''] },
},
},
{
displayName: 'Review ID',
name: 'reviewId',
type: 'string',
required: true,
default: '',
placeholder: 'e.g. 123456',
description: 'The ID of the review request to delete',
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['delete'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['reviewRequest'],
operation: ['send', 'sendAndWait'],
},
hide: { reviewTemplateID: [''] },
},
options: [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Set a title for this review request. Read more about it <a href="https://docs.gotohuman.com/send-requests">here</a>.',
},
{
displayName: 'Auto Approve',
name: 'autoApprove',
type: 'boolean',
default: false,
description: 'Whether to automatically approve this request. Read more about it <a href="https://docs.gotohuman.com/send-requests">here</a>.',
},
{
displayName: 'Workflow Info (Legacy)',
name: 'workflow',
type: 'json',
default: '',
placeholder: '{"runId": "123456", "runName": "My Workflow", "prevSteps": ["1234567890"]}',
description: 'Send to connect multiple review steps. Read more about it <a href="https://docs.gotohuman.com/send-requests#workflow-metadata">here</a>.',
},
{
displayName: 'Update for Review ID (Legacy)',
name: 'updateForReviewId',
type: 'string',
default: '',
description: 'To update a specific review, enter the review ID here. Read more about it <a href="https://docs.gotohuman.com/retries">here</a>.',
},
],
}
]
};
this.methods = {
listSearch: {
async searchAgents(filter) {
const options = {
method: 'GET',
url: `${helpers_1.BASE_URL}/fetchN8nAgents`,
json: true,
};
const agentsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
if (agentsResponse === undefined) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No agents found. Please create one first in our web app.');
}
return {
results: ((agentsResponse === null || agentsResponse === void 0 ? void 0 : agentsResponse.agents) || [])
.filter((agent) => !filter || agent.label.toLowerCase().includes(filter.toLowerCase()))
.map((agent) => ({
name: agent.label,
value: agent.value,
})),
};
},
async searchReviewTemplates(filter) {
const options = {
method: 'GET',
url: `${helpers_1.BASE_URL}/fetchN8nForms`,
json: true,
};
const reviewTemplates = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
if (reviewTemplates === undefined) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No review templates found. Please create one first in our web app.');
}
return {
results: ((reviewTemplates === null || reviewTemplates === void 0 ? void 0 : reviewTemplates.forms) || [])
.filter((template) => !filter || template.label.toLowerCase().includes(filter.toLowerCase()))
.map((template) => ({
name: template.label,
value: template.value,
})),
};
},
},
resourceMapping: {
async getMappingFields() {
const reviewTemplateObj = this.getNodeParameter('reviewTemplateID', 0);
if (!reviewTemplateObj)
return { fields: [] };
const { value: reviewTemplateID } = reviewTemplateObj;
const options = {
method: 'GET',
url: `${helpers_1.BASE_URL}/fetchN8nFields`,
qs: {
formId: reviewTemplateID,
},
json: true,
};
const templateFieldsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
if ((templateFieldsResponse === null || templateFieldsResponse === void 0 ? void 0 : templateFieldsResponse.fields) === undefined) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No fields found for review template. Please add some fields in our web editor first.');
}
const fieldSpecs = templateFieldsResponse.fields;
return {
fields: fieldSpecs
};
}
}
};
this.webhookMethods = helpers_1.gotoHumanWebhookMethods;
this.webhook = async function () {
const bodyData = this.getBodyData();
return {
workflowData: [this.helpers.returnJsonArray(bodyData)],
};
};
}
async execute() {
var _a, _b;
const items = this.getInputData();
const returnData = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
let waitMillis = 14 * 24 * 60 * 60 * 1000;
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'reviewRequest' && (operation === 'sendAndWait' || operation === 'send')) {
const agentID = this.getNodeParameter('agentID', i);
const reviewTemplateID = this.getNodeParameter('reviewTemplateID', i);
const useSendRawReviewData = this.isNodeFeatureEnabled('sendRawReviewData');
let parsedFields;
let parsedConfig;
if (useSendRawReviewData) {
const reviewDataRaw = this.getNodeParameter('reviewData', i);
if (typeof reviewDataRaw === 'string') {
try {
parsedFields = (0, n8n_workflow_1.jsonParse)(reviewDataRaw);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'reviewData is not valid JSON', error);
}
}
else {
parsedFields = reviewDataRaw;
}
const reviewConfigRaw = this.getNodeParameter('reviewConfig', i);
if (reviewConfigRaw) {
if (typeof reviewConfigRaw === 'string') {
try {
parsedConfig = (0, n8n_workflow_1.jsonParse)(reviewConfigRaw);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'reviewConfig is not valid JSON', error);
}
}
else {
parsedConfig = reviewConfigRaw;
}
}
}
else {
const fieldsParam = this.getNodeParameter('fields', i);
parsedFields = { ...(fieldsParam.value || {}) };
const schemaList = fieldsParam.schema;
if (fieldsParam.value && schemaList) {
for (const field of schemaList) {
const val = fieldsParam.value[field.id];
if ((field.type === 'array' || field.type === 'object') && typeof val === 'string') {
try {
parsedFields[field.id] = (0, n8n_workflow_1.jsonParse)(val);
}
catch {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Could not parse field '${field.id}' as JSON: ${val}`, { itemIndex: i });
}
}
}
}
}
const metaSelect = this.getNodeParameter('metaSelect', i);
let meta;
if (metaSelect === 'json') {
const metaJson = this.getNodeParameter('metaJson', i);
if (metaJson) {
try {
meta = (0, n8n_workflow_1.jsonParse)(metaJson);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Not a valid JSON object', error);
}
}
}
else if (metaSelect === 'keyValue') {
const metaKeyValues = this.getNodeParameter('metaKeyValues', i);
if (metaKeyValues && Array.isArray(metaKeyValues.metaArray)) {
meta = {};
for (const pair of metaKeyValues.metaArray) {
meta[pair.key] = pair.value;
}
}
}
const assignToSelect = this.getNodeParameter('assignToSelect', i);
let assignTo;
if (assignToSelect === 'selectByEmail') {
const assignToParam = this.getNodeParameter('assignTo', i);
if (assignToParam && Array.isArray(assignToParam.values)) {
assignTo = assignToParam.values.map((v) => v.email);
}
}
const additionalFields = this.getNodeParameter('additionalFields', i);
const body = {
formId: reviewTemplateID.value,
fields: parsedFields,
origin: 'n8n',
originV: this.getNode().typeVersion,
};
if (parsedConfig !== undefined)
body.config = parsedConfig;
if (agentID === null || agentID === void 0 ? void 0 : agentID.value)
body.agentId = agentID.value;
if (meta !== undefined)
body.meta = meta;
if (assignTo !== undefined)
body.assignTo = assignTo;
if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.title)
body.title = additionalFields.title;
if ((additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.autoApprove) !== undefined)
body.autoApprove = additionalFields.autoApprove;
if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.workflow) {
try {
body.workflow = (0, n8n_workflow_1.jsonParse)(additionalFields.workflow);
}
catch {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'workflow field is not valid JSON', { itemIndex: i });
}
}
if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.updateForReviewId) {
body.updateForReviewId = additionalFields.updateForReviewId;
}
const sessionId = this.getNodeParameter('sessionId', i);
if (sessionId)
body.sessionId = sessionId;
if (operation === 'sendAndWait') {
const resumeUrl = this.evaluateExpression('{{ $execution?.resumeUrl }}', i);
const nodeId = this.evaluateExpression('{{ $nodeId }}', i);
body.webhookUrl = `${resumeUrl}/${nodeId}`;
}
const options = {
method: 'POST',
url: `${helpers_1.BASE_URL}/requestReview`,
body,
json: true,
returnFullResponse: true,
ignoreHttpStatusErrors: true,
};
let responseData;
try {
responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
}
catch (error) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex: i });
}
const statusCode = (responseData === null || responseData === void 0 ? void 0 : responseData.statusCode) || 200;
if (String(statusCode).startsWith('4') || String(statusCode).startsWith('5')) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData, {
message: responseData.body ? responseData.body : JSON.stringify(responseData),
httpCode: String(statusCode),
itemIndex: i,
});
}
if (((_a = responseData === null || responseData === void 0 ? void 0 : responseData.body) === null || _a === void 0 ? void 0 : _a.timeoutInMillis) && typeof responseData.body.timeoutInMillis === 'number' && responseData.body.timeoutInMillis > 0) {
waitMillis = responseData.body.timeoutInMillis;
}
returnData.push({ json: responseData, pairedItem: { item: i } });
}
else if (resource === 'reviewRequest' && operation === 'delete') {
const reviewId = this.getNodeParameter('reviewId', i);
const options = {
method: 'DELETE',
url: `${helpers_1.BASE_URL}/deleteReview`,
body: { reviewId },
json: true,
};
const responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
returnData.push({ json: responseData !== null && responseData !== void 0 ? responseData : { success: true }, pairedItem: { item: i } });
}
else if (resource === 'message' && operation === 'sendMessage') {
const agentID = this.getNodeParameter('agentID', i);
const sessionId = this.getNodeParameter('sessionId', i);
const message = this.getNodeParameter('message', i);
const body = {
sessionId,
message,
origin: 'n8n',
originV: this.getNode().typeVersion,
};
if (agentID === null || agentID === void 0 ? void 0 : agentID.value)
body.agentId = agentID.value;
const options = {
method: 'POST',
url: `${helpers_1.BASE_URL}/sendMessageToHuman`,
body,
json: true,
returnFullResponse: true,
ignoreHttpStatusErrors: true,
};
let responseData;
try {
responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options);
}
catch (error) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex: i });
}
const statusCode = (responseData === null || responseData === void 0 ? void 0 : responseData.statusCode) || 200;
if (String(statusCode).startsWith('4') || String(statusCode).startsWith('5')) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData, {
message: responseData.body ? responseData.body : JSON.stringify(responseData),
httpCode: String(statusCode),
itemIndex: i,
});
}
returnData.push({ json: responseData, pairedItem: { item: i } });
}
}
catch (err) {
if (this.continueOnFail()) {
const errorMessage = err instanceof Error ? err.message : String(err);
returnData.push({
json: { error: errorMessage },
error: err,
pairedItem: { item: i },
});
continue;
}
if (err instanceof n8n_workflow_1.NodeApiError) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: err.message }, {
message: err.message,
httpCode: (_b = err.httpCode) !== null && _b !== void 0 ? _b : undefined,
itemIndex: i,
});
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), err.message || 'An unknown error occurred', { itemIndex: i });
}
}
}
if (operation === 'sendAndWait') {
const waitTill = new Date(new Date().getTime() + waitMillis);
await this.putExecutionToWait(waitTill);
return [this.getInputData()];
}
return [returnData];
}
}
exports.GotoHuman = GotoHuman;
//# sourceMappingURL=GotoHuman.node.js.map