2025-03-20 01:32:55 +08:00
import { FieldType , IDataObject , IExecuteSingleFunctions , IHttpRequestMethods , IHttpRequestOptions , ILoadOptionsFunctions , INodePropertyOptions , INodeType , INodeTypeDescription , NodeConnectionType , ResourceMapperField , ResourceMapperFields } from 'n8n-workflow' ;
2025-04-15 01:43:53 +08:00
import https from 'https' ;
2025-04-23 23:49:03 -07:00
import http from 'http' ;
2025-04-15 01:43:53 +08:00
import { URL } from 'url' ;
async function makeRequest ( url : string , options : any = { } ) : Promise < any > {
return new Promise ( ( resolve , reject ) = > {
const parsedUrl = new URL ( url ) ;
2025-04-23 23:49:03 -07:00
const transport = parsedUrl . protocol === 'https:' ? https : http ;
2025-04-15 01:43:53 +08:00
const requestOptions = {
hostname : parsedUrl.hostname ,
path : parsedUrl.pathname + parsedUrl . search ,
2025-04-30 11:10:14 -07:00
port : parsedUrl.port || ( parsedUrl . protocol === 'https:' ? 443 : 80 ) ,
2025-04-15 01:43:53 +08:00
method : options.method || 'GET' ,
headers : options.headers || { } ,
} ;
2025-04-23 23:49:03 -07:00
const req = transport . request ( requestOptions , ( res ) = > {
2025-04-15 01:43:53 +08:00
let data = '' ;
2025-04-30 11:10:14 -07:00
2025-04-15 01:43:53 +08:00
res . on ( 'data' , ( chunk ) = > {
data += chunk ;
} ) ;
2025-04-30 11:10:14 -07:00
2025-04-15 01:43:53 +08:00
res . on ( 'end' , ( ) = > {
if ( res . statusCode && res . statusCode >= 200 && res . statusCode < 300 ) {
const response = {
ok : true ,
status : res.statusCode ,
statusText : res.statusMessage || '' ,
headers : res.headers ,
json : ( ) = > {
try {
return Promise . resolve ( JSON . parse ( data ) ) ;
} catch ( e ) {
return Promise . reject ( new Error ( 'Invalid JSON response' ) ) ;
}
} ,
text : ( ) = > Promise . resolve ( data ) ,
blob : ( ) = > Promise . resolve ( new Blob ( [ data ] ) ) ,
arrayBuffer : ( ) = > Promise . resolve ( Buffer . from ( data ) ) ,
clone : ( ) = > response ,
} ;
resolve ( response ) ;
} else {
reject ( new Error ( ` Request failed with status code ${ res . statusCode } ` ) ) ;
}
} ) ;
} ) ;
2025-04-30 11:10:14 -07:00
2025-04-15 01:43:53 +08:00
req . on ( 'error' , ( error ) = > {
reject ( error ) ;
} ) ;
2025-04-30 11:10:14 -07:00
2025-04-15 01:43:53 +08:00
if ( options . body ) {
req . write ( options . body ) ;
}
2025-04-30 11:10:14 -07:00
2025-04-15 01:43:53 +08:00
req . end ( ) ;
} ) ;
}
2025-03-20 01:32:55 +08:00
export class Skyvern implements INodeType {
description : INodeTypeDescription = {
displayName : 'Skyvern' ,
name : 'skyvern' ,
icon : 'file:skyvern.png' , // eslint-disable-line
group : [ 'transform' ] ,
description : 'Node to interact with Skyvern' ,
defaults : {
name : 'Skyvern' ,
} ,
inputs : [ NodeConnectionType . Main ] , // eslint-disable-line
outputs : [ NodeConnectionType . Main ] , // eslint-disable-line
credentials : [
{
name : 'skyvernApi' ,
required : true ,
} ,
] ,
properties : [
{
displayName : 'Resource' ,
name : 'resource' ,
type : 'options' ,
2025-04-30 11:10:14 -07:00
noDataExpression : true ,
2025-03-20 01:32:55 +08:00
options : [
{
name : 'Task' ,
value : 'task' ,
} ,
{
name : 'Workflow' ,
value : 'workflow' ,
} ,
] ,
default : 'task' ,
} ,
{
displayName : 'Operation' ,
name : 'taskOperation' ,
type : 'options' ,
required : true ,
default : 'dispatch' ,
options : [
{
name : 'Dispatch a Task' ,
value : 'dispatch' ,
description : 'Dispatch a task to execute asynchronously' ,
} ,
{
name : 'Get a Task' ,
value : 'get' ,
description : 'Get a task by ID' ,
} ,
] ,
displayOptions : {
show : {
resource : [ 'task' ] ,
} ,
} ,
routing : {
request : {
baseURL : '={{$credentials.baseUrl}}' ,
method : '={{ $value === "dispatch" ? "POST" : "GET" }}' as IHttpRequestMethods ,
2025-05-22 22:15:20 +08:00
url : '={{"/v1/run/tasks"}}' ,
2025-03-20 01:32:55 +08:00
} ,
send : {
preSend : [
async function ( this : IExecuteSingleFunctions , requestOptions : IHttpRequestOptions ) : Promise < IHttpRequestOptions > {
const taskOperation = this . getNodeParameter ( 'taskOperation' ) ;
if ( taskOperation === "get" ) return requestOptions ;
const taskOptions : IDataObject = this . getNodeParameter ( 'taskOptions' ) as IDataObject ;
2025-05-22 22:15:20 +08:00
const legacy_engine = taskOptions [ "engine" ] as string | null
if ( legacy_engine === "v1" ) {
( requestOptions . body as IDataObject ) [ 'engine' ] = "skyvern-1.0" ;
} else if ( legacy_engine === "v2" ) {
( requestOptions . body as IDataObject ) [ 'engine' ] = "skyvern-2.0" ;
2025-04-30 11:10:14 -07:00
}
2025-03-20 01:32:55 +08:00
return requestOptions ;
} ,
] ,
} ,
} ,
} ,
{
displayName : 'User Prompt' ,
description : 'The prompt for Skyvern to execute' ,
name : 'userPrompt' ,
type : 'string' ,
required : true ,
default : '' ,
placeholder : 'eg: Navigate to the Hacker News homepage and get the top 3 posts.' ,
displayOptions : {
show : {
resource : [ 'task' ] ,
taskOperation : [ 'dispatch' ] ,
} ,
} ,
routing : {
request : {
body : {
2025-05-22 22:15:20 +08:00
prompt : '={{$value}}' ,
2025-03-20 01:32:55 +08:00
} ,
} ,
} ,
} ,
{
displayName : 'URL' ,
description : 'The URL to navigate to' ,
name : 'url' ,
type : 'string' ,
default : '' ,
placeholder : 'eg: https://news.ycombinator.com/' ,
displayOptions : {
show : {
resource : [ 'task' ] ,
taskOperation : [ 'dispatch' ] ,
} ,
} ,
routing : {
request : {
body : {
url : '={{$value ? $value : null}}' ,
} ,
} ,
} ,
} ,
2025-04-30 11:10:14 -07:00
{
displayName : 'Webhook Callback URL' ,
description : 'Optional URL that Skyvern will call when the task finishes' ,
name : 'webhookUrl' ,
type : 'string' ,
default : '' ,
placeholder : 'https://example.com/webhook' ,
displayOptions : {
show : {
resource : [ 'task' ] ,
taskOperation : [ 'dispatch' ] ,
} ,
} ,
routing : {
request : {
body : {
2025-05-22 22:15:20 +08:00
webhook_url : '={{$value ? $value : null}}' ,
2025-04-30 11:10:14 -07:00
} ,
} ,
} ,
} ,
2025-03-20 01:32:55 +08:00
{
displayName : 'Task ID' ,
description : 'The ID of the task' ,
name : 'taskId' ,
type : 'string' ,
required : true ,
default : '' ,
displayOptions : {
show : {
resource : [ 'task' ] ,
taskOperation : [ 'get' ] ,
} ,
} ,
routing : {
request : {
method : 'GET' ,
2025-05-22 22:15:20 +08:00
url : '={{"/v1/runs/" + $value}}' ,
2025-03-20 01:32:55 +08:00
} ,
} ,
} ,
{
displayName : 'Task Options' ,
name : 'taskOptions' ,
type : 'collection' ,
description : 'Optional Configuration for the task' ,
placeholder : 'Add Task Options' ,
default : { } ,
options : [
{
2025-05-22 22:15:20 +08:00
displayName : 'Engine(Deprecated)' ,
description : 'Deprecated: please migrate to use "Engine" option' ,
2025-03-20 01:32:55 +08:00
name : 'engine' ,
type : 'options' ,
2025-05-22 22:15:20 +08:00
default : '' ,
2025-03-20 01:32:55 +08:00
options : [
{
name : 'TaskV1' ,
value : 'v1' ,
} ,
{
name : 'TaskV2' ,
value : 'v2' ,
} ,
2025-05-23 02:14:31 +08:00
{
name : 'THIS FIELD IS DEPRECATED' ,
value : '' ,
} ,
2025-03-20 01:32:55 +08:00
] ,
} ,
2025-05-22 22:15:20 +08:00
{
displayName : 'Engine' ,
name : 'runEngine' ,
type : 'options' ,
default : 'skyvern-2.0' ,
options : [
{
name : 'Skyvern 1.0' ,
value : 'skyvern-1.0' ,
} ,
{
name : 'Skyvern 2.0' ,
value : 'skyvern-2.0' ,
} ,
{
name : 'OpenAI CUA' ,
value : 'openai-cua' ,
} ,
{
name : 'Anthropic CUA' ,
value : 'anthropic-cua' ,
}
] ,
routing : {
request : {
body : {
engine : '={{$value}}' ,
} ,
} ,
} ,
}
2025-03-20 01:32:55 +08:00
] ,
displayOptions : {
show : {
resource : [ 'task' ] ,
2025-05-22 22:15:20 +08:00
taskOperation : [ 'dispatch' ] ,
2025-03-20 01:32:55 +08:00
} ,
} ,
} ,
{
displayName : 'Workflow Title or ID' , // eslint-disable-line
description : 'The title of the workflow. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.' ,
name : 'workflowId' ,
type : 'options' ,
typeOptions : {
loadOptionsMethod : 'getWorkflows' ,
loadOptionsDependsOn : [ 'resource' ] ,
} ,
required : true ,
default : '' ,
displayOptions : {
show : {
resource : [ 'workflow' ] ,
} ,
} ,
} ,
{
displayName : 'Workflow Operation' ,
name : 'workflowOperation' ,
type : 'options' ,
required : true ,
default : 'get' ,
options : [
{
name : 'Get a Workflow Run' ,
value : 'get' ,
description : 'Get a workflow run by ID' ,
} ,
{
name : 'Dispatch a Workflow Run' ,
value : 'dispatch' ,
description : 'Dispatch a workflow run to execute asynchronously' ,
} ,
] ,
displayOptions : {
show : {
resource : [ 'workflow' ] ,
} ,
} ,
routing : {
request : {
baseURL : '={{$credentials.baseUrl}}' ,
method : '={{ $value === "dispatch" ? "POST" : "GET" }}' as IHttpRequestMethods ,
} ,
} ,
} ,
{
displayName : 'Workflow Run ID' ,
description : 'The ID of the workflow run' ,
name : 'workflowRunId' ,
type : 'string' ,
required : true ,
default : '' ,
displayOptions : {
show : {
resource : [ 'workflow' ] ,
workflowOperation : [ 'get' ] ,
} ,
} ,
routing : {
request : {
url : '={{"/api/v1/workflows/" + $parameter["workflowId"] + "/runs/" + $value}}' ,
} ,
} ,
} ,
{
displayName : 'Workflow Run Parameters' ,
name : 'workflowRunParameters' ,
type : 'resourceMapper' ,
noDataExpression : true ,
description : 'The JSON-formatted parameters to pass the workflow run to execute' ,
required : true ,
default : {
mappingMode : 'defineBelow' ,
value : null ,
} ,
displayOptions : {
show : {
resource : [ 'workflow' ] ,
workflowOperation : [ 'dispatch' ] ,
} ,
} ,
typeOptions : {
loadOptionsDependsOn : [ 'workflowId' ] ,
resourceMapper : {
resourceMapperMethod : 'getWorkflowRunParameters' ,
mode : 'update' ,
fieldWords : {
singular : 'workflowRunParameter' ,
plural : 'workflowRunParameters' ,
} ,
addAllFields : true ,
multiKeyMatch : true ,
} ,
} ,
routing : {
request : {
url : '={{"/api/v1/workflows/" + $parameter["workflowId"] + "/run"}}' ,
body : {
data : '={{$value["value"]}}' ,
} ,
} ,
} ,
} ,
2025-04-30 11:10:14 -07:00
{
displayName : 'Webhook Callback URL' ,
description : 'Optional URL that Skyvern will call when the workflow run finishes' ,
name : 'webhookCallbackUrl' ,
type : 'string' ,
default : '' ,
placeholder : 'https://example.com/webhook' ,
displayOptions : {
show : {
resource : [ 'workflow' ] ,
workflowOperation : [ 'dispatch' ] ,
} ,
} ,
routing : {
request : {
body : {
2025-05-22 22:15:20 +08:00
webhook_callback_url : '={{$value ? $value : null}}' ,
2025-04-30 11:10:14 -07:00
} ,
} ,
} ,
} ,
2025-03-20 01:32:55 +08:00
] ,
version : 1 ,
} ;
2025-04-30 11:10:14 -07:00
2025-03-20 01:32:55 +08:00
methods = {
loadOptions : {
async getWorkflows ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const resource = this . getCurrentNodeParameter ( 'resource' ) as string ;
if ( resource !== 'workflow' ) return [ ] ;
const credentials = await this . getCredentials ( 'skyvernApi' ) ;
2025-04-15 01:43:53 +08:00
const response = await makeRequest ( credentials [ 'baseUrl' ] + '/api/v1/workflows?page_size=100' , {
2025-03-20 01:32:55 +08:00
headers : {
'x-api-key' : credentials [ 'apiKey' ] ,
} ,
} ) ;
if ( ! response . ok ) {
throw new Error ( 'Request to get workflows failed' ) ; // eslint-disable-line
}
const data = await response . json ( ) ;
return data . map ( ( workflow : any ) = > ( {
name : workflow.title ,
value : workflow.workflow_permanent_id ,
} ) ) ;
} ,
} ,
resourceMapping : {
async getWorkflowRunParameters ( this : ILoadOptionsFunctions ) : Promise < ResourceMapperFields > {
const resource = this . getCurrentNodeParameter ( 'resource' ) as string ;
if ( resource !== 'workflow' ) return { fields : [ ] } ;
const workflowOperation = this . getCurrentNodeParameter ( 'workflowOperation' ) as string ;
if ( workflowOperation !== 'dispatch' ) return { fields : [ ] } ;
2025-04-30 11:10:14 -07:00
2025-03-20 01:32:55 +08:00
const workflowId = this . getCurrentNodeParameter ( 'workflowId' ) as string ;
if ( ! workflowId ) return { fields : [ ] } ;
const credentials = await this . getCredentials ( 'skyvernApi' ) ;
2025-04-15 01:43:53 +08:00
const response = await makeRequest ( credentials [ 'baseUrl' ] + '/api/v1/workflows/' + workflowId , {
2025-03-20 01:32:55 +08:00
headers : {
'x-api-key' : credentials [ 'apiKey' ] ,
} ,
} ) ;
if ( ! response . ok ) {
throw new Error ( 'Request to get workflow failed' ) ; // eslint-disable-line
}
const workflow = await response . json ( ) ;
const parameters : any [ ] = workflow . workflow_definition . parameters ;
const fields : ResourceMapperField [ ] = await Promise . all (
2025-04-30 11:10:14 -07:00
parameters
. filter ( ( parameter : any ) = > parameter . parameter_type === 'workflow' || parameter . parameter_type === 'credential' )
. map ( async ( parameter : any ) = > {
let options : INodePropertyOptions [ ] | undefined = undefined ;
let parameterType : FieldType | undefined = undefined ;
if ( parameter . parameter_type === 'credential' ) {
const credResponse = await makeRequest ( credentials [ 'baseUrl' ] + '/api/v1/credentials' , {
headers : {
'x-api-key' : credentials [ 'apiKey' ] ,
} ,
} ) ;
if ( ! credResponse . ok ) {
2025-05-13 22:14:41 +08:00
throw new Error ( 'Request to get credentials failed' ) ; // eslint-disable-line
2025-04-30 11:10:14 -07:00
}
const credData = await credResponse . json ( ) ;
options = credData . map ( ( credential : any ) = > ( {
name : credential.name ,
value : credential.credential_id ,
} ) ) ;
parameterType = 'options' ;
} else {
const parameter_type_map : Record < string , FieldType > = {
string : 'string' ,
integer : 'number' ,
float : 'number' ,
boolean : 'boolean' ,
2025-05-13 22:14:41 +08:00
json : 'object' ,
2025-04-30 11:10:14 -07:00
file_url : 'url' ,
}
parameterType = parameter_type_map [ parameter . workflow_parameter_type ] ;
2025-03-20 01:32:55 +08:00
}
2025-04-30 11:10:14 -07:00
return {
id : parameter.key ,
displayName : parameter.key ,
defaultMatch : true ,
canBeUsedToMatch : false ,
required : parameter.default_value === undefined || parameter . default_value === null ,
display : true ,
type : parameterType ,
options : options ,
} ;
} )
2025-03-20 01:32:55 +08:00
) ;
// HACK: If there are no parameters, add a empty field to avoid the resource mapper from crashing
if ( fields . length === 0 ) {
fields . push ( {
id : 'NO_PARAMETERS' ,
displayName : 'No Parameters' ,
defaultMatch : false ,
canBeUsedToMatch : false ,
required : false ,
display : true ,
type : 'string' ,
} ) ;
}
return {
fields : fields ,
}
} ,
} ,
}
2025-04-23 23:49:03 -07:00
}