3
0

add(crm): api spec for workflow jobs

This commit is contained in:
Tit Petric 2018-12-05 16:51:40 +01:00
parent 101c322299
commit 73728bcf61
13 changed files with 1042 additions and 4 deletions

View File

@ -91,7 +91,7 @@
"name": "get",
"method": "GET",
"path": "/{workflowID}",
"title": "Get existing workflow details",
"title": "Get workflow details",
"parameters": {
"path": [
{
@ -163,6 +163,188 @@
}
]
},
{
"title": "Jobs",
"description": "Workflow Jobs",
"package": "crm",
"entrypoint": "job",
"path": "/job",
"authentication": [],
"struct": [
{
"imports": [
"github.com/crusttech/crust/crm/types",
"sqlxTypes github.com/jmoiron/sqlx/types"
]
}
],
"apis": [
{
"name": "list",
"method": "GET",
"path": "/",
"title": "List jobs",
"parameters": {
"get": [
{
"type": "string",
"name": "status",
"required": false,
"title": "Job status (`ok`, `error`, `running`, `cancelled` or `queued`)"
},
{
"name": "page",
"type": "int",
"required": false,
"title": "Page number (0 based)"
},
{
"name": "perPage",
"type": "int",
"required": false,
"title": "Returned items per page (default 50)"
}
]
}
},
{
"name": "run",
"path": "/",
"method": "POST",
"title": "Create a new job",
"parameters": {
"post": [
{
"type": "string",
"name": "workflowID",
"required": true,
"title": "Workflow ID"
},
{
"type": "string",
"name": "startAt",
"required": false,
"title": "Start datetime for a delayed job"
},
{
"type": "types.JobParameterSet",
"name": "parameters",
"required": false,
"title": "Extra job parameters (map[string]string)"
}
]
}
},
{
"name": "get",
"method": "GET",
"path": "/{jobID}",
"title": "Get job details",
"parameters": {
"path": [
{
"type": "string",
"name": "jobID",
"required": true,
"title": "Job ID"
}
]
}
},
{
"name": "logs",
"method": "GET",
"path": "/{jobID}/logs",
"title": "Get job logs",
"parameters": {
"path": [
{
"type": "string",
"name": "jobID",
"required": true,
"title": "Job ID"
},
{
"name": "page",
"type": "int",
"required": false,
"title": "Page number (0 based)"
},
{
"name": "perPage",
"type": "int",
"required": false,
"title": "Returned items per page (default 50)"
}
]
}
},
{
"name": "update",
"method": "POST",
"path": "/{jobID}",
"title": "Update job details",
"parameters": {
"path": [
{
"type": "string",
"name": "jobID",
"required": true,
"title": "Job ID"
}
],
"post": [
{
"type": "string",
"name": "status",
"required": false,
"title": "Job status (`ok`, `error`, `running`, `cancelled` or `queued`)"
},
{
"type": "sqlxTypes.JSONText",
"name": "log",
"required": false,
"title": "Job log item (append-only)"
},
{
"type": "string",
"name": "workflowID",
"required": false,
"title": "Workflow ID"
},
{
"type": "string",
"name": "startAt",
"required": false,
"title": "Start datetime for a delayed job"
},
{
"type": "types.JobParameterSet",
"name": "parameters",
"required": false,
"title": "Extra job parameters (map[string]string)"
}
]
}
},
{
"name": "delete",
"method": "DELETE",
"path": "/{jobID}",
"title": "Cancel job",
"parameters": {
"path": [
{
"type": "string",
"name": "jobID",
"required": true,
"title": "Job ID"
}
]
}
}
]
},
{
"title": "Pages",
"description": "CRM module pages",

184
api/crm/spec/job.json Normal file
View File

@ -0,0 +1,184 @@
{
"Title": "Jobs",
"Description": "Workflow Jobs",
"Package": "crm",
"Interface": "Job",
"Struct": [
{
"imports": [
"github.com/crusttech/crust/crm/types",
"sqlxTypes github.com/jmoiron/sqlx/types"
]
}
],
"Parameters": null,
"Protocol": "",
"Authentication": [],
"Path": "/job",
"APIs": [
{
"Name": "list",
"Method": "GET",
"Title": "List jobs",
"Path": "/",
"Parameters": {
"get": [
{
"name": "status",
"required": false,
"title": "Job status (`ok`, `error`, `running`, `cancelled` or `queued`)",
"type": "string"
},
{
"name": "page",
"required": false,
"title": "Page number (0 based)",
"type": "int"
},
{
"name": "perPage",
"required": false,
"title": "Returned items per page (default 50)",
"type": "int"
}
]
}
},
{
"Name": "run",
"Method": "POST",
"Title": "Create a new job",
"Path": "/",
"Parameters": {
"post": [
{
"name": "workflowID",
"required": true,
"title": "Workflow ID",
"type": "string"
},
{
"name": "startAt",
"required": false,
"title": "Start datetime for a delayed job",
"type": "string"
},
{
"name": "parameters",
"required": false,
"title": "Extra job parameters (map[string]string)",
"type": "types.JobParameterSet"
}
]
}
},
{
"Name": "get",
"Method": "GET",
"Title": "Get job details",
"Path": "/{jobID}",
"Parameters": {
"path": [
{
"name": "jobID",
"required": true,
"title": "Job ID",
"type": "string"
}
]
}
},
{
"Name": "logs",
"Method": "GET",
"Title": "Get job logs",
"Path": "/{jobID}/logs",
"Parameters": {
"path": [
{
"name": "jobID",
"required": true,
"title": "Job ID",
"type": "string"
},
{
"name": "page",
"required": false,
"title": "Page number (0 based)",
"type": "int"
},
{
"name": "perPage",
"required": false,
"title": "Returned items per page (default 50)",
"type": "int"
}
]
}
},
{
"Name": "update",
"Method": "POST",
"Title": "Update job details",
"Path": "/{jobID}",
"Parameters": {
"path": [
{
"name": "jobID",
"required": true,
"title": "Job ID",
"type": "string"
}
],
"post": [
{
"name": "status",
"required": false,
"title": "Job status (`ok`, `error`, `running`, `cancelled` or `queued`)",
"type": "string"
},
{
"name": "log",
"required": false,
"title": "Job log item (append-only)",
"type": "sqlxTypes.JSONText"
},
{
"name": "workflowID",
"required": false,
"title": "Workflow ID",
"type": "string"
},
{
"name": "startAt",
"required": false,
"title": "Start datetime for a delayed job",
"type": "string"
},
{
"name": "parameters",
"required": false,
"title": "Extra job parameters (map[string]string)",
"type": "types.JobParameterSet"
}
]
}
},
{
"Name": "delete",
"Method": "DELETE",
"Title": "Cancel job",
"Path": "/{jobID}",
"Parameters": {
"path": [
{
"name": "jobID",
"required": true,
"title": "Job ID",
"type": "string"
}
]
}
}
]
}

View File

@ -59,7 +59,7 @@
{
"Name": "get",
"Method": "GET",
"Title": "Get existing workflow details",
"Title": "Get workflow details",
"Path": "/{workflowID}",
"Parameters": {
"path": [

29
crm/repository/job.go Normal file
View File

@ -0,0 +1,29 @@
package repository
import (
"context"
"github.com/titpetric/factory"
_ "github.com/crusttech/crust/crm/types"
)
type (
JobRepository interface {
With(ctx context.Context, db *factory.DB) JobRepository
}
job struct {
*repository
}
)
func Job(ctx context.Context, db *factory.DB) JobRepository {
return (&job{}).With(ctx, db)
}
func (r *job) With(ctx context.Context, db *factory.DB) JobRepository {
return &job{
repository: r.repository.With(ctx, db),
}
}

107
crm/rest/handlers/job.go Normal file
View File

@ -0,0 +1,107 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `job.go`, `job.util.go` or `job_test.go` to
implement your API calls, helper functions and tests. The file `job.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"github.com/go-chi/chi"
"net/http"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/crm/rest/request"
)
// Internal API interface
type JobAPI interface {
List(context.Context, *request.JobList) (interface{}, error)
Run(context.Context, *request.JobRun) (interface{}, error)
Get(context.Context, *request.JobGet) (interface{}, error)
Logs(context.Context, *request.JobLogs) (interface{}, error)
Update(context.Context, *request.JobUpdate) (interface{}, error)
Delete(context.Context, *request.JobDelete) (interface{}, error)
}
// HTTP API interface
type Job struct {
List func(http.ResponseWriter, *http.Request)
Run func(http.ResponseWriter, *http.Request)
Get func(http.ResponseWriter, *http.Request)
Logs func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
}
func NewJob(jh JobAPI) *Job {
return &Job{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.List(r.Context(), params)
})
},
Run: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobRun()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.Run(r.Context(), params)
})
},
Get: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobGet()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.Get(r.Context(), params)
})
},
Logs: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobLogs()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.Logs(r.Context(), params)
})
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobUpdate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.Update(r.Context(), params)
})
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewJobDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return jh.Delete(r.Context(), params)
})
},
}
}
func (jh *Job) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/job", func(r chi.Router) {
r.Get("/", jh.List)
r.Post("/", jh.Run)
r.Get("/{jobID}", jh.Get)
r.Get("/{jobID}/logs", jh.Logs)
r.Post("/{jobID}", jh.Update)
r.Delete("/{jobID}", jh.Delete)
})
})
}

46
crm/rest/job.go Normal file
View File

@ -0,0 +1,46 @@
package rest
import (
"context"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
)
var _ = errors.Wrap
type (
Job struct {
job service.JobService
}
)
func (Job) New(jobSvc service.JobService) *Job {
return &Job{jobSvc}
}
func (ctrl *Job) List(ctx context.Context, r *request.JobList) (interface{}, error) {
return nil, errors.New("Not implemented: Job.list")
}
func (ctrl *Job) Run(ctx context.Context, r *request.JobRun) (interface{}, error) {
return nil, errors.New("Not implemented: Job.run")
}
func (ctrl *Job) Get(ctx context.Context, r *request.JobGet) (interface{}, error) {
return nil, errors.New("Not implemented: Job.get")
}
func (ctrl *Job) Logs(ctx context.Context, r *request.JobLogs) (interface{}, error) {
return nil, errors.New("Not implemented: Job.logs")
}
func (ctrl *Job) Update(ctx context.Context, r *request.JobUpdate) (interface{}, error) {
return nil, errors.New("Not implemented: Job.update")
}
func (ctrl *Job) Delete(ctx context.Context, r *request.JobDelete) (interface{}, error) {
return nil, errors.New("Not implemented: Job.delete")
}

340
crm/rest/request/job.go Normal file
View File

@ -0,0 +1,340 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `job.go`, `job.util.go` or `job_test.go` to
implement your API calls, helper functions and tests. The file `job.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"encoding/json"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/types"
sqlxTypes "github.com/jmoiron/sqlx/types"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Job list request parameters
type JobList struct {
Status string
Page int
PerPage int
}
func NewJobList() *JobList {
return &JobList{}
}
func (j *JobList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["status"]; ok {
j.Status = val
}
if val, ok := get["page"]; ok {
j.Page = parseInt(val)
}
if val, ok := get["perPage"]; ok {
j.PerPage = parseInt(val)
}
return err
}
var _ RequestFiller = NewJobList()
// Job run request parameters
type JobRun struct {
WorkflowID string
StartAt string
Parameters types.JobParameterSet
}
func NewJobRun() *JobRun {
return &JobRun{}
}
func (j *JobRun) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["workflowID"]; ok {
j.WorkflowID = val
}
if val, ok := post["startAt"]; ok {
j.StartAt = val
}
return err
}
var _ RequestFiller = NewJobRun()
// Job get request parameters
type JobGet struct {
JobID string
}
func NewJobGet() *JobGet {
return &JobGet{}
}
func (j *JobGet) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
j.JobID = chi.URLParam(r, "jobID")
return err
}
var _ RequestFiller = NewJobGet()
// Job logs request parameters
type JobLogs struct {
JobID string
Page int
PerPage int
}
func NewJobLogs() *JobLogs {
return &JobLogs{}
}
func (j *JobLogs) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
j.JobID = chi.URLParam(r, "jobID")
j.Page = parseInt(chi.URLParam(r, "page"))
j.PerPage = parseInt(chi.URLParam(r, "perPage"))
return err
}
var _ RequestFiller = NewJobLogs()
// Job update request parameters
type JobUpdate struct {
JobID string
Status string
Log sqlxTypes.JSONText
WorkflowID string
StartAt string
Parameters types.JobParameterSet
}
func NewJobUpdate() *JobUpdate {
return &JobUpdate{}
}
func (j *JobUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
j.JobID = chi.URLParam(r, "jobID")
if val, ok := post["status"]; ok {
j.Status = val
}
if val, ok := post["log"]; ok {
if j.Log, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
if val, ok := post["workflowID"]; ok {
j.WorkflowID = val
}
if val, ok := post["startAt"]; ok {
j.StartAt = val
}
return err
}
var _ RequestFiller = NewJobUpdate()
// Job delete request parameters
type JobDelete struct {
JobID string
}
func NewJobDelete() *JobDelete {
return &JobDelete{}
}
func (j *JobDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(j)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
j.JobID = chi.URLParam(r, "jobID")
return err
}
var _ RequestFiller = NewJobDelete()

View File

@ -15,6 +15,7 @@ func MountRoutes(jwtAuth auth.TokenEncoder) func(chi.Router) {
contentSvc = service.Content()
pageSvc = service.Page()
workflowSvc = service.Workflow()
jobSvc = service.Job()
)
var (
@ -22,6 +23,7 @@ func MountRoutes(jwtAuth auth.TokenEncoder) func(chi.Router) {
module = Module{}.New(moduleSvc, contentSvc)
page = Page{}.New(pageSvc)
workflow = Workflow{}.New(workflowSvc)
job = Job{}.New(jobSvc)
)
// Initialize handers & controllers.
@ -34,6 +36,7 @@ func MountRoutes(jwtAuth auth.TokenEncoder) func(chi.Router) {
handlers.NewPage(page).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r)
handlers.NewWorkflow(workflow).MountRoutes(r)
handlers.NewJob(job).MountRoutes(r)
})
}
}

