How to do operations tctl can do from any programming language?

We need to start, signal, query, terminate workflow from Node JS. Since there is no SDK, I put the tctl binary in the project and call it from Node JS through shell command.
It works with some drawbacks. I have to build a string and the result/failure message is hard to parse.

What is the right way to do this? Should we add some Node grpc library and make grpc calls? Should we write a Go/Java REST service and translate REST call to operations?

I would go with the Node gRCP client library. In the future, we consider adding HTTP endpoint to Temporal frontends.

I’ve never used gRPC before. Is there any documentation on what Temporal expect the gRPC calls to look like? Is there a gRPC API doc for Temporal?

The Temporal gRPC API is found at temporalio/api Github repository.

Here is a tutorial on calling an gRPC endpoint from Node. Note that you only care about the client code there.

Also, the Temporal Web is a NodeJS application that uses a gRPC client to communicate to the Temporal service.

2 Likes

Hi @maxim, I got it working but with some strange problems on the proto3 definitions from your repo. I created a pull request Re-order import so @grpc/proto-loader works by sunshineo · Pull Request #103 · temporalio/api · GitHub

Hi @maxim, I was able to start workflow using grpc but I cannot figure out how to signal a workflow with grpc. I think I made some mistake on the request, but I cannot figure out. It just stuck there without any response, no error either. Could you help me? Thank you very much!

const request = {
    namespace: 'develop',
    workflow_execution: {
        workflow_id: 'grpc-test-order-VBLPyXIucO',
    },
    signal_name: 'updateStatus',
    input: {
        payloads: [
            {
                data: Buffer.from('new')
            }
        ]
    },
    request_id: 'grpc-test-order-VBLPyXIucO-signal',
}
client.SignalWorkflowExecution(request, callback)

Adding all the protos needed here:

message SignalWorkflowExecutionRequest {
    string namespace = 1;
    temporal.api.common.v1.WorkflowExecution workflow_execution = 2;
    string signal_name = 3;
    temporal.api.common.v1.Payloads input = 4;
    string identity = 5;
    string request_id = 6;
    string control = 7;
}

message WorkflowExecution {
    string workflow_id = 1;
    string run_id = 2;
}

message Payloads {
    repeated Payload payloads = 1;
}

message Payload {
    map<string,bytes> metadata = 1;
    bytes data = 2;
}

I’m seeing this problem:

data: Buffer.from('VBLPyXIucO')

But I got the base 64 encoded on the server side


And the worker error out

Caused by: io.temporal.common.converter.DataConverterException: when parsing:"VBLPyXIucO" into following types: [class java.lang.String]
	at io.temporal.common.converter.DefaultDataConverter.fromPayload(DefaultDataConverter.java:109)
	at io.temporal.common.converter.DataConverter.arrayFromPayloads(DataConverter.java:104)
	at io.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation.execute(POJOWorkflowImplementationFactory.java:247)
	at io.temporal.internal.sync.WorkflowExecuteRunnable.run(WorkflowExecuteRunnable.java:52)
	at io.temporal.internal.sync.SyncWorkflow.lambda$start$0(SyncWorkflow.java:126)
	at io.temporal.internal.sync.CancellationScopeImpl.run(CancellationScopeImpl.java:101)
	at io.temporal.internal.sync.WorkflowThreadImpl$RunnableWrapper.run(WorkflowThreadImpl.java:107)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	... 3 common frames omitted

You are missing the required metadata key. Here is the Java code that generates JSON payload.

Consider signaling workflow from Java or Go SDK and then look into the payload format they generate.

Thank you @maxim! How do I look into the payload Java SDK generate?
I used a break point to debug and was able to see this.
It seems to be the same with what I gave in node

const request = {
    namespace: 'develop',
    workflow_execution: {
        workflow_id: 'grpc-test-order-VBLPyXIucO-25',
    },
    signal_name: 'updateStatus',
    input: {
        payloads: [
            {
                data: Buffer.from('"node"'),
                metadata: {
                    encoding: Buffer.from('json/plain')
                }
            }
        ]
    },
    request_id: 'grpc-test-order-VBLPyXIucO-signal'
}

It looks like the problem is in base64 encoding of the payload by the Javascript. It should be a byte array.

Could you try to use the generated protobuf directly to create a request? It looks like you are generating JSON which gets converted to a protobuf. I wouldn’t be surprised that JSON doesn’t really work with the binary payloads.

Here is an example of using protobuf.

Following the tutorial I was able to generate a request directly and it looks like this

req.toObject()
{
  namespace: 'develop',
  workflowExecution: { workflowId: 'grpc-test-order-VBLPyXIucO-25', runId: '' },
  signalName: 'updateStatus',
  input: {
    payloadsList: [
      {
        metadataMap: [ [ 'encoding', <Buffer 6a 73 6f 6e 2f 70 6c 61 69 6e> ] ],
        data: 'Im5vZGUi'
      }
    ]
  },
  identity: '',
  requestId: '',
  control: ''
}
req.serializeBinary()
Uint8Array(92) [
   10,   7, 100, 101, 118, 101, 108, 111, 112,  18,  31,  10,
   29, 103, 114, 112,  99,  45, 116, 101, 115, 116,  45, 111,
  114, 100, 101, 114,  45,  86,  66,  76,  80, 121,  88,  73,
  117,  99,  79,  45,  50,  53,  26,  12, 117, 112, 100,  97,
  116, 101,  83, 116,  97, 116, 117, 115,  34,  34,  10,  32,
   10,  22,  10,   8, 101, 110,  99, 111, 100, 105, 110, 103,
   18,  10, 106, 115, 111, 110,  47, 112, 108,  97, 105, 110,
   18,   6,  34, 110, 111, 100, 101,  34
]

But if I try to use it, I got

client.SignalWorkflowExecution(req, callback)

Error: 3 INVALID_ARGUMENT: Namespace not set on request.
    at Object.callErrorFromStatus (/Users/gordon/workspace/pipe17/pipe17-group/temporal/sync-order-to-temporal/node_modules/@grpc/grpc-js/build/src/call.js:31:26)
    at Object.onReceiveStatus (/Users/gordon/workspace/pipe17/pipe17-group/temporal/sync-order-to-temporal/node_modules/@grpc/grpc-js/build/src/client.js:176:52)
    at Object.onReceiveStatus (/Users/gordon/workspace/pipe17/pipe17-group/temporal/sync-order-to-temporal/node_modules/@grpc/grpc-js/build/src/client-interceptors.js:334:141)
    at Object.onReceiveStatus (/Users/gordon/workspace/pipe17/pipe17-group/temporal/sync-order-to-temporal/node_modules/@grpc/grpc-js/build/src/client-interceptors.js:297:181)
    at /Users/gordon/workspace/pipe17/pipe17-group/temporal/sync-order-to-temporal/node_modules/@grpc/grpc-js/build/src/call-stream.js:129:78
    at processTicksAndRejections (internal/process/task_queues.js:75:11) {
  code: 3,
  details: 'Namespace not set on request.',
  metadata: Metadata {
    internalRepr: Map(1) { 'content-type' => [ 'application/grpc' ] },
    options: {}
  }
}

I was able to get it working with both static code generation and dynamic code generation. Thank you everyone for the help. I will put my sample code in a github repo and post it here

2 Likes