View File

@ -2,9 +2,11 @@ package rest
import (
"context"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
"github.com/pkg/errors"
)
var _ = errors.Wrap

36
crm/service/job.go Normal file
View File

@ -0,0 +1,36 @@
package service
import (
"context"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/repository"
_ "github.com/crusttech/crust/crm/types"
)
type (
job struct {
db *factory.DB
ctx context.Context
repository repository.JobRepository
}
JobService interface {
With(ctx context.Context) JobService
}
)
func Job() JobService {
return (&job{}).With(context.Background())
}
func (s *job) With(ctx context.Context) JobService {
db := repository.DB(ctx)
return &job{
db: db,
ctx: ctx,
repository: repository.Job(ctx, db),
}
}

View File

@ -11,6 +11,7 @@ var (
DefaultModule ModuleService
DefaultPage PageService
DefaultWorkflow WorkflowService
DefaultJob JobService
)
func Init() {
@ -20,5 +21,6 @@ func Init() {
DefaultModule = Module()
DefaultPage = Page()
DefaultWorkflow = Workflow()
DefaultJob = Job()
})
}

5
crm/types/job.go Normal file
View File

@ -0,0 +1,5 @@
package types
type (
JobParameterSet map[string]string
)

View File

@ -32,6 +32,108 @@ CRM input field definitions
# Jobs
Workflow Jobs
## List jobs
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| status | string | GET | Job status (`ok`, `error`, `running`, `cancelled` or `queued`) | N/A | NO |
| page | int | GET | Page number (0 based) | N/A | NO |
| perPage | int | GET | Returned items per page (default 50) | N/A | NO |
## Create a new job
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| workflowID | string | POST | Workflow ID | N/A | YES |
| startAt | string | POST | Start datetime for a delayed job | N/A | NO |
| parameters | types.JobParameterSet | POST | Extra job parameters (map[string]string) | N/A | NO |
## Get job details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/{jobID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| jobID | string | PATH | Job ID | N/A | YES |
## Get job logs
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/{jobID}/logs` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| jobID | string | PATH | Job ID | N/A | YES |
| page | int | PATH | Page number (0 based) | N/A | NO |
| perPage | int | PATH | Returned items per page (default 50) | N/A | NO |
## Update job details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/{jobID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| jobID | string | PATH | Job ID | N/A | YES |
| status | string | POST | Job status (`ok`, `error`, `running`, `cancelled` or `queued`) | N/A | NO |
| log | sqlxTypes.JSONText | POST | Job log item (append-only) | N/A | NO |
| workflowID | string | POST | Workflow ID | N/A | NO |
| startAt | string | POST | Start datetime for a delayed job | N/A | NO |
| parameters | types.JobParameterSet | POST | Extra job parameters (map[string]string) | N/A | NO |
## Cancel job
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/job/{jobID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| jobID | string | PATH | Job ID | N/A | YES |
# Modules
CRM module definitions
@ -550,7 +652,7 @@ CRM workflow definitions
| onError | types.WorkflowTaskSet | POST | Type ID | N/A | NO |
| timeout | int | POST | Timeout in seconds | N/A | NO |
## Get existing workflow details
## Get workflow details
#### Method