Repository: ovh/cds
Pull request #5902 (2021-08-17 10:19:16)

fix(hatchery): fail the job after too many attempts for the same job
Signed-off-by: francois  samin <francois.samin@corp.ovh.com>

1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


ovh-cds: 
CDS Report build-all-cds#16327.0 ✘  
* Build
  * Build and Package All ✘  
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#16329.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 
* Package
  * Docker ✔ 
* Integration
  * Docker Compose Tests ✔ 
  * Standalone Tests ✘  

ovh-cds: 
CDS Report build-all-cds#16329.1 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 
* Package
  * Docker ✔ 
* Integration
  * Docker Compose Tests ✔ 
  * Standalone Tests ✘  

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=ovh_cds&pullRequest=5902&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=ovh_cds&pullRequest=5902&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ovh_cds&pullRequest=5902&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5902&resolved=false&types=CODE_SMELL)

[![96.6%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90-16px.png '96.6%')](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5902&metric=new_coverage&view=list) [96.6% Coverage](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5902&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5902&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5902&metric=new_duplicated_lines_density&view=list)


ovh-cds: 
CDS Report build-all-cds#16335.0 ✘  
* Build
  * Build and Package All ✘  
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✘  
  * Lint UI Translate ✔ 
  * Test CLI ✘  
  * Test Engine ✘  
  * Test SDK ✘  
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#16337.0 ✘  
* Build
  * Build and Package All ✘  
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✘  
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#16338.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#16339.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#16340.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 
* Package
  * Docker ✔ 
* Integration
  * Docker Compose Tests ✔ 
  * Standalone Tests ✘  


Patch:
diff --git a/cli/cdsctl/admin_cdn.go b/cli/cdsctl/admin_cdn.go
index 28bd7ed10..0411500ef 100644
--- a/cli/cdsctl/admin_cdn.go
+++ b/cli/cdsctl/admin_cdn.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 
 	"github.com/spf13/cobra"
@@ -105,7 +104,7 @@ func adminCdnCacheLogStatusRun(_ cli.Values) (cli.ListResult, error) {
 		return nil, err
 	}
 	ts := []sdk.MonitoringStatusLine{}
-	if err := json.Unmarshal(btes, &ts); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &ts); err != nil {
 		return nil, err
 	}
 	return cli.AsListResult(ts), nil
diff --git a/cli/cdsctl/admin_cdn_unit.go b/cli/cdsctl/admin_cdn_unit.go
index e06951210..8bc92dde3 100644
--- a/cli/cdsctl/admin_cdn_unit.go
+++ b/cli/cdsctl/admin_cdn_unit.go
@@ -1,8 +1,8 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
+
 	"github.com/spf13/cobra"
 
 	"github.com/ovh/cds/cli"
@@ -34,7 +34,7 @@ func adminCdnUnitList(_ cli.Values) (cli.ListResult, error) {
 		return nil, err
 	}
 	var result []sdk.CDNUnitHandlerRequest
-	if err := json.Unmarshal(bts, &result); err != nil {
+	if err := sdk.JSONUnmarshal(bts, &result); err != nil {
 		return nil, err
 	}
 	return cli.AsListResult(result), nil
diff --git a/cli/cdsctl/admin_hooks.go b/cli/cdsctl/admin_hooks.go
index f09e479a8..128f88729 100644
--- a/cli/cdsctl/admin_hooks.go
+++ b/cli/cdsctl/admin_hooks.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"time"
@@ -55,7 +54,7 @@ func adminHooksTaskListRun(v cli.Values) (cli.ListResult, error) {
 		return nil, err
 	}
 	ts := []sdk.Task{}
-	if err := json.Unmarshal(btes, &ts); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &ts); err != nil {
 		return nil, err
 	}
 
@@ -113,7 +112,7 @@ func adminHooksTaskExecutionListRun(v cli.Values) (cli.ListResult, error) {
 		TimestampH  string `cli:"Timestamp H"`
 	}
 	ts := sdk.Task{}
-	if err := json.Unmarshal(btes, &ts); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &ts); err != nil {
 		return nil, err
 	}
 	te := []TaskExecutionDisplay{}
diff --git a/cli/cdsctl/workflow_run.go b/cli/cdsctl/workflow_run.go
index 63c953833..90bccc6cb 100644
--- a/cli/cdsctl/workflow_run.go
+++ b/cli/cdsctl/workflow_run.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -34,7 +33,7 @@ var workflowRunManualCmd = cli.Command{
 					return true
 				}
 				data := map[string]interface{}{}
-				return json.Unmarshal([]byte(s), &data) == nil
+				return sdk.JSONUnmarshal([]byte(s), &data) == nil
 			},
 		},
 		{
@@ -97,7 +96,7 @@ func workflowRunManualRun(v cli.Values) error {
 	manual := sdk.WorkflowNodeRunManual{}
 	if strings.TrimSpace(v.GetString("data")) != "" {
 		data := map[string]interface{}{}
-		if err := json.Unmarshal([]byte(v.GetString("data")), &data); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(v.GetString("data")), &data); err != nil {
 			return cli.NewError("error payload isn't a valid json")
 		}
 		manual.Payload = data
diff --git a/cli/cdsctl/workflow_transform_as_code.go b/cli/cdsctl/workflow_transform_as_code.go
index 2662cef13..3ba30b6b0 100644
--- a/cli/cdsctl/workflow_transform_as_code.go
+++ b/cli/cdsctl/workflow_transform_as_code.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
@@ -82,7 +81,7 @@ forLoop:
 			fmt.Printf("Error: %v\n", err)
 		case evt := <-chanMessageReceived:
 			if evt.Event.EventType == fmt.Sprintf("%T", sdk.EventOperation{}) {
-				if err := json.Unmarshal(evt.Event.Payload, &ope); err != nil {
+				if err := sdk.JSONUnmarshal(evt.Event.Payload, &ope); err != nil {
 					return nil, cli.WrapError(err, "cannot parse operation from received event")
 				}
 				if ope.Status > sdk.OperationStatusProcessing {
diff --git a/contrib/grpcplugins/action/plugin-archive/go.sum b/contrib/grpcplugins/action/plugin-archive/go.sum
index 39d1147bd..02d37fdc6 100644
--- a/contrib/grpcplugins/action/plugin-archive/go.sum
+++ b/contrib/grpcplugins/action/plugin-archive/go.sum
@@ -390,8 +390,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-clair/go.sum b/contrib/grpcplugins/action/plugin-clair/go.sum
index e123c11b9..5127c8eb3 100644
--- a/contrib/grpcplugins/action/plugin-clair/go.sum
+++ b/contrib/grpcplugins/action/plugin-clair/go.sum
@@ -459,8 +459,8 @@ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remind101/migrate v0.0.0-20160423010909-d22d647232c2/go.mod h1:rhSvwcijY9wfmrBYrfCvapX8/xOTV46NAUjBRgUyJqc=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-download/go.sum b/contrib/grpcplugins/action/plugin-download/go.sum
index 6d79ae51d..cd91ec2d6 100644
--- a/contrib/grpcplugins/action/plugin-download/go.sum
+++ b/contrib/grpcplugins/action/plugin-download/go.sum
@@ -374,8 +374,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-group-tmpl/applications.go b/contrib/grpcplugins/action/plugin-group-tmpl/applications.go
index 819333785..b707b1e98 100644
--- a/contrib/grpcplugins/action/plugin-group-tmpl/applications.go
+++ b/contrib/grpcplugins/action/plugin-group-tmpl/applications.go
@@ -2,12 +2,13 @@ package main
 
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"regexp"
 	"strings"
 	"text/template"
+
+	"github.com/ovh/cds/sdk"
 )
 
 const (
@@ -39,7 +40,7 @@ func NewApplications(file string) (*Applications, error) {
 		return nil, fmt.Errorf("failed to read applications file : %s", err)
 	}
 
-	err = json.Unmarshal(data, &ret)
+	err = sdk.JSONUnmarshal(data, &ret)
 	if err != nil {
 		return nil, fmt.Errorf("failed to unmarshall applications file : %s", err)
 	}
diff --git a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
index 6d79ae51d..cd91ec2d6 100644
--- a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
@@ -374,8 +374,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/cli_actions.go b/contrib/grpcplugins/action/plugin-kafka-publish/cli_actions.go
index 79ba6a0c9..4700a1226 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/cli_actions.go
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/cli_actions.go
@@ -156,7 +156,7 @@ func ackAction(c *cli.Context) error {
 
 	//Parse the context file
 	ctx := &kafkapublisher.Context{}
-	if err := json.Unmarshal(contextBody, ctx); err != nil {
+	if err := sdk.JSONUnmarshal(contextBody, ctx); err != nil {
 		return cli.NewExitError(err.Error(), 43)
 	}
 
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
index 1219bbbad..6c1f7071c 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
@@ -439,8 +439,8 @@ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/kafka_ack.go b/contrib/grpcplugins/action/plugin-kafka-publish/kafka_ack.go
index 6c74ee4e3..82de374d8 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/kafka_ack.go
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/kafka_ack.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
 	"os/signal"
@@ -11,6 +10,7 @@ import (
 	"github.com/fsamin/go-shredder"
 
 	"github.com/ovh/cds/contrib/grpcplugins/action/kafka-publish/kafkapublisher"
+	"github.com/ovh/cds/sdk"
 )
 
 //Wait for ACK to CDS through kafka. Entrypoint is the actionID from the context file. After a fimeout (seconds) it will return an error
@@ -145,7 +145,7 @@ func ackFromKafka(kafka, topic, group, user, password, key string, timeout time.
 				continue
 			} else {
 				ack := &kafkapublisher.Ack{}
-				if err := json.Unmarshal(msg, ack); err != nil {
+				if err := sdk.JSONUnmarshal(msg, ack); err != nil {
 					fmt.Printf("Unable to parse ack: %s\n", err)
 					continue
 				}
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/kafkapublisher/context.go b/contrib/grpcplugins/action/plugin-kafka-publish/kafkapublisher/context.go
index fc4b5b2a1..047bc14fb 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/kafkapublisher/context.go
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/kafkapublisher/context.go
@@ -1,15 +1,16 @@
 package kafkapublisher
 
 import (
-	"encoding/json"
 	"fmt"
 	"path"
+
+	"github.com/ovh/cds/sdk"
 )
 
 //GetContext returns a context
 func GetContext(data []byte) (*Context, bool) {
 	c := &Context{}
-	if err := json.Unmarshal(data, c); err != nil {
+	if err := sdk.JSONUnmarshal(data, c); err != nil {
 		return nil, false
 	}
 	c.ReceivedFiles = map[string]bool{}
diff --git a/contrib/grpcplugins/action/plugin-marathon/go.sum b/contrib/grpcplugins/action/plugin-marathon/go.sum
index b3c3484d0..978b0a937 100644
--- a/contrib/grpcplugins/action/plugin-marathon/go.sum
+++ b/contrib/grpcplugins/action/plugin-marathon/go.sum
@@ -398,8 +398,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-marathon/main.go b/contrib/grpcplugins/action/plugin-marathon/main.go
index b74239744..4a6c780ae 100644
--- a/contrib/grpcplugins/action/plugin-marathon/main.go
+++ b/contrib/grpcplugins/action/plugin-marathon/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"math"
@@ -293,7 +292,7 @@ func parseApplicationConfigFile(f string) (*marathon.Application, error) {
 
 	//Parse marathon.json
 	appConfig := &marathon.Application{}
-	if err := json.Unmarshal(buff, appConfig); err != nil {
+	if err := sdk.JSONUnmarshal(buff, appConfig); err != nil {
 		fmt.Printf("Configuration file parse error: %s\n", err)
 		return nil, err
 	}
diff --git a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
index 6d79ae51d..cd91ec2d6 100644
--- a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
+++ b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
@@ -374,8 +374,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-npm-audit-parser/main.go b/contrib/grpcplugins/action/plugin-npm-audit-parser/main.go
index de4216d8c..81d28d3b5 100644
--- a/contrib/grpcplugins/action/plugin-npm-audit-parser/main.go
+++ b/contrib/grpcplugins/action/plugin-npm-audit-parser/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"strings"
 
@@ -41,7 +40,7 @@ func (actPlugin *npmAuditParserActionPlugin) Run(ctx context.Context, q *actionp
 		return actionplugin.Fail("Unable to read file %s: %v", file, err)
 	}
 	var npmAudit npmAudit
-	if err := json.Unmarshal(b, &npmAudit); err != nil {
+	if err := sdk.JSONUnmarshal(b, &npmAudit); err != nil {
 		return actionplugin.Fail("Unable to read npm report: %v", err)
 	}
 
diff --git a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
index b04fd768e..0288aaedb 100644
--- a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
+++ b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
@@ -374,8 +374,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-tmpl/go.sum b/contrib/grpcplugins/action/plugin-tmpl/go.sum
index 6d79ae51d..cd91ec2d6 100644
--- a/contrib/grpcplugins/action/plugin-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-tmpl/go.sum
@@ -374,8 +374,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
diff --git a/contrib/grpcplugins/action/plugin-venom/go.sum b/contrib/grpcplugins/action/plugin-venom/go.sum
index f82a11131..fbfb19ba4 100644
--- a/contrib/grpcplugins/action/plugin-venom/go.sum
+++ b/contrib/grpcplugins/action/plugin-venom/go.sum
@@ -411,8 +411,8 @@ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362 h1:lmOdpLt3XS6QyVoY6xNfOOTNWE2xtUBees+OAO+HFOg=
diff --git a/contrib/grpcplugins/action/plugin-venom/main.go b/contrib/grpcplugins/action/plugin-venom/main.go
index 20e70bd8c..0c7eed633 100644
--- a/contrib/grpcplugins/action/plugin-venom/main.go
+++ b/contrib/grpcplugins/action/plugin-venom/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -146,7 +145,7 @@ func (actPlugin *venomActionPlugin) Run(ctx context.Context, q *actionplugin.Act
 		}
 		switch filepath.Ext(varsFromFile) {
 		case ".json":
-			err = json.Unmarshal(bytes, &varFileMap)
+			err = sdk.JSONUnmarshal(bytes, &varFileMap)
 		case ".yaml":
 			err = yaml.Unmarshal(bytes, &varFileMap)
 		default:
diff --git a/contrib/grpcplugins/grpcplugins.go b/contrib/grpcplugins/grpcplugins.go
index 5c0866205..413ebfdaf 100644
--- a/contrib/grpcplugins/grpcplugins.go
+++ b/contrib/grpcplugins/grpcplugins.go
@@ -35,7 +35,7 @@ func GetRunResults(workerHTTPPort int32) ([]sdk.WorkflowRunResult, error) {
 	}
 
 	var results []sdk.WorkflowRunResult
-	if err := json.Unmarshal(body, &results); err != nil {
+	if err := sdk.JSONUnmarshal(body, &results); err != nil {
 		return nil, fmt.Errorf("unable to unmarshal response: %v", err)
 	}
 	return results, nil
@@ -95,7 +95,7 @@ func GetServices(workerHTTPPort int32, serviceType string) ([]sdk.ServiceConfigu
 	}
 
 	var serv []sdk.ServiceConfiguration
-	if err := json.Unmarshal(b, &serv); err != nil {
+	if err := sdk.JSONUnmarshal(b, &serv); err != nil {
 		return nil, fmt.Errorf("cannot unmarshal body /services: %v", err)
 	}
 	return serv, nil
diff --git a/contrib/integrations/arsenal/plugin-arsenal/main.go b/contrib/integrations/arsenal/plugin-arsenal/main.go
index 7ae813722..5ca629bc8 100644
--- a/contrib/integrations/arsenal/plugin-arsenal/main.go
+++ b/contrib/integrations/arsenal/plugin-arsenal/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -137,7 +136,7 @@ func (e *arsenalDeploymentPlugin) Run(ctx context.Context, q *integrationplugin.
 
 	//Read the followUp token
 	bodyResult := map[string]string{}
-	if err := json.Unmarshal(body, &bodyResult); err != nil {
+	if err := sdk.JSONUnmarshal(body, &bodyResult); err != nil {
 		return fail("Error: Unable to read body: %v", err)
 	}
 	var followUpToken = bodyResult["followup_token"]
@@ -177,7 +176,7 @@ func (e *arsenalDeploymentPlugin) Run(ctx context.Context, q *integrationplugin.
 
 		//Read the followUp token
 		bodyResult := map[string]interface{}{}
-		if err := json.Unmarshal(body, &bodyResult); err != nil {
+		if err := sdk.JSONUnmarshal(body, &bodyResult); err != nil {
 			return fail("Error: Unable to read body: %v", err)
 		}
 
diff --git a/contrib/integrations/artifactory/artifactory.go b/contrib/integrations/artifactory/artifactory.go
index 895d04499..a35f812a7 100644
--- a/contrib/integrations/artifactory/artifactory.go
+++ b/contrib/integrations/artifactory/artifactory.go
@@ -1,7 +1,6 @@
 package art
 
 import (
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"strings"
@@ -14,7 +13,6 @@ import (
 	"github.com/jfrog/jfrog-client-go/distribution"
 	authdistrib "github.com/jfrog/jfrog-client-go/distribution/auth"
 
-
 	"github.com/ovh/cds/sdk"
 )
 
@@ -97,7 +95,7 @@ func GetFileInfo(artiClient artifactory.ArtifactoryServicesManager, repoName str
 		return resp, sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to call artifactory [HTTP: %d] %s", re.StatusCode, string(body))
 	}
 
-	if err := json.Unmarshal(body, &resp); err != nil {
+	if err := sdk.JSONUnmarshal(body, &resp); err != nil {
 		return resp, sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read artifactory response %s: %v", string(body), err)
 	}
 	return resp, nil
diff --git a/contrib/integrations/artifactory/plugin-artifactory-release/main.go b/contrib/integrations/artifactory/plugin-artifactory-release/main.go
index 69b659d13..4dfc14833 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-release/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-release/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"path/filepath"
 
@@ -251,7 +250,7 @@ func (e *artifactoryReleasePlugin) listEdgeNodes(distriClient *distribution.Dist
 	}
 
 	var edges []EdgeNode
-	if err := json.Unmarshal(body, &edges); err != nil {
+	if err := sdk.JSONUnmarshal(body, &edges); err != nil {
 		return nil, fmt.Errorf("unable to unmarshal response %s: %v", string(body), err)
 	}
 	return edges, nil
diff --git a/contrib/uservices/cds2http/service/process.go b/contrib/uservices/cds2http/service/process.go
index b5973914c..168696a98 100644
--- a/contrib/uservices/cds2http/service/process.go
+++ b/contrib/uservices/cds2http/service/process.go
@@ -48,7 +48,7 @@ func process(event sdk.Event, client *http.Client) error {
 		return nil
 	}
 
-	if err := json.Unmarshal(event.Payload, &eventNotif); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNotif); err != nil {
 		log.Warnf("process> cannot read payload. type:%s err:%s", event.EventType, err)
 		return nil
 	}
diff --git a/docs/content/development/contribute/error_management.md b/docs/content/development/contribute/error_management.md
index 3086c3e72..bf96263c1 100644
--- a/docs/content/development/contribute/error_management.md
+++ b/docs/content/development/contribute/error_management.md
@@ -16,14 +16,14 @@ cdsctl admin errors get <error_uuid>
 
 All errors from lib should be wrapped like **sdk.WithStack(err)** or **sdk.WrapError(err, format, values...)** directly when created. 
 ```go
-if err := json.Unmarshal(...); err != nil {
+if err := sdk.JSONUnmarshal(...); err != nil {
     return sdk.WithStack(err) // or return sdk.WrapError(err, "Cannot unmarshal given data")
 }
 ```
 
 **WrapError** can be used to add more details about an error when returned.
 ```go
-func one() error { return sdk.WithStack(json.Unmarshal(...)) }
+func one() error { return sdk.WithStack(sdk.JSONUnmarshal(...)) }
 
 func two() error { return sdk.WrapError(one(), "Error calling one") }
 
@@ -42,15 +42,15 @@ func four() error {
 
 To create an error that will generate a specific HTTP status code you should use the **sdk.NewError** func or returned an existing sdk.Error.
 ```go
-if err := json.Unmarshal(...); err != nil {
+if err := sdk.JSONUnmarshal(...); err != nil {
     return sdk.NewError(sdk.ErrWrongRequest, err) // returns a 400 http code with default translated message and from value that contains err cause. 
 }
 
-if err := json.Unmarshal(...); err != nil {
+if err := sdk.JSONUnmarshal(...); err != nil {
     return sdk.NewErrorFrom(sdk.ErrWrongRequest, "A text that will be in from message") // returns a 400 http code with default translated message and test as from. 
 }
 
-if err := json.Unmarshal(...); err != nil {
+if err := sdk.JSONUnmarshal(...); err != nil {
     return sdk.WrapError(sdk.ErrWrongRequest, "Cannot unmarshal given data") // or return sdk.WithStack(sdk.ErrWrongRequest) returns a 400 http code with default translated message.
 }
 ```
@@ -64,7 +64,7 @@ if err := one(); err != nil {
     }
 }
 
-err := json.Unmarshal(...)
+err := sdk.JSONUnmarshal(...)
 sdk.ErrorIs(err, sdk.ErrUnknownError) => true
 sdk.ErrorIsUnknown(err) => true
 ```
diff --git a/engine/api/action.go b/engine/api/action.go
index a7513fd47..b336e1921 100644
--- a/engine/api/action.go
+++ b/engine/api/action.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 
@@ -385,7 +384,7 @@ func (api *API) getActionAuditHandler() service.Handler {
 
 			if clone.DataBefore != "" {
 				var before sdk.Action
-				if err := json.Unmarshal([]byte(clone.DataBefore), &before); err != nil {
+				if err := sdk.JSONUnmarshal([]byte(clone.DataBefore), &before); err != nil {
 					log.Error(ctx, "%+v", sdk.WrapError(err, "cannot parse action audit"))
 					continue
 				}
@@ -402,7 +401,7 @@ func (api *API) getActionAuditHandler() service.Handler {
 
 			if clone.DataAfter != "" {
 				var after sdk.Action
-				if err := json.Unmarshal([]byte(clone.DataAfter), &after); err != nil {
+				if err := sdk.JSONUnmarshal([]byte(clone.DataAfter), &after); err != nil {
 					log.Error(ctx, "%+v", sdk.WrapError(err, "cannot parse action audit"))
 					continue
 				}
@@ -458,7 +457,7 @@ func (api *API) postActionAuditRollbackHandler() service.Handler {
 		}
 
 		var before sdk.Action
-		if err := json.Unmarshal([]byte(aa.DataBefore), &before); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(aa.DataBefore), &before); err != nil {
 			return sdk.WrapError(err, "cannot parse action audit")
 		}
 
diff --git a/engine/api/action/audit.go b/engine/api/action/audit.go
index cd23961ea..270ba477f 100644
--- a/engine/api/action/audit.go
+++ b/engine/api/action/audit.go
@@ -47,7 +47,7 @@ type addActionAudit struct{}
 
 func (a addActionAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var aEvent sdk.EventActionAdd
-	if err := json.Unmarshal(e.Payload, &aEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &aEvent); err != nil {
 		return sdk.WrapError(err, "unable to unmarshal payload")
 	}
 
@@ -72,7 +72,7 @@ type updateActionAudit struct{}
 
 func (a updateActionAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var aEvent sdk.EventActionUpdate
-	if err := json.Unmarshal(e.Payload, &aEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &aEvent); err != nil {
 		return sdk.WrapError(err, "unable to unmarshal payload")
 	}
 
diff --git a/engine/api/application/application_variable.go b/engine/api/application/application_variable.go
index f2d7944a0..d66828444 100644
--- a/engine/api/application/application_variable.go
+++ b/engine/api/application/application_variable.go
@@ -2,7 +2,6 @@ package application
 
 import (
 	"database/sql"
-	"encoding/json"
 	"fmt"
 
 	"github.com/go-gorp/gorp"
@@ -41,7 +40,7 @@ func GetVariableAudit(db gorp.SqlExecutor, key, appName string) ([]sdk.VariableA
 			return nil, err
 		}
 		var vars []sdk.Variable
-		err = json.Unmarshal([]byte(data), &vars)
+		err = sdk.JSONUnmarshal([]byte(data), &vars)
 		if err != nil {
 			return nil, err
 		}
diff --git a/engine/api/application/gorp_model.go b/engine/api/application/gorp_model.go
index 7bbc96db4..f28095ce2 100644
--- a/engine/api/application/gorp_model.go
+++ b/engine/api/application/gorp_model.go
@@ -33,7 +33,7 @@ func (ava *dbApplicationVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if before.Valid {
 		vBefore := &sdk.ApplicationVariable{}
-		if err := json.Unmarshal([]byte(before.String), vBefore); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(before.String), vBefore); err != nil {
 			return err
 		}
 		if sdk.NeedPlaceholder(vBefore.Type) {
@@ -45,7 +45,7 @@ func (ava *dbApplicationVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if after.Valid {
 		vAfter := &sdk.ApplicationVariable{}
-		if err := json.Unmarshal([]byte(after.String), vAfter); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(after.String), vAfter); err != nil {
 			return err
 		}
 		if sdk.NeedPlaceholder(vAfter.Type) {
diff --git a/engine/api/application_import.go b/engine/api/application_import.go
index 41b9fbdcf..0f09f5cfb 100644
--- a/engine/api/application_import.go
+++ b/engine/api/application_import.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -45,7 +44,7 @@ func (api *API) postApplicationImportHandler() service.Handler {
 		var errapp error
 		switch contentType {
 		case "application/json":
-			errapp = json.Unmarshal(body, eapp)
+			errapp = sdk.JSONUnmarshal(body, eapp)
 		case "application/x-yaml", "text/x-yam":
 			errapp = yaml.Unmarshal(body, eapp)
 		default:
diff --git a/engine/api/authentication/corpsso/corpsso.go b/engine/api/authentication/corpsso/corpsso.go
index faf8d0e80..127555688 100644
--- a/engine/api/authentication/corpsso/corpsso.go
+++ b/engine/api/authentication/corpsso/corpsso.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"strings"
@@ -129,7 +128,7 @@ func (d authDriver) CheckSigninStateToken(req sdk.AuthConsumerSigninRequest) err
 	}
 
 	var signinStateToken sdk.AuthSigninConsumerToken
-	if err := json.Unmarshal(rawRequest, &signinStateToken); err != nil {
+	if err := sdk.JSONUnmarshal(rawRequest, &signinStateToken); err != nil {
 		return sdk.NewError(sdk.ErrWrongRequest, fmt.Errorf("unable to parse state: %v", err))
 	}
 
@@ -212,7 +211,7 @@ func (d authDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 	}
 
 	var itk issuedToken
-	if err := json.Unmarshal(rawIssuedToken, &itk); err != nil {
+	if err := sdk.JSONUnmarshal(rawIssuedToken, &itk); err != nil {
 		return u, sdk.NewError(sdk.ErrUnauthorized, err)
 	}
 
diff --git a/engine/api/authentication/corpsso/jws.go b/engine/api/authentication/corpsso/jws.go
index b56b610da..872d810eb 100644
--- a/engine/api/authentication/corpsso/jws.go
+++ b/engine/api/authentication/corpsso/jws.go
@@ -46,7 +46,7 @@ func prepareRequest(requestSigningKey string, request sdk.AuthSigninConsumerToke
 		return "", sdk.WithStack(err)
 	}
 	var requestData map[string]interface{}
-	if err := json.Unmarshal(requestJSON, &requestData); err != nil {
+	if err := sdk.JSONUnmarshal(requestJSON, &requestData); err != nil {
 		return "", sdk.WithStack(err)
 	}
 
diff --git a/engine/api/authentication/github/github.go b/engine/api/authentication/github/github.go
index daddf5647..566aebe17 100644
--- a/engine/api/authentication/github/github.go
+++ b/engine/api/authentication/github/github.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -128,7 +127,7 @@ func (d authDriver) GetUserInfo(ctx context.Context, req sdk.AuthConsumerSigninR
 		Email string `json:"email"`
 		Name  string `json:"name"`
 	}
-	if err := json.Unmarshal(resBody, &githubUser); err != nil {
+	if err := sdk.JSONUnmarshal(resBody, &githubUser); err != nil {
 		return info, sdk.WithStack(err)
 	}
 
diff --git a/engine/api/cdn/item.go b/engine/api/cdn/item.go
index 6a9c10401..0955a63ab 100644
--- a/engine/api/cdn/item.go
+++ b/engine/api/cdn/item.go
@@ -2,7 +2,6 @@ package cdn
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -50,7 +49,7 @@ func ListItems(ctx context.Context, db gorp.SqlExecutor, itemtype sdk.CDNItemTyp
 		return result, err
 	}
 	var cdnItems []sdk.CDNItem
-	if err := json.Unmarshal(btes, &cdnItems); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &cdnItems); err != nil {
 		return result, sdk.WithStack(err)
 	}
 
diff --git a/engine/api/database/gorpmapping/gorpmapping.go b/engine/api/database/gorpmapping/gorpmapping.go
index 8cd1c8d23..e3b27bd99 100644
--- a/engine/api/database/gorpmapping/gorpmapping.go
+++ b/engine/api/database/gorpmapping/gorpmapping.go
@@ -3,6 +3,8 @@ package gorpmapping
 import (
 	"database/sql"
 	"encoding/json"
+
+	"github.com/ovh/cds/sdk"
 )
 
 //JSONToNullString returns a valid sql.NullString with json-marshalled i
@@ -22,5 +24,5 @@ func JSONNullString(s sql.NullString, holder interface{}) error {
 	if !s.Valid {
 		return nil
 	}
-	return json.Unmarshal([]byte(s.String), holder)
+	return sdk.JSONUnmarshal([]byte(s.String), holder)
 }
diff --git a/engine/api/environment/gorp_model.go b/engine/api/environment/gorp_model.go
index 2a323f2cc..9289731a5 100644
--- a/engine/api/environment/gorp_model.go
+++ b/engine/api/environment/gorp_model.go
@@ -97,7 +97,7 @@ func (eva *dbEnvironmentVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if before.Valid {
 		vBefore := &sdk.EnvironmentVariable{}
-		if err := json.Unmarshal([]byte(before.String), vBefore); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(before.String), vBefore); err != nil {
 			return sdk.WithStack(err)
 		}
 		if sdk.NeedPlaceholder(vBefore.Type) {
@@ -109,7 +109,7 @@ func (eva *dbEnvironmentVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if after.Valid {
 		vAfter := &sdk.EnvironmentVariable{}
-		if err := json.Unmarshal([]byte(after.String), vAfter); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(after.String), vAfter); err != nil {
 			return sdk.WithStack(err)
 		}
 		if sdk.NeedPlaceholder(vAfter.Type) {
diff --git a/engine/api/error.go b/engine/api/error.go
index cc3e6b666..0767adbaf 100644
--- a/engine/api/error.go
+++ b/engine/api/error.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -55,7 +54,7 @@ func (api *API) getErrorHandler() service.Handler {
 		}
 
 		var res graylogResponse
-		if err := json.Unmarshal(body, &res); err != nil {
+		if err := sdk.JSONUnmarshal(body, &res); err != nil {
 			return sdk.WrapError(err, "cannot unmarshal response from Graylog")
 		}
 
diff --git a/engine/api/integration/artifact_manager/artifactory/client.go b/engine/api/integration/artifact_manager/artifactory/client.go
index 06bd0144c..72bac6b8c 100644
--- a/engine/api/integration/artifact_manager/artifactory/client.go
+++ b/engine/api/integration/artifact_manager/artifactory/client.go
@@ -1,7 +1,6 @@
 package artifactory
 
 import (
-	"encoding/json"
 	"fmt"
 	"strconv"
 	"time"
@@ -58,7 +57,7 @@ func (c *Client) GetFileInfo(repoName string, filePath string) (sdk.FileInfo, er
 	}
 
 	var resp FileInfoResponse
-	if err := json.Unmarshal(body, &resp); err != nil {
+	if err := sdk.JSONUnmarshal(body, &resp); err != nil {
 		return fi, sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read artifactory response %s: %v", string(body), err)
 	}
 
diff --git a/engine/api/migrate/migrate_run_secrets.go b/engine/api/migrate/migrate_run_secrets.go
index b1ed7f24a..35ddac112 100644
--- a/engine/api/migrate/migrate_run_secrets.go
+++ b/engine/api/migrate/migrate_run_secrets.go
@@ -3,7 +3,6 @@ package migrate
 import (
 	"context"
 	"database/sql"
-	"encoding/json"
 	"fmt"
 
 	"github.com/go-gorp/gorp"
@@ -385,7 +384,7 @@ func loadRuns(db gorp.SqlExecutor, query string) ([]sdk.WorkflowRun, error) {
 			return nil, err
 		}
 		if ww.Valid {
-			if err := json.Unmarshal([]byte(ww.String), &wr.Workflow); err != nil {
+			if err := sdk.JSONUnmarshal([]byte(ww.String), &wr.Workflow); err != nil {
 				return nil, err
 			}
 		}
diff --git a/engine/api/pipeline/audit.go b/engine/api/pipeline/audit.go
index 878779c60..a339a07c2 100644
--- a/engine/api/pipeline/audit.go
+++ b/engine/api/pipeline/audit.go
@@ -92,7 +92,7 @@ func (p *PipelineAudit) PostGet(s gorp.SqlExecutor) error {
 		return sdk.WrapError(err, "error on queryRow")
 	}
 
-	if err := json.Unmarshal(pip, &p.Pipeline); err != nil {
+	if err := sdk.JSONUnmarshal(pip, &p.Pipeline); err != nil {
 		return sdk.WrapError(err, "error on unmarshal job")
 	}
 	return nil
diff --git a/engine/api/pipeline/pipeline_stage.go b/engine/api/pipeline/pipeline_stage.go
index fd5eff6d3..78a894a80 100644
--- a/engine/api/pipeline/pipeline_stage.go
+++ b/engine/api/pipeline/pipeline_stage.go
@@ -201,7 +201,7 @@ func LoadPipelineStage(ctx context.Context, db gorp.SqlExecutor, p *sdk.Pipeline
 
 			var pipelineActionParameter []sdk.Parameter
 			var isUpdated bool
-			err = json.Unmarshal([]byte(mapArgs[id][index]), &pipelineActionParameter)
+			err = sdk.JSONUnmarshal([]byte(mapArgs[id][index]), &pipelineActionParameter)
 			if err != nil {
 				return sdk.WithStack(err)
 			}
diff --git a/engine/api/project/dao.go b/engine/api/project/dao.go
index b60d08539..910a4079a 100644
--- a/engine/api/project/dao.go
+++ b/engine/api/project/dao.go
@@ -224,6 +224,8 @@ func DeleteByID(db gorp.SqlExecutor, id int64) error {
 
 // LoadProjectByNodeJobRunID return a project from node job run id
 func LoadProjectByNodeJobRunID(ctx context.Context, db gorp.SqlExecutor, store cache.Store, nodeJobRunID int64, opts ...LoadOptionFunc) (*sdk.Project, error) {
+	ctx, end := telemetry.Span(ctx, "project.LoadProjectByNodeJobRunID")
+	defer end()
 	query := `
 		SELECT project.* FROM project
 		JOIN workflow_run ON workflow_run.project_id = project.id
diff --git a/engine/api/project/gorp_model.go b/engine/api/project/gorp_model.go
index 5e68201ba..eb2b36e25 100644
--- a/engine/api/project/gorp_model.go
+++ b/engine/api/project/gorp_model.go
@@ -101,7 +101,7 @@ func (pva *dbProjectVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if before.Valid {
 		vBefore := &sdk.ProjectVariable{}
-		if err := json.Unmarshal([]byte(before.String), vBefore); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(before.String), vBefore); err != nil {
 			return err
 		}
 		if sdk.NeedPlaceholder(vBefore.Type) {
@@ -113,7 +113,7 @@ func (pva *dbProjectVariableAudit) PostGet(db gorp.SqlExecutor) error {
 
 	if after.Valid {
 		vAfter := &sdk.ProjectVariable{}
-		if err := json.Unmarshal([]byte(after.String), vAfter); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(after.String), vAfter); err != nil {
 			return err
 		}
 		if sdk.NeedPlaceholder(vAfter.Type) {
diff --git a/engine/api/project/project_variable.go b/engine/api/project/project_variable.go
index ee7d317a4..dd40c7d12 100644
--- a/engine/api/project/project_variable.go
+++ b/engine/api/project/project_variable.go
@@ -2,7 +2,6 @@ package project
 
 import (
 	"database/sql"
-	"encoding/json"
 
 	"github.com/go-gorp/gorp"
 
@@ -32,7 +31,7 @@ func GetVariableAudit(db gorp.SqlExecutor, key string) ([]sdk.VariableAudit, err
 			return nil, err
 		}
 		var vars []sdk.Variable
-		err = json.Unmarshal([]byte(data), &vars)
+		err = sdk.JSONUnmarshal([]byte(data), &vars)
 		if err != nil {
 			return nil, err
 		}
diff --git a/engine/api/repositoriesmanager/events.go b/engine/api/repositoriesmanager/events.go
index a1a4d1ad5..c55450dc8 100644
--- a/engine/api/repositoriesmanager/events.go
+++ b/engine/api/repositoriesmanager/events.go
@@ -2,7 +2,6 @@ package repositoriesmanager
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"time"
 
@@ -67,7 +66,7 @@ func processEvent(ctx context.Context, db gorpmapper.SqlExecutorWithTx, event sd
 
 	var eventWNR sdk.EventRunWorkflowNode
 
-	if err := json.Unmarshal(event.Payload, &eventWNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventWNR); err != nil {
 		return sdk.WrapError(err, "cannot read payload")
 	}
 	if eventWNR.RepositoryManagerName == "" {
diff --git a/engine/api/services.go b/engine/api/services.go
index c4ba3f080..93fcbfe32 100644
--- a/engine/api/services.go
+++ b/engine/api/services.go
@@ -232,7 +232,7 @@ func (api *API) serviceAPIHeartbeatUpdate(ctx context.Context, db *gorp.DbMap) {
 
 	var srvConfig sdk.ServiceConfig
 	b, _ := json.Marshal(api.Config)
-	json.Unmarshal(b, &srvConfig) // nolint
+	sdk.JSONUnmarshal(b, &srvConfig) // nolint
 
 	srv := &sdk.Service{
 		CanonicalService: sdk.CanonicalService{
diff --git a/engine/api/services/cache.go b/engine/api/services/cache.go
index e3273dbeb..c265d3558 100644
--- a/engine/api/services/cache.go
+++ b/engine/api/services/cache.go
@@ -2,7 +2,6 @@ package services
 
 import (
 	"context"
-	"encoding/json"
 	"sync"
 	"time"
 
@@ -133,7 +132,7 @@ func (c *iCache) doListenDatabase(ctx context.Context) {
 
 		case n := <-listener.Notify:
 			e := map[string]interface{}{}
-			if err := json.Unmarshal([]byte(n.Extra), &e); err != nil {
+			if err := sdk.JSONUnmarshal([]byte(n.Extra), &e); err != nil {
 				log.Warn(ctx, "unable to unmarshal received event: %v", err)
 				continue
 			}
diff --git a/engine/api/services/external.go b/engine/api/services/external.go
index 1d48bba4f..ec8204086 100644
--- a/engine/api/services/external.go
+++ b/engine/api/services/external.go
@@ -29,7 +29,7 @@ type ExternalService struct {
 func (e ExternalService) ServiceConfig() sdk.ServiceConfig {
 	b, _ := json.Marshal(e)
 	var cfg sdk.ServiceConfig
-	json.Unmarshal(b, &cfg) // nolint
+	sdk.JSONUnmarshal(b, &cfg) // nolint
 	return cfg
 }
 
diff --git a/engine/api/services/http.go b/engine/api/services/http.go
index a1a1daf1c..7e5ba0f05 100644
--- a/engine/api/services/http.go
+++ b/engine/api/services/http.go
@@ -118,7 +118,7 @@ func doMultiPartRequest(ctx context.Context, srvs []sdk.Service, method, path st
 			}
 
 			if out != nil {
-				if err := json.Unmarshal(res, out); err != nil {
+				if err := sdk.JSONUnmarshal(res, out); err != nil {
 					return code, sdk.WrapError(err, "Unable to marshal output")
 				}
 			}
@@ -186,7 +186,7 @@ func _doJSONRequest(ctx context.Context, srv *sdk.Service, method, path string,
 	}
 
 	if out != nil {
-		if err := json.Unmarshal(res, out); err != nil {
+		if err := sdk.JSONUnmarshal(res, out); err != nil {
 			return headers, code, sdk.WrapError(err, "unable to unmarshal output")
 		}
 	}
@@ -209,7 +209,7 @@ func PostBinary(ctx context.Context, srvs []sdk.Service, path string, r io.Reade
 		return code, err
 	}
 	if out != nil {
-		if err := json.Unmarshal(res, out); err != nil {
+		if err := sdk.JSONUnmarshal(res, out); err != nil {
 			return code, sdk.WithStack(err)
 		}
 	}
diff --git a/engine/api/websocket.go b/engine/api/websocket.go
index 9b76608dc..d939a82e6 100644
--- a/engine/api/websocket.go
+++ b/engine/api/websocket.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"math/rand"
 	"net/http"
@@ -82,10 +81,10 @@ func (c *websocketClientData) updateEventFilters(ctx context.Context, db gorp.Sq
 	var fs []sdk.WebsocketFilter
 
 	var f sdk.WebsocketFilter
-	if err := json.Unmarshal(msg, &f); err == nil {
+	if err := sdk.JSONUnmarshal(msg, &f); err == nil {
 		fs = []sdk.WebsocketFilter{f}
 	} else {
-		if err := json.Unmarshal(msg, &fs); err != nil {
+		if err := sdk.JSONUnmarshal(msg, &fs); err != nil {
 			return sdk.WrapError(err, "cannot unmarshal websocket input message")
 		}
 	}
@@ -210,7 +209,7 @@ func (a *API) initWebsocket(pubSubKey string) error {
 		telemetry.Record(a.Router.Background, WebSocketEvents, 1)
 
 		var e sdk.Event
-		if err := json.Unmarshal(m, &e); err != nil {
+		if err := sdk.JSONUnmarshal(m, &e); err != nil {
 			err = sdk.WrapError(err, "cannot parse event from WS broker")
 			ctx := sdk.ContextWithStacktrace(context.TODO(), err)
 			log.Warn(ctx, err.Error())
diff --git a/engine/api/worker_model_import.go b/engine/api/worker_model_import.go
index 4600be4d9..48dacda83 100644
--- a/engine/api/worker_model_import.go
+++ b/engine/api/worker_model_import.go
@@ -2,7 +2,6 @@ package api
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 
@@ -40,7 +39,7 @@ func (api *API) postWorkerModelImportHandler() service.Handler {
 		var errUnMarshall error
 		switch contentType {
 		case "application/json":
-			errUnMarshall = json.Unmarshal(body, &eWorkerModel)
+			errUnMarshall = sdk.JSONUnmarshal(body, &eWorkerModel)
 		case "application/x-yaml", "text/x-yam":
 			errUnMarshall = yaml.Unmarshal(body, &eWorkerModel)
 		default:
diff --git a/engine/api/workflow/audit.go b/engine/api/workflow/audit.go
index 8f3d65041..c1224976c 100644
--- a/engine/api/workflow/audit.go
+++ b/engine/api/workflow/audit.go
@@ -28,7 +28,7 @@ type addWorkflowAudit struct{}
 
 func (a addWorkflowAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var wEvent sdk.EventWorkflowAdd
-	if err := json.Unmarshal(e.Payload, &wEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &wEvent); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal payload")
 	}
 
@@ -58,7 +58,7 @@ type updateWorkflowAudit struct{}
 
 func (u updateWorkflowAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var wEvent sdk.EventWorkflowUpdate
-	if err := json.Unmarshal(e.Payload, &wEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &wEvent); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal payload")
 	}
 
@@ -98,7 +98,7 @@ type addWorkflowPermissionAudit struct{}
 
 func (a addWorkflowPermissionAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var wEvent sdk.EventWorkflowPermissionAdd
-	if err := json.Unmarshal(e.Payload, &wEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &wEvent); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal payload")
 	}
 
@@ -124,7 +124,7 @@ type updateWorkflowPermissionAudit struct{}
 
 func (u updateWorkflowPermissionAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var wEvent sdk.EventWorkflowPermissionUpdate
-	if err := json.Unmarshal(e.Payload, &wEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &wEvent); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal payload")
 	}
 
@@ -156,7 +156,7 @@ type deleteWorkflowPermissionAudit struct{}
 
 func (a deleteWorkflowPermissionAudit) Compute(ctx context.Context, db gorp.SqlExecutor, e sdk.Event) error {
 	var wEvent sdk.EventWorkflowPermissionDelete
-	if err := json.Unmarshal(e.Payload, &wEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.Payload, &wEvent); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal payload")
 	}
 
diff --git a/engine/api/workflow/dao_hook_model.go b/engine/api/workflow/dao_hook_model.go
index 09be0fe30..67c706829 100644
--- a/engine/api/workflow/dao_hook_model.go
+++ b/engine/api/workflow/dao_hook_model.go
@@ -39,7 +39,7 @@ func (r *hookModel) PostGet(db gorp.SqlExecutor) error {
 		return err
 	}
 	conf := sdk.WorkflowNodeHookConfig{}
-	if err := json.Unmarshal([]byte(confStr), &conf); err != nil {
+	if err := sdk.JSONUnmarshal([]byte(confStr), &conf); err != nil {
 		return err
 	}
 	r.DefaultConfig = conf
diff --git a/engine/api/workflow/dao_node_run.go b/engine/api/workflow/dao_node_run.go
index ecf5248d7..7695d5931 100644
--- a/engine/api/workflow/dao_node_run.go
+++ b/engine/api/workflow/dao_node_run.go
@@ -814,8 +814,8 @@ func updateNodeRunCommits(db gorp.SqlExecutor, id int64, commits []sdk.VCSCommit
 	return nil
 }
 
-// updateNodeRunStatusAndStage update just noderun status and stage
-func updateNodeRunStatusAndStage(db gorp.SqlExecutor, nodeRun *sdk.WorkflowNodeRun) error {
+// UpdateNodeRunStatusAndStage update just noderun status and stage
+func UpdateNodeRunStatusAndStage(db gorp.SqlExecutor, nodeRun *sdk.WorkflowNodeRun) error {
 	stagesBts, errMarshal := json.Marshal(nodeRun.Stages)
 	if errMarshal != nil {
 		return sdk.WrapError(errMarshal, "unable to marshal stages")
diff --git a/engine/api/workflow/dao_outgoing_hook_model.go b/engine/api/workflow/dao_outgoing_hook_model.go
index e6edd8183..81d82ed95 100644
--- a/engine/api/workflow/dao_outgoing_hook_model.go
+++ b/engine/api/workflow/dao_outgoing_hook_model.go
@@ -39,7 +39,7 @@ func (r *outgoingHookModel) PostGet(db gorp.SqlExecutor) error {
 		return err
 	}
 	conf := sdk.WorkflowNodeHookConfig{}
-	if err := json.Unmarshal([]byte(confStr), &conf); err != nil {
+	if err := sdk.JSONUnmarshal([]byte(confStr), &conf); err != nil {
 		return err
 	}
 	r.DefaultConfig = conf
diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go
index 4301476f4..d60515f3f 100644
--- a/engine/api/workflow/dao_run.go
+++ b/engine/api/workflow/dao_run.go
@@ -3,7 +3,6 @@ package workflow
 import (
 	"context"
 	"database/sql"
-	"encoding/json"
 	"fmt"
 	"sort"
 	"strconv"
@@ -1075,7 +1074,7 @@ func stopRunsBlocked(ctx context.Context, db *gorp.DbMap) error {
 			Status: resp[i].Status,
 		}
 		if resp[i].Stages.Valid {
-			if err := json.Unmarshal([]byte(resp[i].Stages.String), &nr.Stages); err != nil {
+			if err := sdk.JSONUnmarshal([]byte(resp[i].Stages.String), &nr.Stages); err != nil {
 				return sdk.WrapError(err, "cannot unmarshal stages")
 			}
 		}
@@ -1086,7 +1085,7 @@ func stopRunsBlocked(ctx context.Context, db *gorp.DbMap) error {
 			nr.Done = now
 		}
 
-		if err := updateNodeRunStatusAndStage(tx, &nr); err != nil {
+		if err := UpdateNodeRunStatusAndStage(tx, &nr); err != nil {
 			return sdk.WrapError(err, "cannot update node runs stages")
 		}
 	}
diff --git a/engine/api/workflow/execute_node_job_run.go b/engine/api/workflow/execute_node_job_run.go
index 451daa110..224387f11 100644
--- a/engine/api/workflow/execute_node_job_run.go
+++ b/engine/api/workflow/execute_node_job_run.go
@@ -127,10 +127,10 @@ func UpdateNodeJobRunStatus(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 	report := new(ProcessorReport)
 
 	_, next := telemetry.Span(ctx, "workflow.LoadRunByID")
-	nodeRun, errLoad := LoadNodeRunByID(db, job.WorkflowNodeRunID, LoadRunOptions{})
+	nodeRun, err := LoadNodeRunByID(db, job.WorkflowNodeRunID, LoadRunOptions{})
 	next()
-	if errLoad != nil {
-		return nil, sdk.WrapError(errLoad, "Unable to load node run id %d", job.WorkflowNodeRunID)
+	if err != nil {
+		return nil, sdk.WrapError(err, "Unable to load node run id %d", job.WorkflowNodeRunID)
 	}
 
 	query := `SELECT status FROM workflow_node_run_job WHERE id = $1`
@@ -139,6 +139,8 @@ func UpdateNodeJobRunStatus(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 		return nil, sdk.WrapError(err, "Cannot select status from workflow_node_run_job node job run %d", job.ID)
 	}
 
+	log.Info(ctx, "job %d current status %q, new status %q", job.ID, status, currentStatus)
+
 	switch status {
 	case sdk.StatusBuilding:
 		if currentStatus != sdk.StatusWaiting {
@@ -158,12 +160,12 @@ func UpdateNodeJobRunStatus(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 		job.Status = status
 
 		_, next := telemetry.Span(ctx, "workflow.LoadRunByID")
-		wf, errLoadWf := LoadRunByID(db, nodeRun.WorkflowRunID, LoadRunOptions{
+		wf, err := LoadRunByID(db, nodeRun.WorkflowRunID, LoadRunOptions{
 			WithDeleted: true,
 		})
 		next()
-		if errLoadWf != nil {
-			return nil, sdk.WrapError(errLoadWf, "workflow.UpdateNodeJobRunStatus> Unable to load run id %d", nodeRun.WorkflowRunID)
+		if err != nil {
+			return nil, sdk.WrapError(err, "workflow.UpdateNodeJobRunStatus> Unable to load run id %d", nodeRun.WorkflowRunID)
 		}
 
 		wf.LastExecution = time.Now()
@@ -174,23 +176,14 @@ func UpdateNodeJobRunStatus(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 		return nil, sdk.WithStack(fmt.Errorf("cannot update WorkflowNodeJobRun %d to status %v", job.ID, status))
 	}
 
-	//If the job has been set to building, set the stage to building
-	var stageIndex int
-	for i := range nodeRun.Stages {
-		s := &nodeRun.Stages[i]
-		for _, j := range s.Jobs {
-			if j.Action.ID == job.Job.Job.Action.ID {
-				stageIndex = i
-			}
-		}
-	}
-
 	if err := UpdateNodeJobRun(ctx, db, job); err != nil {
 		return nil, sdk.WrapError(err, "Cannot update WorkflowNodeJobRun %d", job.ID)
 	}
 
 	report.Add(ctx, *job)
 
+	//If the job has been set to building, set the stage to building
+	var stageIndex = nodeRun.GetStageIndex(job)
 	if status == sdk.StatusBuilding {
 		// Sync job status in noderun
 		r, err := syncTakeJobInNodeRun(ctx, db, nodeRun, job, stageIndex)
diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go
index 773cd16a1..0e9d678fa 100644
--- a/engine/api/workflow/execute_node_run.go
+++ b/engine/api/workflow/execute_node_run.go
@@ -97,7 +97,7 @@ func syncTakeJobInNodeRun(ctx context.Context, db gorp.SqlExecutor, n *sdk.Workf
 	}
 
 	// Save the node run in database
-	if err := updateNodeRunStatusAndStage(db, n); err != nil {
+	if err := UpdateNodeRunStatusAndStage(db, n); err != nil {
 		return nil, sdk.WrapError(err, "unable to update node id=%d at status %s", n.ID, n.Status)
 	}
 	return report, nil
@@ -278,7 +278,7 @@ func executeNodeRun(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store
 	}
 
 	// Save the node run in database
-	if err := updateNodeRunStatusAndStage(db, workflowNodeRun); err != nil {
+	if err := UpdateNodeRunStatusAndStage(db, workflowNodeRun); err != nil {
 		return nil, sdk.WrapError(err, "unable to update node id=%d at status %s", workflowNodeRun.ID, workflowNodeRun.Status)
 	}
 
diff --git a/engine/api/workflow/hook.go b/engine/api/workflow/hook.go
index a389bb63d..32f4d6ea6 100644
--- a/engine/api/workflow/hook.go
+++ b/engine/api/workflow/hook.go
@@ -238,10 +238,10 @@ func updateSchedulerPayload(ctx context.Context, db gorpmapper.SqlExecutorWithTx
 			var bodyJSON interface{}
 			//Try to parse the body as an array
 			bodyJSONArray := []interface{}{}
-			if err := json.Unmarshal([]byte(h.Config["payload"].Value), &bodyJSONArray); err != nil {
+			if err := sdk.JSONUnmarshal([]byte(h.Config["payload"].Value), &bodyJSONArray); err != nil {
 				//Try to parse the body as a map
 				bodyJSONMap := map[string]interface{}{}
-				if err2 := json.Unmarshal([]byte(h.Config["payload"].Value), &bodyJSONMap); err2 == nil {
+				if err2 := sdk.JSONUnmarshal([]byte(h.Config["payload"].Value), &bodyJSONMap); err2 == nil {
 					bodyJSON = bodyJSONMap
 				}
 			} else {
@@ -444,7 +444,7 @@ func DefaultPayload(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store
 				GitRepository: app.RepositoryFullname,
 			}
 			defaultPayloadBtes, _ := json.Marshal(structuredDefaultPayload)
-			if err := json.Unmarshal(defaultPayloadBtes, &defaultPayload); err != nil {
+			if err := sdk.JSONUnmarshal(defaultPayloadBtes, &defaultPayload); err != nil {
 				return nil, err
 			}
 		} else if defaultPayloadMap, err := wf.WorkflowData.Node.Context.DefaultPayloadToMap(); err == nil && defaultPayloadMap["git.branch"] == "" {
diff --git a/engine/api/workflow/process_requirements.go b/engine/api/workflow/process_requirements.go
index e0eec100d..06fe3aad9 100644
--- a/engine/api/workflow/process_requirements.go
+++ b/engine/api/workflow/process_requirements.go
@@ -53,7 +53,7 @@ func processNodeJobRunRequirements(ctx context.Context, db gorp.SqlExecutor, j s
 		}
 		if v.Type == sdk.ModelRequirement {
 			// It is forbidden to have more than one model requirement.
-			if model != "" {
+			if j.Action.Enabled && model != "" {
 				errm.Append(sdk.ErrInvalidJobRequirementDuplicateModel)
 				break
 			}
@@ -81,7 +81,7 @@ func processNodeJobRunRequirements(ctx context.Context, db gorp.SqlExecutor, j s
 							break
 						}
 					}
-					if !hasCapa {
+					if j.Action.Enabled && !hasCapa {
 						errm.Append(sdk.ErrInvalidJobRequirementWorkerModelCapabilitites)
 						break
 					}
diff --git a/engine/api/workflow/resync_workflow.go b/engine/api/workflow/resync_workflow.go
index 9543f25ee..40007ab79 100644
--- a/engine/api/workflow/resync_workflow.go
+++ b/engine/api/workflow/resync_workflow.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/telemetry"
 )
 
 // Resync a workflow in the given workflow run
@@ -83,6 +84,9 @@ func ResyncWorkflowRunStatus(ctx context.Context, db gorp.SqlExecutor, wr *sdk.W
 
 // ResyncNodeRunsWithCommits load commits build in this node run and save it into node run
 func ResyncNodeRunsWithCommits(ctx context.Context, db *gorp.DbMap, store cache.Store, proj sdk.Project, report *ProcessorReport) {
+	ctx, end := telemetry.Span(ctx, "workflow.ResyncNodeRunsWithCommits")
+	defer end()
+
 	if report == nil {
 		return
 	}
diff --git a/engine/api/workflow_queue.go b/engine/api/workflow_queue.go
index fc8876fc7..f0fcc1eae 100644
--- a/engine/api/workflow_queue.go
+++ b/engine/api/workflow_queue.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/base64"
 	"fmt"
+	"strings"
 
 	"net/http"
 	"strconv"
@@ -30,6 +31,7 @@ import (
 	"github.com/ovh/cds/engine/api/workflow"
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/engine/featureflipping"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/engine/service"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/jws"
@@ -395,12 +397,24 @@ func (api *API) postWorkflowJobResultHandler() service.Handler {
 			return err
 		}
 
-		if ok := isWorker(ctx); !ok {
-			return sdk.WithStack(sdk.ErrForbidden)
+		var wk *sdk.Worker
+		var hatch *sdk.Service
+		if isWorker(ctx) {
+			wk, err = worker.LoadByID(ctx, api.mustDB(), getAPIConsumer(ctx).Worker.ID)
+			if err != nil {
+				return err
+			}
 		}
-		wk, err := worker.LoadByID(ctx, api.mustDB(), getAPIConsumer(ctx).Worker.ID)
-		if err != nil {
-			return err
+
+		if isHatchery(ctx) {
+			hatch, err = services.LoadByID(ctx, api.mustDBWithCtx(ctx), getAPIConsumer(ctx).Service.ID)
+			if err != nil {
+				return err
+			}
+		}
+
+		if wk == nil && hatch == nil {
+			return sdk.WithStack(sdk.ErrForbidden)
 		}
 
 		// Unmarshal into results
@@ -408,37 +422,48 @@ func (api *API) postWorkflowJobResultHandler() service.Handler {
 		if err := service.UnmarshalBody(r, &res); err != nil {
 			return sdk.WrapError(err, "cannot unmarshal request")
 		}
+
 		customCtx, cancel := context.WithTimeout(ctx, 180*time.Second)
 		defer cancel()
-		dbWithCtx := api.mustDBWithCtx(customCtx)
-
-		_, next := telemetry.Span(ctx, "project.LoadProjectByNodeJobRunID")
-		proj, err := project.LoadProjectByNodeJobRunID(ctx, dbWithCtx, api.Cache, id, project.LoadOptions.WithVariables)
-		next()
-		if err != nil {
-			if sdk.ErrorIs(err, sdk.ErrNoProject) {
-				_, err := workflow.LoadNodeJobRun(ctx, dbWithCtx, api.Cache, id)
-				if sdk.ErrorIs(err, sdk.ErrWorkflowNodeRunJobNotFound) {
-					// job result already send as job is no more in database
-					// this log is here to stats it and we returns nil for unlock the worker
-					// and avoid a "worker timeout"
-					log.Warn(ctx, "NodeJobRun not found: %d err:%v", id, err)
-					return nil
-				}
-				return sdk.WrapError(err, "cannot load NodeJobRun %d", id)
-			}
+		proj, err := project.LoadProjectByNodeJobRunID(ctx, api.mustDBWithCtx(customCtx), api.Cache, id, project.LoadOptions.WithVariables)
+		if err != nil {
 			return sdk.WrapError(err, "cannot load project from job %d", id)
 		}
 
-		telemetry.Current(ctx,
-			telemetry.Tag(telemetry.TagProjectKey, proj.Key),
-		)
+		telemetry.Current(ctx, telemetry.Tag(telemetry.TagProjectKey, proj.Key))
+		ctx = context.WithValue(ctx, log.Field("action_metadata_project_key"), proj.Key)
+
+		// Start the transaction
+		tx, err := api.mustDBWithCtx(ctx).Begin()
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		defer tx.Rollback() // nolint
+
+		// Load workflow node job run
+		job, err := workflow.LoadAndLockNodeJobRunSkipLocked(ctx, tx, api.Cache, res.BuildID)
+		if err != nil {
+			return sdk.WrapError(err, "cannot load node run job %d", res.BuildID)
+		}
+
+		// If the call is made by an hatchery, we have to check that the job is at status waiting
+		if hatch != nil {
+			if job.Status != sdk.StatusWaiting {
+				return sdk.WithStack(sdk.ErrForbidden)
+			}
+		}
 
-		report, err := api.postJobResult(customCtx, proj, wk, &res)
+		// Let's work
+		report, err := api.postJobResult(customCtx, tx, proj, job, wk, hatch, &res)
 		if err != nil {
 			return sdk.WrapError(err, "unable to post job result")
 		}
 
+		// Commit the transaction
+		if err := tx.Commit(); err != nil {
+			return sdk.WithStack(err)
+		}
+
 		workflowRuns := report.WorkflowRuns()
 		if len(workflowRuns) > 0 {
 			telemetry.Current(ctx,
@@ -449,9 +474,7 @@ func (api *API) postWorkflowJobResultHandler() service.Handler {
 			}
 		}
 
-		_, next = telemetry.Span(ctx, "workflow.ResyncNodeRunsWithCommits")
 		workflow.ResyncNodeRunsWithCommits(ctx, api.mustDB(), api.Cache, *proj, report)
-		next()
 
 		go api.WorkflowSendEvent(context.Background(), *proj, report)
 
@@ -459,38 +482,64 @@ func (api *API) postWorkflowJobResultHandler() service.Handler {
 	}
 }
 
-func (api *API) postJobResult(ctx context.Context, proj *sdk.Project, wr *sdk.Worker, res *sdk.Result) (*workflow.ProcessorReport, error) {
+func (api *API) postJobResult(ctx context.Context, tx gorpmapper.SqlExecutorWithTx, proj *sdk.Project, job *sdk.WorkflowNodeJobRun, wr *sdk.Worker, hatch *sdk.Service, res *sdk.Result) (*workflow.ProcessorReport, error) {
+	// Warning: here the worker "wk" of the hatchery "hatch" can be nil
+
 	var end func()
 	ctx, end = telemetry.Span(ctx, "postJobResult")
 	defer end()
 
-	//Start the transaction
-	tx, errb := api.mustDBWithCtx(ctx).Begin()
-	if errb != nil {
-		return nil, sdk.WrapError(errb, "postJobResult> Cannot begin tx")
-	}
-	defer tx.Rollback() // nolint
-
-	//Load workflow node job run
-	job, errj := workflow.LoadAndLockNodeJobRunSkipLocked(ctx, tx, api.Cache, res.BuildID)
-	if errj != nil {
-		return nil, sdk.WrapError(errj, "cannot load node run job %d", res.BuildID)
-	}
-
 	telemetry.Current(ctx,
 		telemetry.Tag(telemetry.TagWorkflowNodeJobRun, res.BuildID),
 		telemetry.Tag(telemetry.TagWorkflowNodeRun, job.WorkflowNodeRunID),
 		telemetry.Tag(telemetry.TagJob, job.Job.Action.Name))
 
-	msg := sdk.SpawnMsg{ID: sdk.MsgSpawnInfoWorkerEnd.ID, Args: []interface{}{wr.Name, res.Duration}}
-	infos := []sdk.SpawnInfo{{
-		RemoteTime:  res.RemoteTime,
-		Message:     msg,
-		UserMessage: msg.DefaultUserMessage(),
-	}}
+	// Add spawn info
+	if wr != nil {
+		msg := sdk.SpawnMsg{ID: sdk.MsgSpawnInfoWorkerEnd.ID, Args: []interface{}{wr.Name}}
+		infos := []sdk.SpawnInfo{{
+			RemoteTime:  res.RemoteTime,
+			Message:     msg,
+			UserMessage: msg.DefaultUserMessage(),
+		}}
+		if err := workflow.AddSpawnInfosNodeJobRun(tx, job.WorkflowNodeRunID, job.ID, workflow.PrepareSpawnInfos(infos)); err != nil {
+			return nil, sdk.WrapError(err, "Cannot save spawn info job %d", job.ID)
+		}
+	}
+	if hatch != nil && res.Status == sdk.StatusFail {
+		msg := sdk.SpawnMsg{ID: sdk.MsgSpawnErrorHatcheryRetryAttempt.ID, Args: []interface{}{hatch.Name, res.Reason}}
+		infos := []sdk.SpawnInfo{{
+			RemoteTime:  res.RemoteTime,
+			Message:     msg,
+			UserMessage: msg.DefaultUserMessage(),
+		}}
+		if err := workflow.AddSpawnInfosNodeJobRun(tx, job.WorkflowNodeRunID, job.ID, workflow.PrepareSpawnInfos(infos)); err != nil {
+			return nil, sdk.WrapError(err, "Cannot save spawn info job %d", job.ID)
+		}
+
+		// If the call is made by a hatchery, we have to update the stage status, because it is still at status waiting
+		nodeRun, err := workflow.LoadNodeRunByID(tx, job.WorkflowNodeRunID, workflow.LoadRunOptions{})
+		if err != nil {
+			return nil, sdk.WrapError(err, "Unable to load node run id %d", job.WorkflowNodeRunID)
+		}
 
-	if err := workflow.AddSpawnInfosNodeJobRun(tx, job.WorkflowNodeRunID, job.ID, workflow.PrepareSpawnInfos(infos)); err != nil {
-		return nil, sdk.WrapError(err, "Cannot save spawn info job %d", job.ID)
+		var stageIndex = nodeRun.GetStageIndex(job)
+		if stageIndex == -1 {
+			if err != nil {
+				return nil, sdk.NewError(sdk.ErrWrongRequest, sdk.WithStack(fmt.Errorf("stage not found")))
+			}
+		}
+		var stage = &nodeRun.Stages[stageIndex]
+		if stage.Status == sdk.StatusWaiting {
+			stage.Status = sdk.StatusBuilding
+		}
+		if nodeRun.Status == sdk.StatusWaiting {
+			nodeRun.Status = sdk.StatusBuilding
+		}
+		// Save the node run in database
+		if err := workflow.UpdateNodeRunStatusAndStage(tx, nodeRun); err != nil {
+			return nil, sdk.WrapError(err, "unable to update node id=%d at status %s", nodeRun.ID, nodeRun.Status)
+		}
 	}
 
 	// Manage build variables, we have to push them on the job and to propagate on the node above
@@ -515,6 +564,7 @@ func (api *API) postJobResult(ctx context.Context, proj *sdk.Project, wr *sdk.Wo
 		return nil, sdk.WrapError(err, "Unable to update node job run %d", res.BuildID)
 	}
 
+	// Handle build variables sent by the worker
 	if len(res.NewVariables) > 0 {
 		nodeRun, err := workflow.LoadAndLockNodeRunByID(ctx, tx, job.WorkflowNodeRunID)
 		if err != nil {
@@ -548,23 +598,30 @@ func (api *API) postJobResult(ctx context.Context, proj *sdk.Project, wr *sdk.Wo
 	}
 	// ^ build variables are now updated on job run and on node
 
-	//Update worker status
-	if err := worker.SetStatus(ctx, tx, wr.ID, sdk.StatusWaiting); err != nil {
-		return nil, sdk.WrapError(err, "cannot update worker %s status", wr.ID)
+	if wr != nil {
+		//Update worker status
+		if err := worker.SetStatus(ctx, tx, wr.ID, sdk.StatusWaiting); err != nil {
+			return nil, sdk.WrapError(err, "cannot update worker %s status", wr.ID)
+		}
 	}
 
 	// Update action status
 	log.Debug(ctx, "postJobResult> Updating %d to %s in queue", job.ID, res.Status)
 	report, err := workflow.UpdateNodeJobRunStatus(ctx, tx, api.Cache, *proj, job, res.Status)
 	if err != nil {
+		// Checking the error:
+		// pq: update or delete on table "workflow_node_run_job" violates foreign key constraint "fk_worker_workflow_node_run_job" on table "worker")
+		if wr != nil && strings.Contains(err.Error(), "fk_worker_workflow_node_run_job") {
+			log.ErrorWithStackTrace(ctx, err)
+			statusFromDB, err := tx.SelectNullStr("select status from worker where id = $1", wr.ID)
+			if err != nil {
+				log.Error(ctx, "unable to get worker %q status in db", wr.Name)
+			}
+			log.Error(ctx, "worker %q status is %q (%q in DB)", wr.Name, wr.Status, statusFromDB)
+		}
 		return nil, sdk.WrapError(err, "cannot update NodeJobRun %d status", job.ID)
 	}
 
-	//Commit the transaction
-	if err := tx.Commit(); err != nil {
-		return nil, sdk.WrapError(err, "cannot commit tx")
-	}
-
 	for i := range report.WorkflowRuns() {
 		run := &report.WorkflowRuns()[i]
 		reportParent, err := api.updateParentWorkflowRun(ctx, run)
diff --git a/engine/api/workflowtemplate/execute.go b/engine/api/workflowtemplate/execute.go
index e72924418..8aae033a7 100644
--- a/engine/api/workflowtemplate/execute.go
+++ b/engine/api/workflowtemplate/execute.go
@@ -3,7 +3,6 @@ package workflowtemplate
 import (
 	"bytes"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"regexp"
 	"strconv"
@@ -34,7 +33,7 @@ func prepareParams(wt sdk.WorkflowTemplate, r sdk.WorkflowTemplateRequest) inter
 			case sdk.ParameterTypeJSON:
 				var res interface{}
 				// safely ignore the error because the value of v has been validated on apply submit
-				_ = json.Unmarshal([]byte(v), &res)
+				_ = sdk.JSONUnmarshal([]byte(v), &res)
 				m[p.Key] = res
 			default:
 				m[p.Key] = v
diff --git a/engine/authentication/signature.go b/engine/authentication/signature.go
index b2296180e..ca9e6b3a9 100644
--- a/engine/authentication/signature.go
+++ b/engine/authentication/signature.go
@@ -103,7 +103,7 @@ func (s signer) SignJWS(content interface{}, now time.Time, duration time.Durati
 		return "", sdk.WithStack(err)
 	}
 	var jsonData map[string]interface{}
-	if err := json.Unmarshal(buf, &jsonData); err != nil {
+	if err := sdk.JSONUnmarshal(buf, &jsonData); err != nil {
 		return "", sdk.WithStack(err)
 	}
 
@@ -141,7 +141,7 @@ func (s verifier) VerifyJWS(signature string, content interface{}) error {
 	if err != nil {
 		return sdk.WrapError(err, "unable to decode payload data")
 	}
-	if err := json.Unmarshal(buf, content); err != nil {
+	if err := sdk.JSONUnmarshal(buf, content); err != nil {
 		return sdk.WrapError(err, "unable to decode payload data")
 	}
 
diff --git a/engine/cache/redis.go b/engine/cache/redis.go
index 35529f158..aaafb02a2 100644
--- a/engine/cache/redis.go
+++ b/engine/cache/redis.go
@@ -116,7 +116,7 @@ func (s *RedisStore) Get(key string, value interface{}) (bool, error) {
 		return false, sdk.WrapError(errRedis, "redis> get error %s", key)
 	}
 	if val != "" {
-		if err := json.Unmarshal([]byte(val), value); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(val), value); err != nil {
 			return false, sdk.WrapError(err, "redis> cannot get unmarshal %s", key)
 		}
 		return true, nil
@@ -281,7 +281,7 @@ func (s *RedisStore) DequeueWithContext(c context.Context, queueName string, wai
 	}
 	if elem != "" {
 		b := []byte(elem)
-		if err := json.Unmarshal(b, value); err != nil {
+		if err := sdk.JSONUnmarshal(b, value); err != nil {
 			return sdk.WrapError(err, "redis.DequeueWithContext> error on unmarshal value on queue:%s", queueName)
 		}
 	}
@@ -440,7 +440,7 @@ func (s *RedisStore) SetScan(ctx context.Context, key string, members ...interfa
 				return sdk.WithStack(fmt.Errorf("SetScan member %s not found", keys[i]))
 			}
 
-			if err := json.Unmarshal([]byte(res[i].(string)), members[i]); err != nil {
+			if err := sdk.JSONUnmarshal([]byte(res[i].(string)), members[i]); err != nil {
 				return sdk.WrapError(err, "redis> cannot unmarshal %s", keys[i])
 			}
 		}
@@ -556,7 +556,7 @@ func (s *RedisStore) ScoredSetRange(_ context.Context, key string, from, to int6
 
 	for i := 0; i < v.Len(); i++ {
 		m := v.Index(i).Interface()
-		if err := json.Unmarshal([]byte(values[i]), &m); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(values[i]), &m); err != nil {
 			return sdk.WrapError(err, "redis> cannot unmarshal %s", values[i])
 		}
 		v.Index(i).Set(reflect.ValueOf(m))
@@ -584,7 +584,7 @@ func (s *RedisStore) ScoredSetRevRange(_ context.Context, key string, offset int
 
 	for i := 0; i < v.Len(); i++ {
 		m := v.Index(i).Interface()
-		if err := json.Unmarshal([]byte(values[i]), &m); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(values[i]), &m); err != nil {
 			return sdk.WrapError(err, "redis> cannot unmarshal %s", values[i])
 		}
 		v.Index(i).Set(reflect.ValueOf(m))
@@ -625,7 +625,7 @@ func (s *RedisStore) ScoredSetScan(_ context.Context, key string, from, to float
 
 	for i := 0; i < v.Len(); i++ {
 		m := v.Index(i).Interface()
-		if err := json.Unmarshal([]byte(values[i]), &m); err != nil {
+		if err := sdk.JSONUnmarshal([]byte(values[i]), &m); err != nil {
 			return sdk.WrapError(err, "redis> cannot unmarshal %s", values[i])
 		}
 		v.Index(i).Set(reflect.ValueOf(m))
diff --git a/engine/cdn/cdn_log_engine.go b/engine/cdn/cdn_log_engine.go
index 42b214e06..cfd84dff5 100644
--- a/engine/cdn/cdn_log_engine.go
+++ b/engine/cdn/cdn_log_engine.go
@@ -2,7 +2,6 @@ package cdn
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 	"time"
@@ -119,7 +118,7 @@ func (s *Service) dequeueMessages(ctx context.Context, jobLogsQueueKey string, q
 				hms := make([]handledMessage, 0, len(msgs))
 				for _, m := range msgs {
 					var hm handledMessage
-					if err := json.Unmarshal(m, &hm); err != nil {
+					if err := sdk.JSONUnmarshal(m, &hm); err != nil {
 						return sdk.WithStack(err)
 					}
 					hms = append(hms, hm)
diff --git a/engine/cdn/item/gorp_model.go b/engine/cdn/item/gorp_model.go
index 59be8d5f1..302e9ec59 100644
--- a/engine/cdn/item/gorp_model.go
+++ b/engine/cdn/item/gorp_model.go
@@ -22,19 +22,19 @@ func (c cdnItemDB) ToCDSItem() (sdk.CDNItem, error) {
 	switch item.Type {
 	case sdk.CDNTypeItemServiceLog, sdk.CDNTypeItemStepLog:
 		var apiRef sdk.CDNLogAPIRef
-		if err := json.Unmarshal(c.APIRefDB, &apiRef); err != nil {
+		if err := sdk.JSONUnmarshal(c.APIRefDB, &apiRef); err != nil {
 			return item, sdk.WithStack(err)
 		}
 		item.APIRef = &apiRef
 	case sdk.CDNTypeItemRunResult:
 		var apiRef sdk.CDNRunResultAPIRef
-		if err := json.Unmarshal(c.APIRefDB, &apiRef); err != nil {
+		if err := sdk.JSONUnmarshal(c.APIRefDB, &apiRef); err != nil {
 			return item, sdk.WithStack(err)
 		}
 		item.APIRef = &apiRef
 	case sdk.CDNTypeItemWorkerCache:
 		var apiRef sdk.CDNWorkerCacheAPIRef
-		if err := json.Unmarshal(c.APIRefDB, &apiRef); err != nil {
+		if err := sdk.JSONUnmarshal(c.APIRefDB, &apiRef); err != nil {
 			return item, sdk.WithStack(err)
 		}
 		item.APIRef = &apiRef
diff --git a/engine/cdn/item_logs_handler.go b/engine/cdn/item_logs_handler.go
index dc89c02a7..d368fa3d5 100644
--- a/engine/cdn/item_logs_handler.go
+++ b/engine/cdn/item_logs_handler.go
@@ -3,7 +3,6 @@ package cdn
 import (
 	"bytes"
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -134,7 +133,7 @@ func (s *Service) sendLogsToWSClient(ctx context.Context, wsClient websocket.Cli
 		return sdk.WrapError(err, "cannot copy data from reader to memory buffer")
 	}
 	var lines []redis.Line
-	if err := json.Unmarshal(buf.Bytes(), &lines); err != nil {
+	if err := sdk.JSONUnmarshal(buf.Bytes(), &lines); err != nil {
 		return sdk.WrapError(err, "cannot unmarshal lines from buffer %v", string(buf.Bytes()))
 	}
 
diff --git a/engine/cdn/redis/reader.go b/engine/cdn/redis/reader.go
index f3d728a5a..a56455a1b 100644
--- a/engine/cdn/redis/reader.go
+++ b/engine/cdn/redis/reader.go
@@ -2,7 +2,6 @@ package redis
 
 import (
 	"context"
-	"encoding/json"
 	"io"
 	"sort"
 	"strings"
@@ -36,7 +35,7 @@ func (r *Reader) get(from uint, to uint) ([]Line, error) {
 	for i := range res {
 		ls[i].Number = int64(res[i].Score)
 		var value string
-		if err := json.Unmarshal(res[i].Value, &value); err != nil {
+		if err := sdk.JSONUnmarshal(res[i].Value, &value); err != nil {
 			return nil, sdk.WrapError(err, "cannot unmarshal line value from store")
 		}
 		ls[i].Value = strings.TrimFunc(value, unicode.IsNumber)
diff --git a/engine/cdn/storage/storageunit.go b/engine/cdn/storage/storageunit.go
index 6bc98c3fa..db7a74f13 100644
--- a/engine/cdn/storage/storageunit.go
+++ b/engine/cdn/storage/storageunit.go
@@ -155,7 +155,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, store cache.Store, db *gorp
 		if sdk.ErrorIs(err, sdk.ErrNotFound) {
 			var srvConfig sdk.ServiceConfig
 			b, _ := json.Marshal(bu)
-			_ = json.Unmarshal(b, &srvConfig) // nolint
+			_ = sdk.JSONUnmarshal(b, &srvConfig) // nolint
 			u = &sdk.CDNUnit{
 				ID:      sdk.UUID(),
 				Created: time.Now(),
@@ -250,7 +250,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, store cache.Store, db *gorp
 		if sdk.ErrorIs(err, sdk.ErrNotFound) {
 			var srvConfig sdk.ServiceConfig
 			b, _ := json.Marshal(cfg)
-			_ = json.Unmarshal(b, &srvConfig) // nolint
+			_ = sdk.JSONUnmarshal(b, &srvConfig) // nolint
 			u = &sdk.CDNUnit{
 				ID:      sdk.UUID(),
 				Created: time.Now(),
diff --git a/engine/cdn/websocket.go b/engine/cdn/websocket.go
index 518401ed4..19f4e4163 100644
--- a/engine/cdn/websocket.go
+++ b/engine/cdn/websocket.go
@@ -33,7 +33,7 @@ func (s *Service) initWebsocket() error {
 		telemetry.Record(s.Router.Background, s.Metrics.WSEvents, 1)
 
 		var e sdk.CDNWSEvent
-		if err := json.Unmarshal(m, &e); err != nil {
+		if err := sdk.JSONUnmarshal(m, &e); err != nil {
 			err = sdk.WrapError(err, "cannot parse event from WS broker")
 			ctx := sdk.ContextWithStacktrace(context.TODO(), err)
 			log.Warn(ctx, err.Error())
@@ -172,7 +172,7 @@ func (d *websocketClientData) ConsumeTrigger() (triggered bool) {
 
 func (d *websocketClientData) UpdateFilter(msg []byte) error {
 	var filter sdk.CDNStreamFilter
-	if err := json.Unmarshal(msg, &filter); err != nil {
+	if err := sdk.JSONUnmarshal(msg, &filter); err != nil {
 		return sdk.WithStack(err)
 	}
 	if err := filter.Validate(); err != nil {
diff --git a/engine/elasticsearch/elasticsearch_handlers.go b/engine/elasticsearch/elasticsearch_handlers.go
index c5b26d15a..bd5910e65 100644
--- a/engine/elasticsearch/elasticsearch_handlers.go
+++ b/engine/elasticsearch/elasticsearch_handlers.go
@@ -2,7 +2,6 @@ package elasticsearch
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"strings"
@@ -161,7 +160,7 @@ func (s *Service) loadMetric(ctx context.Context, ID string) (sdk.Metric, error)
 		return m, nil
 	}
 
-	if err := json.Unmarshal(*results.Hits.Hits[0].Source, &m); err != nil {
+	if err := sdk.JSONUnmarshal(*results.Hits.Hits[0].Source, &m); err != nil {
 		return m, err
 	}
 	return m, nil
diff --git a/engine/gorpmapper/encryption.go b/engine/gorpmapper/encryption.go
index e9327de4e..b5e5e8bf5 100644
--- a/engine/gorpmapper/encryption.go
+++ b/engine/gorpmapper/encryption.go
@@ -55,7 +55,7 @@ func (m *Mapper) Decrypt(src []byte, dest interface{}, extra []interface{}) erro
 		return sdk.WithStack(fmt.Errorf("unable to decrypt content: %v", err))
 	}
 
-	return json.Unmarshal(clearContent, dest)
+	return sdk.JSONUnmarshal(clearContent, dest)
 }
 
 func (m *Mapper) updateEncryptedData(db gorp.SqlExecutor, i interface{}) error {
diff --git a/engine/hatchery/marathon/marathon.go b/engine/hatchery/marathon/marathon.go
index 3d3021c0c..86e42a1de 100644
--- a/engine/hatchery/marathon/marathon.go
+++ b/engine/hatchery/marathon/marathon.go
@@ -354,7 +354,7 @@ func (h *HatcheryMarathon) SpawnWorker(ctx context.Context, spawnArgs hatchery.S
 	next()
 	if err != nil {
 		ticker.Stop()
-		return fmt.Errorf("spawnMarathonDockerWorker> failed to list deployments: %s", err.Error())
+		return sdk.WrapError(err, "failed to list deployments")
 	}
 
 	if len(deployments) == 0 {
@@ -415,7 +415,7 @@ func (h *HatcheryMarathon) SpawnWorker(ctx context.Context, spawnArgs hatchery.S
 		return nil
 	}
 
-	return fmt.Errorf("spawnMarathonDockerWorker> %s", errors.Error())
+	return fmt.Errorf(errors.Error())
 }
 
 func (h *HatcheryMarathon) listApplications(idPrefix string) ([]string, error) {
diff --git a/engine/hatchery/vsphere/hatchery.go b/engine/hatchery/vsphere/hatchery.go
index 40067b7b2..5c1b9b2e0 100644
--- a/engine/hatchery/vsphere/hatchery.go
+++ b/engine/hatchery/vsphere/hatchery.go
@@ -2,7 +2,6 @@ package vsphere
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 	"time"
@@ -215,7 +214,7 @@ func getVirtualMachineCDSAnnotation(ctx context.Context, srv mo.VirtualMachine)
 		return nil
 	}
 	var annot annotation
-	if err := json.Unmarshal([]byte(srv.Config.Annotation), &annot); err != nil {
+	if err := sdk.JSONUnmarshal([]byte(srv.Config.Annotation), &annot); err != nil {
 		return nil
 	}
 	return &annot
diff --git a/engine/hooks/bitbucket_cloud.go b/engine/hooks/bitbucket_cloud.go
index f1e3aa1d3..c05545aca 100644
--- a/engine/hooks/bitbucket_cloud.go
+++ b/engine/hooks/bitbucket_cloud.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"strings"
 
 	"github.com/rockbears/log"
@@ -14,7 +13,7 @@ func (s *Service) generatePayloadFromBitbucketCloudRequest(ctx context.Context,
 	payloads := []map[string]interface{}{}
 
 	var request BitbucketCloudWebHookEvent
-	if err := json.Unmarshal(t.WebHook.RequestBody, &request); err != nil {
+	if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &request); err != nil {
 		return nil, sdk.WrapError(err, "unable ro read bitbucket request: %s", string(t.WebHook.RequestBody))
 	}
 
diff --git a/engine/hooks/bitbucket_server.go b/engine/hooks/bitbucket_server.go
index e80aca3ef..cd69f0cec 100644
--- a/engine/hooks/bitbucket_server.go
+++ b/engine/hooks/bitbucket_server.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -15,7 +14,7 @@ func (s *Service) generatePayloadFromBitbucketServerRequest(ctx context.Context,
 	payloads := []map[string]interface{}{}
 
 	var request sdk.BitbucketServerWebhookEvent
-	if err := json.Unmarshal(t.WebHook.RequestBody, &request); err != nil {
+	if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &request); err != nil {
 		return nil, sdk.WrapError(err, "unable ro read bitbucket request: %s", string(t.WebHook.RequestBody))
 	}
 
diff --git a/engine/hooks/gerrit.go b/engine/hooks/gerrit.go
index dc96557f8..5ac6deb80 100644
--- a/engine/hooks/gerrit.go
+++ b/engine/hooks/gerrit.go
@@ -95,7 +95,7 @@ func (s *Service) doGerritExecution(e *sdk.TaskExecution) (*sdk.WorkflowNodeRunH
 	}
 
 	var gerritEvent GerritEvent
-	if err := json.Unmarshal(e.GerritEvent.Message, &gerritEvent); err != nil {
+	if err := sdk.JSONUnmarshal(e.GerritEvent.Message, &gerritEvent); err != nil {
 		return nil, sdk.WrapError(err, "unable to unmarshal gerrit event %s", string(e.GerritEvent.Message))
 	}
 
@@ -314,7 +314,7 @@ func ListenGerritStreamEvent(ctx context.Context, store cache.Store, goRoutines
 			}
 			var event GerritEvent
 			lineBytes := []byte(line)
-			if err := json.Unmarshal(lineBytes, &event); err != nil {
+			if err := sdk.JSONUnmarshal(lineBytes, &event); err != nil {
 				log.Error(ctx, "unable to read gerrit event %v: %s", err, line)
 				continue
 			}
diff --git a/engine/hooks/github.go b/engine/hooks/github.go
index 83951198e..c06401e75 100644
--- a/engine/hooks/github.go
+++ b/engine/hooks/github.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"strings"
 
 	"github.com/rockbears/log"
@@ -15,7 +14,7 @@ func (s *Service) generatePayloadFromGithubRequest(ctx context.Context, t *sdk.T
 	workflowName := t.Config["workflow"].Value
 
 	var request GithubWebHookEvent
-	if err := json.Unmarshal(t.WebHook.RequestBody, &request); err != nil {
+	if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &request); err != nil {
 		return nil, sdk.WrapError(err, "unable ro read github request: %s", string(t.WebHook.RequestBody))
 	}
 
diff --git a/engine/hooks/gitlab.go b/engine/hooks/gitlab.go
index 86441ae99..3449de31f 100644
--- a/engine/hooks/gitlab.go
+++ b/engine/hooks/gitlab.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"strings"
 
 	"github.com/rockbears/log"
@@ -15,7 +14,7 @@ func (s *Service) generatePayloadFromGitlabRequest(ctx context.Context, t *sdk.T
 	workflowName := t.Config["workflow"].Value
 
 	var request GitlabEvent
-	if err := json.Unmarshal(t.WebHook.RequestBody, &request); err != nil {
+	if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &request); err != nil {
 		return nil, sdk.WrapError(err, "unable ro read gitlab request: %s", string(t.WebHook.RequestBody))
 	}
 
diff --git a/engine/hooks/kafka.go b/engine/hooks/kafka.go
index 45b923c96..d8f9c6abc 100644
--- a/engine/hooks/kafka.go
+++ b/engine/hooks/kafka.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 	"sync/atomic"
@@ -156,10 +155,10 @@ func (s *Service) doKafkaTaskExecution(t *sdk.TaskExecution) (*sdk.WorkflowNodeR
 
 	//Try to parse the body as an array
 	bodyJSONArray := []interface{}{}
-	if err := json.Unmarshal(t.Kafka.Message, &bodyJSONArray); err != nil {
+	if err := sdk.JSONUnmarshal(t.Kafka.Message, &bodyJSONArray); err != nil {
 		//Try to parse the body as a map
 		bodyJSONMap := map[string]interface{}{}
-		if err2 := json.Unmarshal(t.Kafka.Message, &bodyJSONMap); err2 == nil {
+		if err2 := sdk.JSONUnmarshal(t.Kafka.Message, &bodyJSONMap); err2 == nil {
 			bodyJSON = bodyJSONMap
 		}
 	} else {
diff --git a/engine/hooks/outgoing_hooks.go b/engine/hooks/outgoing_hooks.go
index 876f80862..65bbf65d0 100644
--- a/engine/hooks/outgoing_hooks.go
+++ b/engine/hooks/outgoing_hooks.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"encoding/base64"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
@@ -197,7 +196,7 @@ func (s *Service) doOutgoingWorkflowExecution(ctx context.Context, t *sdk.TaskEx
 
 		var payloadInt interface{}
 		if payloadstr != "" {
-			if err := json.Unmarshal([]byte(payloadstr), &payloadInt); err == nil {
+			if err := sdk.JSONUnmarshal([]byte(payloadstr), &payloadInt); err == nil {
 				e := dump.NewDefaultEncoder()
 				e.Formatters = []dump.KeyFormatterFunc{dump.WithDefaultLowerCaseFormatter()}
 				e.ExtraFields.DetailedMap = false
diff --git a/engine/hooks/poller.go b/engine/hooks/poller.go
index 977f730e6..70b14982e 100644
--- a/engine/hooks/poller.go
+++ b/engine/hooks/poller.go
@@ -69,7 +69,7 @@ func (s *Service) doPollerTaskExecution(ctx context.Context, task *sdk.Task, tas
 	payloadValues := map[string]string{}
 	if payload, ok := task.Config["payload"]; ok && payload.Value != "{}" {
 		var payloadInt interface{}
-		if err := json.Unmarshal([]byte(payload.Value), &payloadInt); err == nil {
+		if err := sdk.JSONUnmarshal([]byte(payload.Value), &payloadInt); err == nil {
 			e := dump.NewDefaultEncoder()
 			e.Formatters = []dump.KeyFormatterFunc{dump.WithDefaultLowerCaseFormatter()}
 			e.ExtraFields.DetailedMap = false
diff --git a/engine/hooks/rabbitmq.go b/engine/hooks/rabbitmq.go
index 5a7d44974..50cd8fb67 100644
--- a/engine/hooks/rabbitmq.go
+++ b/engine/hooks/rabbitmq.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"os/signal"
@@ -103,10 +102,10 @@ func (s *Service) doRabbitMQTaskExecution(t *sdk.TaskExecution) (*sdk.WorkflowNo
 
 	//Try to parse the body as an array
 	bodyJSONArray := []interface{}{}
-	if err := json.Unmarshal(t.RabbitMQ.Message, &bodyJSONArray); err != nil {
+	if err := sdk.JSONUnmarshal(t.RabbitMQ.Message, &bodyJSONArray); err != nil {
 		//Try to parse the body as a map
 		bodyJSONMap := map[string]interface{}{}
-		if err2 := json.Unmarshal(t.RabbitMQ.Message, &bodyJSONMap); err2 == nil {
+		if err2 := sdk.JSONUnmarshal(t.RabbitMQ.Message, &bodyJSONMap); err2 == nil {
 			bodyJSON = bodyJSONMap
 		}
 	} else {
diff --git a/engine/hooks/scheduled_task.go b/engine/hooks/scheduled_task.go
index b5cd60a93..cd9e1db6b 100644
--- a/engine/hooks/scheduled_task.go
+++ b/engine/hooks/scheduled_task.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 
 	dump "github.com/fsamin/go-dump"
 	"github.com/rockbears/log"
@@ -24,7 +23,7 @@ func (s *Service) doScheduledTaskExecution(ctx context.Context, t *sdk.TaskExecu
 	payloadValues := map[string]string{}
 	if payload, ok := t.Config[sdk.Payload]; ok && payload.Value != "{}" {
 		var payloadInt interface{}
-		if err := json.Unmarshal([]byte(payload.Value), &payloadInt); err == nil {
+		if err := sdk.JSONUnmarshal([]byte(payload.Value), &payloadInt); err == nil {
 			e := dump.NewDefaultEncoder()
 			e.Formatters = []dump.KeyFormatterFunc{dump.WithDefaultLowerCaseFormatter()}
 			e.ExtraFields.DetailedMap = false
diff --git a/engine/hooks/type_github.go b/engine/hooks/type_github.go
index f116e7d7f..edb07ce37 100644
--- a/engine/hooks/type_github.go
+++ b/engine/hooks/type_github.go
@@ -1,7 +1,6 @@
 package hooks
 
 import (
-	"encoding/json"
 	"strconv"
 	"time"
 
@@ -170,7 +169,7 @@ func (g *GithubDate) UnmarshalJSON(data []byte) error {
 	if err == nil {
 		d = time.Unix(int64(dateInt), 0)
 	} else {
-		if err := json.Unmarshal(data, &d); err != nil {
+		if err := sdk.JSONUnmarshal(data, &d); err != nil {
 			return err
 		}
 	}
diff --git a/engine/hooks/webhook.go b/engine/hooks/webhook.go
index c42125b61..a19415365 100644
--- a/engine/hooks/webhook.go
+++ b/engine/hooks/webhook.go
@@ -2,7 +2,6 @@ package hooks
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"mime"
 	"net/http"
@@ -37,7 +36,7 @@ func getRepositoryHeader(t *sdk.TaskExecution, events []string) string {
 	} else if v, ok := t.WebHook.RequestHeader[BitbucketHeader]; ok {
 		if sdk.IsInArray(v[0], events) || (len(events) == 0 && (v[0] == "repo:refs_changed" || v[0] == "repo:push")) {
 			var request sdk.BitbucketServerWebhookEvent
-			if err := json.Unmarshal(t.WebHook.RequestBody, &request); err == nil && request.EventKey != "" {
+			if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &request); err == nil && request.EventKey != "" {
 				return BitbucketHeader
 			} else {
 				// We return a fake header to make a difference between server and cloud version
@@ -157,11 +156,11 @@ func executeWebHook(t *sdk.TaskExecution) (*sdk.WorkflowNodeRunHookEvent, error)
 
 			//Try to parse the body as an array
 			bodyJSONArray := []interface{}{}
-			if err := json.Unmarshal(t.WebHook.RequestBody, &bodyJSONArray); err != nil {
+			if err := sdk.JSONUnmarshal(t.WebHook.RequestBody, &bodyJSONArray); err != nil {
 
 				//Try to parse the body as a map
 				bodyJSONMap := map[string]interface{}{}
-				if err2 := json.Unmarshal(t.WebHook.RequestBody, &bodyJSONMap); err2 == nil {
+				if err2 := sdk.JSONUnmarshal(t.WebHook.RequestBody, &bodyJSONMap); err2 == nil {
 					bodyJSON = bodyJSONMap
 				}
 			} else {
diff --git a/engine/repositories/repositories_handlers.go b/engine/repositories/repositories_handlers.go
index 687b2df48..7e1cfd982 100644
--- a/engine/repositories/repositories_handlers.go
+++ b/engine/repositories/repositories_handlers.go
@@ -4,7 +4,6 @@ import (
 	"archive/tar"
 	"bytes"
 	"context"
-	"encoding/json"
 	"io"
 	"net/http"
 	"strings"
@@ -105,7 +104,7 @@ func readOperationMultipart(r *http.Request) (*sdk.Operation, map[string][]byte,
 		file.Close()
 	}
 
-	if err := json.Unmarshal([]byte(r.FormValue("dataJSON")), &op); err != nil {
+	if err := sdk.JSONUnmarshal([]byte(r.FormValue("dataJSON")), &op); err != nil {
 		return nil, nil, sdk.WithStack(err)
 	}
 	return op, files, nil
diff --git a/engine/service/http.go b/engine/service/http.go
index 3e6a54481..a89597bb1 100644
--- a/engine/service/http.go
+++ b/engine/service/http.go
@@ -138,7 +138,7 @@ func UnmarshalBody(r *http.Request, i interface{}) error {
 		return sdk.NewError(sdk.ErrWrongRequest, err)
 	}
 	defer r.Body.Close()
-	if err := json.Unmarshal(data, i); err != nil {
+	if err := sdk.JSONUnmarshal(data, i); err != nil {
 		return sdk.NewError(sdk.ErrWrongRequest, sdk.WrapError(err, "unable to unmarshal %s", string(data)))
 	}
 	return nil
diff --git a/engine/service/service.go b/engine/service/service.go
index 6d7daa3e2..f73d79347 100644
--- a/engine/service/service.go
+++ b/engine/service/service.go
@@ -129,7 +129,7 @@ func (c *Common) Register(ctx context.Context, cfg interface{}) error {
 	if err != nil {
 		return sdk.WithStack(err)
 	}
-	if err := json.Unmarshal(b, &sdkConfig); err != nil {
+	if err := sdk.JSONUnmarshal(b, &sdkConfig); err != nil {
 		return sdk.WithStack(err)
 	}
 
diff --git a/engine/sql/api/228_workflow_node_job_run_region.sql b/engine/sql/api/228_workflow_node_job_run_region.sql
index 89d2ced32..e87982da4 100644
--- a/engine/sql/api/228_workflow_node_job_run_region.sql
+++ b/engine/sql/api/228_workflow_node_job_run_region.sql
@@ -3,5 +3,5 @@ ALTER TABLE "workflow_node_run_job" ADD COLUMN IF NOT EXISTS "region" VARCHAR(25
 SELECT create_unique_index('workflow_node_run_job','IDX_WORKFLOW_NODE_RUN_JOB_REGION','region');
 
 -- +migrate Down
-DROP INDEX idx_workflow_node_run_job_region;
+DROP INDEX IF EXISTS idx_workflow_node_run_job_region;
 ALTER TABLE "workflow_node_run_job" DROP COLUMN IF EXISTS "region";
diff --git a/engine/sql/api/229_workflow_node_job_run_region_index.sql b/engine/sql/api/229_workflow_node_job_run_region_index.sql
new file mode 100644
index 000000000..c663774e7
--- /dev/null
+++ b/engine/sql/api/229_workflow_node_job_run_region_index.sql
@@ -0,0 +1,6 @@
+-- +migrate Up
+DROP INDEX IF EXISTS idx_workflow_node_run_job_region;
+SELECT create_index('workflow_node_run_job','IDX_WORKFLOW_NODE_RUN_JOB_REGION','region');
+
+-- +migrate Down
+DROP INDEX IF EXISTS idx_workflow_node_run_job_region;
diff --git a/engine/vcs/bitbucketcloud/client_branch.go b/engine/vcs/bitbucketcloud/client_branch.go
index 29108b716..8ce6fb89b 100644
--- a/engine/vcs/bitbucketcloud/client_branch.go
+++ b/engine/vcs/bitbucketcloud/client_branch.go
@@ -2,7 +2,6 @@ package bitbucketcloud
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
 
@@ -87,7 +86,7 @@ func (client *bitbucketcloudClient) Branch(ctx context.Context, fullname, theBra
 	}
 
 	var branch Branch
-	if err := json.Unmarshal(body, &branch); err != nil {
+	if err := sdk.JSONUnmarshal(body, &branch); err != nil {
 		log.Warn(ctx, "bitbucketcloudClient.Branch> Unable to parse github branch: %s", err)
 		return nil, err
 	}
diff --git a/engine/vcs/bitbucketcloud/client_commit.go b/engine/vcs/bitbucketcloud/client_commit.go
index 7268f9f57..be530d941 100644
--- a/engine/vcs/bitbucketcloud/client_commit.go
+++ b/engine/vcs/bitbucketcloud/client_commit.go
@@ -2,7 +2,6 @@ package bitbucketcloud
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"strings"
@@ -92,7 +91,7 @@ func (client *bitbucketcloudClient) Commit(ctx context.Context, repo, hash strin
 		return commit, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body))
 	}
 	var c Commit
-	if err := json.Unmarshal(body, &c); err != nil {
+	if err := sdk.JSONUnmarshal(body, &c); err != nil {
 		log.Warn(ctx, "bitbucketcloudClient.Commit> Unable to parse bitbucket cloud commit: %s", err)
 		return sdk.VCSCommit{}, err
 	}
diff --git a/engine/vcs/bitbucketcloud/client_hook.go b/engine/vcs/bitbucketcloud/client_hook.go
index 5b2c677ef..c92f59eb7 100644
--- a/engine/vcs/bitbucketcloud/client_hook.go
+++ b/engine/vcs/bitbucketcloud/client_hook.go
@@ -52,7 +52,7 @@ func (client *bitbucketcloudClient) CreateHook(ctx context.Context, repo string,
 	}
 
 	var webhook Webhook
-	if err := json.Unmarshal(body, &webhook); err != nil {
+	if err := sdk.JSONUnmarshal(body, &webhook); err != nil {
 		return sdk.WrapError(err, "Cannot unmarshal response")
 	}
 	hook.ID = webhook.UUID
diff --git a/engine/vcs/bitbucketcloud/client_pull_request.go b/engine/vcs/bitbucketcloud/client_pull_request.go
index 5573de36d..f21e9ed46 100644
--- a/engine/vcs/bitbucketcloud/client_pull_request.go
+++ b/engine/vcs/bitbucketcloud/client_pull_request.go
@@ -24,7 +24,7 @@ func (client *bitbucketcloudClient) PullRequest(ctx context.Context, fullname st
 		return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body))
 	}
 	var pullrequest PullRequest
-	if err := json.Unmarshal(body, &pullrequest); err != nil {
+	if err := sdk.JSONUnmarshal(body, &pullrequest); err != nil {
 		log.Warn(ctx, "bitbucketcloudClient.PullRequest> Unable to parse bitbucket cloud commit: %s", err)
 		return sdk.VCSPullRequest{}, err
 	}
@@ -136,7 +136,7 @@ func (client *bitbucketcloudClient) PullRequestCreate(ctx context.Context, repo
 	}
 
 	var prResponse PullRequest
-	if err := json.Unmarshal(body, &prResponse); err != nil {
+	if err := sdk.JSONUnmarshal(body, &prResponse); err != nil {
 		return sdk.VCSPullRequest{}, sdk.WrapError(err, "Unable to unmarshal pullrequest %s", string(body))
 	}
 
diff --git a/engine/vcs/bitbucketcloud/client_repos.go b/engine/vcs/bitbucketcloud/client_repos.go
index 11681208b..ec1a3dc0e 100644
--- a/engine/vcs/bitbucketcloud/client_repos.go
+++ b/engine/vcs/bitbucketcloud/client_repos.go
@@ -2,7 +2,6 @@ package bitbucketcloud
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
 
@@ -127,7 +126,7 @@ func (client *bitbucketcloudClient) repoByFullname(ctx context.Context, fullname
 		return repo, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body))
 	}
 
-	if err := json.Unmarshal(body, &repo); err != nil {
+	if err := sdk.JSONUnmarshal(body, &repo); err != nil {
 		return repo, sdk.WrapError(err, "Unable to parse github repository")
 	}
 
diff --git a/engine/vcs/bitbucketcloud/client_status.go b/engine/vcs/bitbucketcloud/client_status.go
index b607ee276..98bab4aef 100644
--- a/engine/vcs/bitbucketcloud/client_status.go
+++ b/engine/vcs/bitbucketcloud/client_status.go
@@ -78,7 +78,7 @@ func (client *bitbucketcloudClient) SetStatus(ctx context.Context, event sdk.Eve
 	}
 
 	var resp Status
-	if err := json.Unmarshal(body, &resp); err != nil {
+	if err := sdk.JSONUnmarshal(body, &resp); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal body")
 	}
 
@@ -97,7 +97,7 @@ func (client *bitbucketcloudClient) ListStatuses(ctx context.Context, repo strin
 		return []sdk.VCSCommitStatus{}, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body))
 	}
 	var ss Statuses
-	if err := json.Unmarshal(body, &ss); err != nil {
+	if err := sdk.JSONUnmarshal(body, &ss); err != nil {
 		return []sdk.VCSCommitStatus{}, sdk.WrapError(err, "Unable to parse bitbucket cloud commit: %s", ref)
 	}
 
@@ -133,7 +133,7 @@ func processBbitbucketState(s Status) string {
 func processEventWorkflowNodeRun(event sdk.Event, cdsUIURL string, disabledStatusDetail bool) (statusData, error) {
 	data := statusData{}
 	var eventNR sdk.EventRunWorkflowNode
-	if err := json.Unmarshal(event.Payload, &eventNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNR); err != nil {
 		return data, sdk.WrapError(err, "cannot unmarshal payload")
 	}
 	//We only manage status Success, Failure and Stopped
diff --git a/engine/vcs/bitbucketcloud/client_user.go b/engine/vcs/bitbucketcloud/client_user.go
index 913d3a767..be5f5248b 100644
--- a/engine/vcs/bitbucketcloud/client_user.go
+++ b/engine/vcs/bitbucketcloud/client_user.go
@@ -2,7 +2,6 @@ package bitbucketcloud
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/rockbears/log"
@@ -23,7 +22,7 @@ func (client *bitbucketcloudClient) User(ctx context.Context, username string) (
 	if status >= 400 {
 		return user, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body))
 	}
-	if err := json.Unmarshal(body, &user); err != nil {
+	if err := sdk.JSONUnmarshal(body, &user); err != nil {
 		log.Warn(ctx, "bitbucketcloudClient.User> Unable to parse bitbucket cloud commit: %s", err)
 		return user, err
 	}
@@ -50,7 +49,7 @@ func (client *bitbucketcloudClient) CurrentUser(ctx context.Context) (User, erro
 		if status >= 400 {
 			return user, sdk.NewError(sdk.ErrUserNotFound, errorAPI(body))
 		}
-		if err := json.Unmarshal(body, &user); err != nil {
+		if err := sdk.JSONUnmarshal(body, &user); err != nil {
 			return user, sdk.WithStack(err)
 		}
 		//Put the body on cache for 1 hour
@@ -81,7 +80,7 @@ func (client *bitbucketcloudClient) Teams(ctx context.Context) (Teams, error) {
 		if status >= 400 {
 			return teams, sdk.NewError(sdk.ErrNotFound, errorAPI(body))
 		}
-		if err := json.Unmarshal(body, &teams); err != nil {
+		if err := sdk.JSONUnmarshal(body, &teams); err != nil {
 			return teams, sdk.WithStack(err)
 		}
 		//Put the body on cache for 1 hour
diff --git a/engine/vcs/bitbucketcloud/error.go b/engine/vcs/bitbucketcloud/error.go
index 18be96441..38c6d4a67 100644
--- a/engine/vcs/bitbucketcloud/error.go
+++ b/engine/vcs/bitbucketcloud/error.go
@@ -1,8 +1,9 @@
 package bitbucketcloud
 
 import (
-	"encoding/json"
 	"fmt"
+
+	"github.com/ovh/cds/sdk"
 )
 
 //Error wraps bitbucketcloud error format
@@ -36,6 +37,6 @@ var (
 //errorAPI creates a new error
 func errorAPI(body []byte) error {
 	var res Error
-	_ = json.Unmarshal(body, &res)
+	_ = sdk.JSONUnmarshal(body, &res)
 	return res
 }
diff --git a/engine/vcs/bitbucketcloud/http.go b/engine/vcs/bitbucketcloud/http.go
index f923b71df..a8ce2cfdb 100644
--- a/engine/vcs/bitbucketcloud/http.go
+++ b/engine/vcs/bitbucketcloud/http.go
@@ -3,7 +3,6 @@ package bitbucketcloud
 import (
 	"bytes"
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -51,7 +50,7 @@ func (consumer *bitbucketcloudConsumer) postForm(url string, data url.Values, he
 
 	if res.StatusCode > 400 {
 		var errBb Error
-		if err := json.Unmarshal(resBody, &errBb); err == nil {
+		if err := sdk.JSONUnmarshal(resBody, &errBb); err == nil {
 			return res.StatusCode, resBody, errBb
 		}
 	}
@@ -215,5 +214,5 @@ func (client *bitbucketcloudClient) do(ctx context.Context, method, api, path st
 		return sdk.WithStack(sdk.ErrWrongRequest)
 	}
 
-	return sdk.WithStack(json.Unmarshal(body, v))
+	return sdk.WithStack(sdk.JSONUnmarshal(body, v))
 }
diff --git a/engine/vcs/bitbucketcloud/oauth_consumer.go b/engine/vcs/bitbucketcloud/oauth_consumer.go
index 701870562..7455abc8b 100644
--- a/engine/vcs/bitbucketcloud/oauth_consumer.go
+++ b/engine/vcs/bitbucketcloud/oauth_consumer.go
@@ -2,7 +2,6 @@ package bitbucketcloud
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"time"
@@ -55,7 +54,7 @@ func (consumer *bitbucketcloudConsumer) AuthorizeToken(ctx context.Context, _, c
 	}
 
 	var resp AccessToken
-	if err := json.Unmarshal(res, &resp); err != nil {
+	if err := sdk.JSONUnmarshal(res, &resp); err != nil {
 		return "", "", fmt.Errorf("Unable to parse bitbucketcloud response (%d) %s ", status, string(res))
 	}
 
@@ -81,7 +80,7 @@ func (consumer *bitbucketcloudConsumer) RefreshToken(ctx context.Context, refres
 	}
 
 	var resp AccessToken
-	if err := json.Unmarshal(res, &resp); err != nil {
+	if err := sdk.JSONUnmarshal(res, &resp); err != nil {
 		return "", "", fmt.Errorf("Unable to parse bitbucketcloud response (%d) %s ", status, string(res))
 	}
 
diff --git a/engine/vcs/bitbucketserver/client_status.go b/engine/vcs/bitbucketserver/client_status.go
index 6b8d193e0..b2a7d936c 100644
--- a/engine/vcs/bitbucketserver/client_status.go
+++ b/engine/vcs/bitbucketserver/client_status.go
@@ -128,7 +128,7 @@ const (
 func processWorkflowNodeRunEvent(event sdk.Event, uiURL string) (statusData, error) {
 	data := statusData{}
 	var eventNR sdk.EventRunWorkflowNode
-	if err := json.Unmarshal(event.Payload, &eventNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNR); err != nil {
 		return data, sdk.WrapError(err, "cannot unmarshal payload")
 	}
 	data.key = fmt.Sprintf("%s-%s-%s",
diff --git a/engine/vcs/bitbucketserver/http.go b/engine/vcs/bitbucketserver/http.go
index a7c522b95..724db3d08 100644
--- a/engine/vcs/bitbucketserver/http.go
+++ b/engine/vcs/bitbucketserver/http.go
@@ -186,7 +186,7 @@ func (c *bitbucketClient) do(ctx context.Context, method, api, path string, para
 
 		// bitbucket can return 204 with no-content
 		if resp.StatusCode != 204 || strings.TrimSpace(string(body)) != "" {
-			if err := json.Unmarshal(body, v); err != nil {
+			if err := sdk.JSONUnmarshal(body, v); err != nil {
 				return err
 			}
 		}
diff --git a/engine/vcs/gerrit/client_status.go b/engine/vcs/gerrit/client_status.go
index 61da3d02a..909371cf2 100644
--- a/engine/vcs/gerrit/client_status.go
+++ b/engine/vcs/gerrit/client_status.go
@@ -2,7 +2,6 @@ package gerrit
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -15,7 +14,7 @@ import (
 // SetStatus set build status on Gerrit
 func (c *gerritClient) SetStatus(ctx context.Context, event sdk.Event) error {
 	var eventNR sdk.EventRunWorkflowNode
-	if err := json.Unmarshal(event.Payload, &eventNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNR); err != nil {
 		return sdk.WrapError(err, "cannot unmarshal payload")
 	}
 
diff --git a/engine/vcs/github/client_branch.go b/engine/vcs/github/client_branch.go
index 1e04db9ee..ae6bb37ab 100644
--- a/engine/vcs/github/client_branch.go
+++ b/engine/vcs/github/client_branch.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/http"
 
@@ -63,7 +62,7 @@ func (g *githubClient) Branches(ctx context.Context, fullname string) ([]sdk.VCS
 			noEtag = true
 			continue
 		} else {
-			if err := json.Unmarshal(body, &nextBranches); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextBranches); err != nil {
 				log.Warn(ctx, "githubClient.Branches> Unable to parse github branches: %s", err)
 				return nil, err
 			}
@@ -134,7 +133,7 @@ func (g *githubClient) Branch(ctx context.Context, fullname, theBranch string) (
 			log.Error(ctx, "Unable to get branch (%s) from the cache", cacheBranchKey)
 		}
 	} else {
-		if err := json.Unmarshal(body, &branch); err != nil {
+		if err := sdk.JSONUnmarshal(body, &branch); err != nil {
 			log.Warn(ctx, "githubClient.Branch> Unable to parse github branch: %s", err)
 			return nil, err
 		}
diff --git a/engine/vcs/github/client_commit.go b/engine/vcs/github/client_commit.go
index 629663f95..f2bacfc2c 100644
--- a/engine/vcs/github/client_commit.go
+++ b/engine/vcs/github/client_commit.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -213,7 +212,7 @@ func (g *githubClient) allCommitBetween(ctx context.Context, repo string, untilD
 		}
 		nextCommits := []Commit{}
 
-		if err := json.Unmarshal(body, &nextCommits); err != nil {
+		if err := sdk.JSONUnmarshal(body, &nextCommits); err != nil {
 			log.Warn(ctx, "githubClient.Commits> Unable to parse github commits: %s", err)
 			return nil, err
 		}
@@ -247,7 +246,7 @@ func (g *githubClient) Commit(ctx context.Context, repo, hash string) (sdk.VCSCo
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(body, &c); err != nil {
+		if err := sdk.JSONUnmarshal(body, &c); err != nil {
 			log.Warn(ctx, "githubClient.Commit> Unable to parse github commit: %s", err)
 			return sdk.VCSCommit{}, err
 		}
@@ -295,7 +294,7 @@ func (g *githubClient) CommitsBetweenRefs(ctx context.Context, repo, base, head
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(body, &diff); err != nil {
+		if err := sdk.JSONUnmarshal(body, &diff); err != nil {
 			log.Warn(ctx, "githubClient.CommitsBetweenRefs> Unable to parse github commit: %s", err)
 			return commits, err
 		}
diff --git a/engine/vcs/github/client_event.go b/engine/vcs/github/client_event.go
index 33d4c0950..ee491009f 100644
--- a/engine/vcs/github/client_event.go
+++ b/engine/vcs/github/client_event.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -44,7 +43,7 @@ func (g *githubClient) GetEvents(ctx context.Context, fullname string, dateRef t
 	}
 
 	nextEvents := []Event{}
-	if err := json.Unmarshal(body, &nextEvents); err != nil {
+	if err := sdk.JSONUnmarshal(body, &nextEvents); err != nil {
 		log.Warn(ctx, "githubClient.GetEvents> Unable to parse github events: %s", err)
 		return nil, interval, fmt.Errorf("Unable to parse github events %s: %s", string(body), err)
 	}
diff --git a/engine/vcs/github/client_forks.go b/engine/vcs/github/client_forks.go
index eb5fd2f0c..ac97364be 100644
--- a/engine/vcs/github/client_forks.go
+++ b/engine/vcs/github/client_forks.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 	"strconv"
 	"strings"
@@ -57,7 +56,7 @@ func (g *githubClient) ListForks(ctx context.Context, repo string) ([]sdk.VCSRep
 			noEtag = true
 			continue
 		} else {
-			if err := json.Unmarshal(body, &nextRepos); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextRepos); err != nil {
 				log.Warn(ctx, "githubClient.ListForks> Unable to parse github repositories: %s", err)
 				return nil, err
 			}
diff --git a/engine/vcs/github/client_hook.go b/engine/vcs/github/client_hook.go
index 6c09e2dbf..dd4ff1149 100644
--- a/engine/vcs/github/client_hook.go
+++ b/engine/vcs/github/client_hook.go
@@ -59,7 +59,7 @@ func (g *githubClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC
 		return sdk.WrapError(err, "github.CreateHook. Data : %s", b)
 	}
 
-	if err := json.Unmarshal(body, &r); err != nil {
+	if err := sdk.JSONUnmarshal(body, &r); err != nil {
 		return sdk.WrapError(err, "Cannot unmarshal response")
 	}
 	hook.ID = fmt.Sprintf("%d", r.ID)
@@ -141,7 +141,7 @@ func (g *githubClient) getHooks(ctx context.Context, fullname string) ([]Webhook
 			}
 			break
 		} else {
-			if err := json.Unmarshal(body, &nextHooks); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextHooks); err != nil {
 				log.Warn(ctx, "githubClient.getHooks> Unable to parse github hooks: %s", err)
 				return nil, err
 			}
@@ -206,7 +206,7 @@ func (g *githubClient) getHookByID(ctx context.Context, fullname, id string) (We
 			return webhook, sdk.WithStack(fmt.Errorf("unable to get getHooks (%s) from the cache", strings.ReplaceAll(cacheKey, g.OAuthToken, "")))
 		}
 	} else {
-		if err := json.Unmarshal(body, &webhook); err != nil {
+		if err := sdk.JSONUnmarshal(body, &webhook); err != nil {
 			log.Warn(ctx, "githubClient.getHookByID> Unable to parse github hook: %v", err)
 			return webhook, err
 		}
diff --git a/engine/vcs/github/client_pull_request.go b/engine/vcs/github/client_pull_request.go
index d78b09eb5..04bcf4656 100644
--- a/engine/vcs/github/client_pull_request.go
+++ b/engine/vcs/github/client_pull_request.go
@@ -57,7 +57,7 @@ func (g *githubClient) PullRequest(ctx context.Context, fullname string, id stri
 			}
 
 		} else {
-			if err := json.Unmarshal(body, &pr); err != nil {
+			if err := sdk.JSONUnmarshal(body, &pr); err != nil {
 				log.Warn(ctx, "githubClient.PullRequest> Unable to parse github pullrequest: %s", err)
 				return sdk.VCSPullRequest{}, sdk.WithStack(err)
 			}
@@ -118,7 +118,7 @@ func (g *githubClient) PullRequests(ctx context.Context, fullname string, opts s
 			}
 			break
 		} else {
-			if err := json.Unmarshal(body, &nextPullRequests); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextPullRequests); err != nil {
 				log.Warn(ctx, "githubClient.Branches> Unable to parse github branches: %s", err)
 				return nil, err
 			}
@@ -232,7 +232,7 @@ func (g *githubClient) PullRequestCreate(ctx context.Context, repo string, pr sd
 	}
 
 	var prResponse PullRequest
-	if err := json.Unmarshal(body, &prResponse); err != nil {
+	if err := sdk.JSONUnmarshal(body, &prResponse); err != nil {
 		return sdk.VCSPullRequest{}, sdk.WrapError(err, "Unable to unmarshal pullrequest %s", string(body))
 	}
 
diff --git a/engine/vcs/github/client_rate_limit.go b/engine/vcs/github/client_rate_limit.go
index e28ccfb42..bb9f1bf18 100644
--- a/engine/vcs/github/client_rate_limit.go
+++ b/engine/vcs/github/client_rate_limit.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 	"strings"
 	"time"
@@ -37,7 +36,7 @@ func (g *githubClient) RateLimit(ctx context.Context) error {
 		return sdk.NewError(sdk.ErrUnknownError, errorAPI(body))
 	}
 	rateLimit := &RateLimit{}
-	if err := json.Unmarshal(body, rateLimit); err != nil {
+	if err := sdk.JSONUnmarshal(body, rateLimit); err != nil {
 		log.Warn(ctx, "githubClient.RateLimit> Error %s", err)
 		return err
 	}
diff --git a/engine/vcs/github/client_release.go b/engine/vcs/github/client_release.go
index 962804299..c4c4c3e26 100644
--- a/engine/vcs/github/client_release.go
+++ b/engine/vcs/github/client_release.go
@@ -44,7 +44,7 @@ func (g *githubClient) Release(ctx context.Context, fullname string, tagName str
 	}
 
 	var response ReleaseResponse
-	if err := json.Unmarshal(body, &response); err != nil {
+	if err := sdk.JSONUnmarshal(body, &response); err != nil {
 		return nil, sdk.WrapError(err, "Cannot unmarshal response: %s", string(body))
 	}
 
diff --git a/engine/vcs/github/client_repos.go b/engine/vcs/github/client_repos.go
index 288e44017..0e9360c2f 100644
--- a/engine/vcs/github/client_repos.go
+++ b/engine/vcs/github/client_repos.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -61,7 +60,7 @@ func (g *githubClient) Repos(ctx context.Context) ([]sdk.VCSRepo, error) {
 			noEtag = true
 			continue
 		} else {
-			if err := json.Unmarshal(body, &nextRepos); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextRepos); err != nil {
 				log.Warn(ctx, "githubClient.Repos> Unable to parse github repositories: %s", err)
 				return nil, err
 			}
@@ -138,7 +137,7 @@ func (g *githubClient) repoByFullname(ctx context.Context, fullname string) (Rep
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(body, &repo); err != nil {
+		if err := sdk.JSONUnmarshal(body, &repo); err != nil {
 			log.Warn(ctx, "githubClient.Repos> Unable to parse github repository: %s", err)
 			return Repository{}, err
 		}
@@ -178,7 +177,7 @@ func (g *githubClient) UserHasWritePermission(ctx context.Context, fullname stri
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(resp, &permResp); err != nil {
+		if err := sdk.JSONUnmarshal(resp, &permResp); err != nil {
 			return false, sdk.WrapError(err, "unable to unmarshal: %s", string(resp))
 		}
 		if err := g.Cache.SetWithTTL(k, permResp, 61*60); err != nil {
@@ -218,7 +217,7 @@ func (g *githubClient) GrantWritePermission(ctx context.Context, fullname string
 	// Response when a new invitation is created
 	if resp.StatusCode == 201 {
 		invit := RepositoryInvitation{}
-		if err := json.Unmarshal(body, &invit); err != nil {
+		if err := sdk.JSONUnmarshal(body, &invit); err != nil {
 			log.Warn(ctx, "githubClient.GrantWritePermission> unable to unmarshal invitation %s", err)
 			return err
 		}
diff --git a/engine/vcs/github/client_status.go b/engine/vcs/github/client_status.go
index c959d6f62..1ef4b1b6d 100644
--- a/engine/vcs/github/client_status.go
+++ b/engine/vcs/github/client_status.go
@@ -87,7 +87,7 @@ func (g *githubClient) SetStatus(ctx context.Context, event sdk.Event) error {
 	}
 
 	s := &Status{}
-	if err := json.Unmarshal(body, s); err != nil {
+	if err := sdk.JSONUnmarshal(body, s); err != nil {
 		return sdk.WrapError(err, "Unable to unmarshal body")
 	}
 
@@ -115,7 +115,7 @@ func (g *githubClient) ListStatuses(ctx context.Context, repo string, ref string
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(body, &ss); err != nil {
+		if err := sdk.JSONUnmarshal(body, &ss); err != nil {
 			return []sdk.VCSCommitStatus{}, sdk.WrapError(err, "Unable to parse github commit: %s", ref)
 		}
 		//Put the body on cache for one hour and one minute
@@ -155,7 +155,7 @@ func processGithubState(s Status) string {
 func processEventWorkflowNodeRun(event sdk.Event, cdsUIURL string, disabledStatusDetail bool) (statusData, error) {
 	data := statusData{}
 	var eventNR sdk.EventRunWorkflowNode
-	if err := json.Unmarshal(event.Payload, &eventNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNR); err != nil {
 		return data, sdk.WrapError(err, "cannot unmarshal payload")
 	}
 	//We only manage status Success and Failure
diff --git a/engine/vcs/github/client_tag.go b/engine/vcs/github/client_tag.go
index f9887229f..df85c3d1d 100644
--- a/engine/vcs/github/client_tag.go
+++ b/engine/vcs/github/client_tag.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 	"strings"
 
@@ -61,7 +60,7 @@ func (g *githubClient) Tags(ctx context.Context, fullname string) ([]sdk.VCSTag,
 			noEtag = true
 			continue
 		} else {
-			if err := json.Unmarshal(body, &nextTags); err != nil {
+			if err := sdk.JSONUnmarshal(body, &nextTags); err != nil {
 				log.Warn(ctx, "githubClient.Tags> Unable to parse github tags: %s", err)
 				return nil, err
 			}
diff --git a/engine/vcs/github/client_user.go b/engine/vcs/github/client_user.go
index 980d13889..c3b00dcc5 100644
--- a/engine/vcs/github/client_user.go
+++ b/engine/vcs/github/client_user.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 
 	"github.com/rockbears/log"
@@ -33,7 +32,7 @@ func (g *githubClient) User(ctx context.Context, username string) (User, error)
 			log.Error(ctx, "cannot get from cache %s: %v", k, err)
 		}
 	} else {
-		if err := json.Unmarshal(body, &user); err != nil {
+		if err := sdk.JSONUnmarshal(body, &user); err != nil {
 			return User{}, err
 		}
 		//Put the body on cache for one hour and one minute
diff --git a/engine/vcs/github/error.go b/engine/vcs/github/error.go
index 400c64047..aeb9672c8 100644
--- a/engine/vcs/github/error.go
+++ b/engine/vcs/github/error.go
@@ -1,8 +1,9 @@
 package github
 
 import (
-	"encoding/json"
 	"fmt"
+
+	"github.com/ovh/cds/sdk"
 )
 
 //Error wraps github error format
@@ -38,7 +39,7 @@ var (
 //errorAPI creates a new error
 func errorAPI(body []byte) error {
 	res := map[string]string{}
-	json.Unmarshal(body, &res)
+	sdk.JSONUnmarshal(body, &res)
 	return ghError{
 		ID:   "api_error",
 		Desc: res["message"],
diff --git a/engine/vcs/github/http.go b/engine/vcs/github/http.go
index 546bcf173..a27e008a9 100644
--- a/engine/vcs/github/http.go
+++ b/engine/vcs/github/http.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -57,7 +56,7 @@ func (g *githubConsumer) postForm(path string, data url.Values, headers map[stri
 
 	if res.StatusCode > 400 {
 		ghErr := &ghError{}
-		if err := json.Unmarshal(resBody, ghErr); err == nil {
+		if err := sdk.JSONUnmarshal(resBody, ghErr); err == nil {
 			return res.StatusCode, resBody, ghErr
 		}
 	}
diff --git a/engine/vcs/github/oauth_consumer.go b/engine/vcs/github/oauth_consumer.go
index 111ee0b57..d1b766a55 100644
--- a/engine/vcs/github/oauth_consumer.go
+++ b/engine/vcs/github/oauth_consumer.go
@@ -2,7 +2,6 @@ package github
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
 	"strings"
@@ -67,7 +66,7 @@ func (g *githubConsumer) AuthorizeToken(ctx context.Context, state, code string)
 	}
 
 	ghResponse := map[string]string{}
-	if err := json.Unmarshal(res, &ghResponse); err != nil {
+	if err := sdk.JSONUnmarshal(res, &ghResponse); err != nil {
 		return "", "", fmt.Errorf("Unable to parse github response (%d) %s ", status, string(res))
 	}
 
diff --git a/engine/vcs/gitlab/client_status.go b/engine/vcs/gitlab/client_status.go
index cbfbd3678..2a57e35d9 100644
--- a/engine/vcs/gitlab/client_status.go
+++ b/engine/vcs/gitlab/client_status.go
@@ -2,7 +2,6 @@ package gitlab
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -145,7 +144,7 @@ func processGitlabState(s gitlab.CommitStatus) string {
 func processWorkflowNodeRunEvent(event sdk.Event, uiURL string) (statusData, error) {
 	data := statusData{}
 	var eventNR sdk.EventRunWorkflowNode
-	if err := json.Unmarshal(event.Payload, &eventNR); err != nil {
+	if err := sdk.JSONUnmarshal(event.Payload, &eventNR); err != nil {
 		return data, sdk.WrapError(err, "cannot read payload")
 	}
 
diff --git a/engine/vcs/gitlab/oauth_consumer.go b/engine/vcs/gitlab/oauth_consumer.go
index f71a6ca10..50146b722 100644
--- a/engine/vcs/gitlab/oauth_consumer.go
+++ b/engine/vcs/gitlab/oauth_consumer.go
@@ -2,7 +2,6 @@ package gitlab
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -76,7 +75,7 @@ func (g *gitlabConsumer) postForm(path string, data url.Values, headers map[stri
 
 	if res.StatusCode > 400 {
 		glErr := &Error{}
-		if err := json.Unmarshal(resBody, glErr); err == nil {
+		if err := sdk.JSONUnmarshal(resBody, glErr); err == nil {
 			return res.StatusCode, resBody, fmt.Errorf("%s: %s", glErr.Error, glErr.Description)
 		}
 	}
@@ -109,7 +108,7 @@ func (g *gitlabConsumer) AuthorizeToken(ctx context.Context, state, code string)
 	}
 
 	glResponse := authorizeResponse{}
-	if err := json.Unmarshal(res, &glResponse); err != nil {
+	if err := sdk.JSONUnmarshal(res, &glResponse); err != nil {
 		return "", "", fmt.Errorf("Unable to parse gitlab response (%d) %s ", status, string(res))
 	}
 
diff --git a/engine/worker/cmd_cache.go b/engine/worker/cmd_cache.go
index 71bc5b006..6f4819e26 100644
--- a/engine/worker/cmd_cache.go
+++ b/engine/worker/cmd_cache.go
@@ -147,7 +147,7 @@ func cachePushCmd() func(cmd *cobra.Command, args []string) {
 				sdk.Exit("cache push HTTP %d error %v", resp.StatusCode, err)
 			}
 			var sdkErr sdk.Error
-			if json.Unmarshal(body, &sdkErr); err != nil {
+			if sdk.JSONUnmarshal(body, &sdkErr); err != nil {
 				sdk.Exit("unable to read error: %s: %v", string(body), err)
 			}
 			sdk.Exit("%v", sdkErr)
@@ -236,7 +236,7 @@ func cachePullCmd() func(cmd *cobra.Command, args []string) {
 				sdk.Exit("cache pull HTTP %d error %v", resp.StatusCode, err)
 			}
 			var sdkErr sdk.Error
-			if json.Unmarshal(body, &sdkErr); err != nil {
+			if sdk.JSONUnmarshal(body, &sdkErr); err != nil {
 				sdk.Exit("unable to read error: %s: %v", string(body), err)
 			}
 			sdk.Exit("%v", sdkErr)
diff --git a/engine/worker/cmd_key.go b/engine/worker/cmd_key.go
index b162c4fd6..b627865ab 100644
--- a/engine/worker/cmd_key.go
+++ b/engine/worker/cmd_key.go
@@ -150,7 +150,7 @@ func keyInstallCmd() func(cmd *cobra.Command, args []string) {
 		defer resp.Body.Close() // nolint
 
 		var keyResp workerruntime.KeyResponse
-		if err := json.Unmarshal(bodyBtes, &keyResp); err != nil {
+		if err := sdk.JSONUnmarshal(bodyBtes, &keyResp); err != nil {
 			sdk.Exit("Error: worker key install> cannot unmarshall key response: %s", string(bodyBtes))
 		}
 
diff --git a/engine/worker/internal/handler_artifacts.go b/engine/worker/internal/handler_artifacts.go
index 478e25960..6cbb39518 100644
--- a/engine/worker/internal/handler_artifacts.go
+++ b/engine/worker/internal/handler_artifacts.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -29,7 +28,7 @@ func artifactsHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		defer r.Body.Close()
 
 		var reqArgs workerruntime.DownloadArtifact
-		if err := json.Unmarshal(data, &reqArgs); err != nil {
+		if err := sdk.JSONUnmarshal(data, &reqArgs); err != nil {
 			newError := sdk.NewError(sdk.ErrWrongRequest, err)
 			writeError(w, r, newError)
 			return
diff --git a/engine/worker/internal/handler_cache.go b/engine/worker/internal/handler_cache.go
index e189f1427..8ae4f0b72 100644
--- a/engine/worker/internal/handler_cache.go
+++ b/engine/worker/internal/handler_cache.go
@@ -3,7 +3,6 @@ package internal
 import (
 	"archive/tar"
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -40,7 +39,7 @@ func cachePushHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		}
 
 		var c sdk.Cache
-		if err := json.Unmarshal(data, &c); err != nil {
+		if err := sdk.JSONUnmarshal(data, &c); err != nil {
 			err = sdk.Error{
 				Message: "worker cache push > Cannot unmarshall body : " + err.Error(),
 				Status:  http.StatusInternalServerError,
diff --git a/engine/worker/internal/handler_cds_version_set.go b/engine/worker/internal/handler_cds_version_set.go
index c67cfe4da..62b90c48a 100644
--- a/engine/worker/internal/handler_cds_version_set.go
+++ b/engine/worker/internal/handler_cds_version_set.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 
@@ -24,7 +23,7 @@ func setVersionHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		defer r.Body.Close()
 
 		var req workerruntime.CDSVersionSet
-		if err := json.Unmarshal(data, &req); err != nil {
+		if err := sdk.JSONUnmarshal(data, &req); err != nil {
 			writeError(w, r, sdk.NewError(sdk.ErrWrongRequest, err))
 			return
 		}
diff --git a/engine/worker/internal/handler_check_secret.go b/engine/worker/internal/handler_check_secret.go
index 057a5c641..1c1221ba2 100644
--- a/engine/worker/internal/handler_check_secret.go
+++ b/engine/worker/internal/handler_check_secret.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -25,7 +24,7 @@ func checkSecretHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		}
 
 		var a workerruntime.FilePath
-		if err := json.Unmarshal(data, &a); err != nil {
+		if err := sdk.JSONUnmarshal(data, &a); err != nil {
 			returnHTTPError(ctx, w, 400, fmt.Errorf("failed to unmarshal %s", data))
 			return
 		}
diff --git a/engine/worker/internal/handler_download.go b/engine/worker/internal/handler_download.go
index b2e7124d1..bd8d25e65 100644
--- a/engine/worker/internal/handler_download.go
+++ b/engine/worker/internal/handler_download.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -35,7 +34,7 @@ func downloadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		defer r.Body.Close() // nolint
 
 		var reqArgs workerruntime.DownloadArtifact
-		if err := json.Unmarshal(data, &reqArgs); err != nil {
+		if err := sdk.JSONUnmarshal(data, &reqArgs); err != nil {
 			newError := sdk.NewError(sdk.ErrWrongRequest, err)
 			writeError(w, r, newError)
 			return
diff --git a/engine/worker/internal/handler_export.go b/engine/worker/internal/handler_export.go
index d25ecedea..39c385179 100644
--- a/engine/worker/internal/handler_export.go
+++ b/engine/worker/internal/handler_export.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 
@@ -27,7 +26,7 @@ func addBuildVarHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		}
 
 		var v sdk.Variable
-		if err := json.Unmarshal(data, &v); err != nil {
+		if err := sdk.JSONUnmarshal(data, &v); err != nil {
 			log.Error(ctx, "addBuildVarHandler> Cannot Unmarshal err: %s", err)
 			w.WriteHeader(http.StatusBadRequest)
 			return
diff --git a/engine/worker/internal/handler_key.go b/engine/worker/internal/handler_key.go
index 8de420687..a07ca86d4 100644
--- a/engine/worker/internal/handler_key.go
+++ b/engine/worker/internal/handler_key.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -33,7 +32,7 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 
 		var mapBody = make(map[string]string)
 		if len(body) > 0 {
-			if err := json.Unmarshal(body, &mapBody); err != nil {
+			if err := sdk.JSONUnmarshal(body, &mapBody); err != nil {
 				writeError(w, r, sdk.NewError(sdk.ErrWrongRequest, err))
 				return
 			}
diff --git a/engine/worker/internal/handler_run_result.go b/engine/worker/internal/handler_run_result.go
index 7a5ec9bec..bec15eb47 100644
--- a/engine/worker/internal/handler_run_result.go
+++ b/engine/worker/internal/handler_run_result.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -52,7 +51,7 @@ func addRunResulthandler(ctx context.Context, wk *CurrentWorker) http.HandlerFun
 		defer r.Body.Close() //nolint
 
 		var reqArgs sdk.WorkflowRunResultArtifactManager
-		if err := json.Unmarshal(data, &reqArgs); err != nil {
+		if err := sdk.JSONUnmarshal(data, &reqArgs); err != nil {
 			newError := sdk.NewError(sdk.ErrWrongRequest, err)
 			writeError(w, r, newError)
 			return
diff --git a/engine/worker/internal/handler_tmpl.go b/engine/worker/internal/handler_tmpl.go
index d19dcfcfa..4a1b6fc0e 100644
--- a/engine/worker/internal/handler_tmpl.go
+++ b/engine/worker/internal/handler_tmpl.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 	"os"
@@ -29,7 +28,7 @@ func tmplHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		}
 
 		var a workerruntime.TmplPath
-		if err := json.Unmarshal(data, &a); err != nil {
+		if err := sdk.JSONUnmarshal(data, &a); err != nil {
 			newError := sdk.NewError(sdk.ErrWrongRequest, err)
 			writeError(w, r, newError)
 			return
diff --git a/engine/worker/internal/handler_upload.go b/engine/worker/internal/handler_upload.go
index e95df05a5..5a90a4f0a 100644
--- a/engine/worker/internal/handler_upload.go
+++ b/engine/worker/internal/handler_upload.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -29,7 +28,7 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		}
 
 		var art workerruntime.UploadArtifact
-		if err := json.Unmarshal(data, &art); err != nil {
+		if err := sdk.JSONUnmarshal(data, &art); err != nil {
 			w.WriteHeader(http.StatusBadRequest)
 			return
 		}
diff --git a/engine/worker/internal/handler_vulnerability.go b/engine/worker/internal/handler_vulnerability.go
index fcf4bc5d4..1abfa9322 100644
--- a/engine/worker/internal/handler_vulnerability.go
+++ b/engine/worker/internal/handler_vulnerability.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"context"
-	"encoding/json"
 	"io/ioutil"
 	"net/http"
 
@@ -29,7 +28,7 @@ func vulnerabilityHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFu
 			return
 		}
 
-		if err := json.Unmarshal(data, &report); err != nil {
+		if err := sdk.JSONUnmarshal(data, &report); err != nil {
 			writeError(w, r, err)
 			return
 		}
diff --git a/engine/worker/internal/types.go b/engine/worker/internal/types.go
index 13308b8ed..92a2b8ea9 100644
--- a/engine/worker/internal/types.go
+++ b/engine/worker/internal/types.go
@@ -319,7 +319,7 @@ func (w *CurrentWorker) Blur(i interface{}) error {
 		}
 	}
 
-	if err := json.Unmarshal([]byte(dataS), i); err != nil {
+	if err := sdk.JSONUnmarshal([]byte(dataS), i); err != nil {
 		return err
 	}
 
diff --git a/go.mod b/go.mod
index ce9ae6a7d..9b4cb663b 100644
--- a/go.mod
+++ b/go.mod
@@ -101,7 +101,7 @@ require (
 	github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
 	github.com/prometheus/client_golang v1.1.0 // indirect
 	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
-	github.com/rockbears/log v0.3.0
+	github.com/rockbears/log v0.4.0
 	github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362
 	github.com/satori/go.uuid v1.2.0
 	github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197
diff --git a/go.sum b/go.sum
index 30adcf04c..bc8d8b602 100644
--- a/go.sum
+++ b/go.sum
@@ -527,8 +527,8 @@ github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rockbears/log v0.3.0 h1:V26Z4/UnlwM4sIUXT01rrY0gfvrPSc//U3a6KlPcwhk=
-github.com/rockbears/log v0.3.0/go.mod h1:fKa6erpK6U16kk8xOLGusFuVz4qr+mqe+9zD0kVn7VI=
+github.com/rockbears/log v0.4.0 h1:/zBVygXaaJJ+SQDdG4m5ptwLdkQqfTbl8afCbUF5Q8U=
+github.com/rockbears/log v0.4.0/go.mod h1:z46IOEOh914gJvg256Vm3F4s8K7D3ePaEAzuMYcfk98=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362 h1:lmOdpLt3XS6QyVoY6xNfOOTNWE2xtUBees+OAO+HFOg=
diff --git a/sdk/action.go b/sdk/action.go
index 2c51cf841..c066219e5 100644
--- a/sdk/action.go
+++ b/sdk/action.go
@@ -110,7 +110,7 @@ func (a *Action) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal Action")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal Action")
 }
 
 // IsValidDefault returns default action validity.
diff --git a/sdk/ascode.go b/sdk/ascode.go
index 9e4885575..3c213f84c 100644
--- a/sdk/ascode.go
+++ b/sdk/ascode.go
@@ -34,7 +34,7 @@ func (d *AsCodeEventData) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, d), "cannot unmarshal AsCodeEventData")
+	return WrapError(JSONUnmarshal(source, d), "cannot unmarshal AsCodeEventData")
 }
 
 // Value returns driver.Value from consumer data.
diff --git a/sdk/cdn.go b/sdk/cdn.go
index 024cc46b8..0f48e906e 100644
--- a/sdk/cdn.go
+++ b/sdk/cdn.go
@@ -53,26 +53,26 @@ func (c CDNItem) MarshalJSON() ([]byte, error) {
 func (c *CDNItem) UnmarshalJSON(data []byte) error {
 	type Alias CDNItem // prevent recursion
 	var itemAlias Alias
-	if err := json.Unmarshal(data, &itemAlias); err != nil {
+	if err := JSONUnmarshal(data, &itemAlias); err != nil {
 		return WithStack(err)
 	}
 
 	switch itemAlias.Type {
 	case CDNTypeItemStepLog, CDNTypeItemServiceLog:
 		var apiRef CDNLogAPIRef
-		if err := json.Unmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
+		if err := JSONUnmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
 			return WithStack(err)
 		}
 		itemAlias.APIRef = &apiRef
 	case CDNTypeItemRunResult:
 		var apiRef CDNRunResultAPIRef
-		if err := json.Unmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
+		if err := JSONUnmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
 			return WithStack(err)
 		}
 		itemAlias.APIRef = &apiRef
 	case CDNTypeItemWorkerCache:
 		var apiRef CDNWorkerCacheAPIRef
-		if err := json.Unmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
+		if err := JSONUnmarshal(itemAlias.APIRefRaw, &apiRef); err != nil {
 			return WithStack(err)
 		}
 		itemAlias.APIRef = &apiRef
@@ -331,7 +331,7 @@ func (a *CDNLogAPIRef) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal CDNLogAPIRef")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal CDNLogAPIRef")
 }
 
 type CDNItemType string
diff --git a/sdk/cdsclient/client_admin.go b/sdk/cdsclient/client_admin.go
index 6a63c0b16..657f1b8ba 100644
--- a/sdk/cdsclient/client_admin.go
+++ b/sdk/cdsclient/client_admin.go
@@ -109,7 +109,7 @@ func (c *client) ServiceGetJSON(ctx context.Context, stype, path string, out int
 	if err != nil {
 		return code, err
 	}
-	if err := json.Unmarshal(btes, out); err != nil {
+	if err := sdk.JSONUnmarshal(btes, out); err != nil {
 		return code, newError(err)
 	}
 	return code, nil
@@ -131,7 +131,7 @@ func (c *client) ServicePostJSON(ctx context.Context, stype, path string, in, ou
 	}
 
 	if len(btes) > 0 {
-		if err := json.Unmarshal(btes, out); err != nil {
+		if err := sdk.JSONUnmarshal(btes, out); err != nil {
 			return code, newError(err)
 		}
 	}
@@ -155,7 +155,7 @@ func (c *client) ServicePutJSON(ctx context.Context, stype, path string, in, out
 	}
 
 	if len(btes) > 0 {
-		if err := json.Unmarshal(btes, out); err != nil {
+		if err := sdk.JSONUnmarshal(btes, out); err != nil {
 			return code, newError(err)
 		}
 	}
@@ -168,7 +168,7 @@ func (c *client) ServiceDeleteJSON(ctx context.Context, stype, path string, out
 		return code, err
 	}
 
-	if err := json.Unmarshal(btes, out); err != nil {
+	if err := sdk.JSONUnmarshal(btes, out); err != nil {
 		return code, newError(err)
 	}
 	return code, nil
diff --git a/sdk/cdsclient/client_cdn.go b/sdk/cdsclient/client_cdn.go
index ac85ea78e..18f0c53fa 100644
--- a/sdk/cdsclient/client_cdn.go
+++ b/sdk/cdsclient/client_cdn.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"crypto/md5"
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -72,7 +71,7 @@ func (c *client) CDNItemStream(ctx context.Context, cdnAddr string, hash string,
 		if reader != nil {
 			body, _ := ioutil.ReadAll(reader)
 			var errSdk sdk.Error
-			if err := json.Unmarshal(body, &errSdk); err == nil && errSdk.Message != "" {
+			if err := sdk.JSONUnmarshal(body, &errSdk); err == nil && errSdk.Message != "" {
 				stringBody = errSdk.Error()
 			}
 			if stringBody == "" {
diff --git a/sdk/cdsclient/client_events.go b/sdk/cdsclient/client_events.go
index 4cfcc2793..41b269ea6 100644
--- a/sdk/cdsclient/client_events.go
+++ b/sdk/cdsclient/client_events.go
@@ -25,7 +25,7 @@ func (c *client) WebsocketEventsListen(ctx context.Context, goRoutines *sdk.GoRo
 				chanMsgToSend <- m
 			case m := <-chanMsgReceived:
 				var wsEvent sdk.WebsocketEvent
-				if err := json.Unmarshal(m, &wsEvent); err != nil {
+				if err := sdk.JSONUnmarshal(m, &wsEvent); err != nil {
 					chanErrorReceived <- newError(fmt.Errorf("unable to unmarshal message: %s: %v", string(m), err))
 					continue
 				}
diff --git a/sdk/cdsclient/client_import.go b/sdk/cdsclient/client_import.go
index 2a1c082b8..ec0ecbd0d 100644
--- a/sdk/cdsclient/client_import.go
+++ b/sdk/cdsclient/client_import.go
@@ -3,7 +3,6 @@ package cdsclient
 import (
 	"archive/tar"
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -20,7 +19,7 @@ func (c *client) PipelineImport(projectKey string, content io.Reader, mods ...Re
 	}
 
 	messages := []string{}
-	_ = json.Unmarshal(btes, &messages)
+	_ = sdk.JSONUnmarshal(btes, &messages)
 	return messages, nil
 }
 
@@ -33,7 +32,7 @@ func (c *client) ApplicationImport(projectKey string, content io.Reader, mods ..
 	}
 
 	messages := []string{}
-	_ = json.Unmarshal(btes, &messages)
+	_ = sdk.JSONUnmarshal(btes, &messages)
 	return messages, nil
 }
 
@@ -46,7 +45,7 @@ func (c *client) EnvironmentImport(projectKey string, content io.Reader, mods ..
 	}
 
 	messages := []string{}
-	_ = json.Unmarshal(btes, &messages)
+	_ = sdk.JSONUnmarshal(btes, &messages)
 	return messages, nil
 }
 
@@ -63,7 +62,7 @@ func (c *client) WorkerModelImport(content io.Reader, mods ...RequestModifier) (
 	}
 
 	var wm sdk.Model
-	if err := json.Unmarshal(btes, &wm); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &wm); err != nil {
 		return nil, err
 	}
 
@@ -75,7 +74,7 @@ func (c *client) WorkflowImport(projectKey string, content io.Reader, mods ...Re
 
 	btes, _, _, err := c.Request(context.Background(), "POST", url, content, mods...)
 	messages := []string{} // could contains msg even if there is a 400 returned
-	_ = json.Unmarshal(btes, &messages)
+	_ = sdk.JSONUnmarshal(btes, &messages)
 	return messages, err
 }
 
@@ -95,7 +94,7 @@ func (c *client) WorkflowPush(projectKey string, tarContent io.Reader, mods ...R
 	}
 
 	messages := []string{}
-	if err := json.Unmarshal(btes, &messages); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &messages); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/sdk/cdsclient/client_mon.go b/sdk/cdsclient/client_mon.go
index f64217f7d..22d979e4f 100644
--- a/sdk/cdsclient/client_mon.go
+++ b/sdk/cdsclient/client_mon.go
@@ -2,7 +2,6 @@ package cdsclient
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	"github.com/ovh/cds/sdk"
@@ -39,7 +38,7 @@ func (c *client) MonErrorsGet(requestID string) ([]sdk.Error, error) {
 	}
 
 	var errs []sdk.Error
-	if err := json.Unmarshal(res, &errs); err != nil {
+	if err := sdk.JSONUnmarshal(res, &errs); err != nil {
 		return nil, err
 	}
 
diff --git a/sdk/cdsclient/client_project.go b/sdk/cdsclient/client_project.go
index eed8d3cf2..422910705 100644
--- a/sdk/cdsclient/client_project.go
+++ b/sdk/cdsclient/client_project.go
@@ -2,7 +2,6 @@ package cdsclient
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/url"
@@ -73,7 +72,7 @@ func (c *client) ProjectGroupsImport(projectKey string, content io.Reader, mods
 		return proj, err
 	}
 
-	if err := json.Unmarshal(btes, &proj); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &proj); err != nil {
 		return proj, err
 	}
 
diff --git a/sdk/cdsclient/client_queue.go b/sdk/cdsclient/client_queue.go
index c5a58f5a8..ea1ae19d5 100644
--- a/sdk/cdsclient/client_queue.go
+++ b/sdk/cdsclient/client_queue.go
@@ -79,7 +79,7 @@ func (c *client) QueuePolling(ctx context.Context, goRoutines *sdk.GoRoutines, j
 			}
 			if wsEvent.Event.EventType == "sdk.EventRunWorkflowJob" && wsEvent.Event.Status == sdk.StatusWaiting {
 				var jobEvent sdk.EventRunWorkflowJob
-				if err := json.Unmarshal(wsEvent.Event.Payload, &jobEvent); err != nil {
+				if err := sdk.JSONUnmarshal(wsEvent.Event.Payload, &jobEvent); err != nil {
 					errs <- newError(fmt.Errorf("unable to unmarshal job %v: %v", wsEvent.Event.Payload, err))
 					continue
 				}
@@ -559,7 +559,7 @@ func (c *client) queueDirectStaticFilesUpload(projectKey, integrationName string
 			SetHeader("Content-Disposition", "attachment; filename=archive.tar"),
 			SetHeader("Content-Type", writer.FormDataContentType()))
 		if err == nil && code < 300 {
-			if err := json.Unmarshal(respBody, &staticFileResp); err != nil {
+			if err := sdk.JSONUnmarshal(respBody, &staticFileResp); err != nil {
 				return "", newError(fmt.Errorf("unable to unmarshal body: %v: %v", string(respBody), err))
 			}
 			fmt.Printf("Files uploaded with public URL: %s\n", staticFileResp.PublicURL)
diff --git a/sdk/cdsclient/client_template.go b/sdk/cdsclient/client_template.go
index 39889971d..5e4dcf1fa 100644
--- a/sdk/cdsclient/client_template.go
+++ b/sdk/cdsclient/client_template.go
@@ -104,7 +104,7 @@ func (c *client) TemplatePush(tarContent io.Reader) ([]string, *tar.Reader, erro
 	}
 
 	messages := []string{}
-	if err := json.Unmarshal(btes, &messages); err != nil {
+	if err := sdk.JSONUnmarshal(btes, &messages); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/sdk/cdsclient/http.go b/sdk/cdsclient/http.go
index 1d29ebc6f..495b2705a 100644
--- a/sdk/cdsclient/http.go
+++ b/sdk/cdsclient/http.go
@@ -117,7 +117,7 @@ func (c *client) RequestJSON(ctx context.Context, method, path string, in interf
 	}
 
 	if out != nil {
-		if err := json.Unmarshal(res, out); err != nil {
+		if err := sdk.JSONUnmarshal(res, out); err != nil {
 			return res, nil, code, newError(err)
 		}
 	}
diff --git a/sdk/common.go b/sdk/common.go
index c9759aab1..0eb61ab87 100644
--- a/sdk/common.go
+++ b/sdk/common.go
@@ -205,7 +205,7 @@ func (s *StringSlice) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, s), "cannot unmarshal StringSlice")
+	return WrapError(JSONUnmarshal(source, s), "cannot unmarshal StringSlice")
 }
 
 // Value returns driver.Value from string slice.
@@ -232,7 +232,7 @@ func (s *Int64Slice) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, s), "cannot unmarshal Int64Slice")
+	return WrapError(JSONUnmarshal(source, s), "cannot unmarshal Int64Slice")
 }
 
 // Value returns driver.Value from int64 slice.
@@ -309,3 +309,13 @@ func PathIsAbs(s string) bool {
 	}
 	return path.IsAbs(s)
 }
+
+func JSONUnmarshal(btes []byte, i interface{}) error {
+	d := json.NewDecoder(bytes.NewReader(btes))
+	d.UseNumber()
+	err := d.Decode(i)
+	if err != nil {
+		return WithStack(err)
+	}
+	return nil
+}
diff --git a/sdk/environment.go b/sdk/environment.go
index 85fe2f6eb..703ade420 100644
--- a/sdk/environment.go
+++ b/sdk/environment.go
@@ -1,7 +1,7 @@
 package sdk
 
 import (
-	json "encoding/json"
+	"encoding/json"
 	"time"
 )
 
diff --git a/sdk/error.go b/sdk/error.go
index 772c80e8c..51833cb38 100644
--- a/sdk/error.go
+++ b/sdk/error.go
@@ -2,12 +2,10 @@ package sdk
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"os"
-	"runtime"
 	"strings"
 
 	cdslog "github.com/ovh/cds/sdk/log"
@@ -464,8 +462,7 @@ func NewErrorWithStack(root error, err error) error {
 }
 
 type errorWithStack struct {
-	root      error  // root error should be wrapped with stack
-	stack     *stack // used to generate inline call stack
+	root      error // root error should be wrapped with stack
 	httpError Error
 }
 
@@ -473,9 +470,12 @@ func (w errorWithStack) Error() string {
 	var cause string
 	root := w.root.Error()
 	if root != "" && root != w.httpError.From && root != w.httpError.Error() {
-		cause = fmt.Sprintf(" (caused by: %s)", w.root)
+		cause = fmt.Sprintf("(caused by: %s)", w.root)
 	}
-	return fmt.Sprintf("%s: %s%s", w.stack.String(), w.httpError, cause)
+	if cause == "" {
+		return w.httpError.Error()
+	}
+	return fmt.Sprintf("%s %s", w.httpError, cause)
 }
 
 func (w errorWithStack) Format(s fmt.State, verb rune) {
@@ -513,50 +513,6 @@ func IsErrorWithStack(err error) bool {
 	return ok
 }
 
-type stack []uintptr
-
-func (s *stack) String() string {
-	var names []string
-	for _, pc := range *s {
-		name := runtime.FuncForPC(pc).Name()
-		if strings.HasPrefix(name, "github.com/ovh/cds/vendor") {
-			continue
-		}
-		if strings.HasPrefix(name, "github.com/ovh/cds") {
-			sp := strings.Split(name, "/")
-			sp = strings.Split(sp[len(sp)-1], ".")
-			var name string
-			// check if it's a struct or package func
-			if strings.HasPrefix(sp[1], "(") {
-				name = sp[2]
-			} else {
-				name = sp[1]
-			}
-			ignoredNames := StringSlice{"NewError", "NewErrorFrom", "WithStack", "WrapError", "Append", "NewErrorWithStack", "extractBodyErrorFromResponse", "Stream"}
-			if !ignoredNames.Contains(name) {
-				names = append(names, name)
-			}
-		}
-	}
-	reverse(names)
-	return strings.Join(names, ">")
-}
-
-func reverse(ss []string) {
-	last := len(ss) - 1
-	for i := 0; i < len(ss)/2; i++ {
-		ss[i], ss[last-i] = ss[last-i], ss[i]
-	}
-}
-
-func callers() *stack {
-	const depth = 32
-	var pcs [depth]uintptr
-	n := runtime.Callers(3, pcs[:])
-	var st stack = pcs[0:n]
-	return &st
-}
-
 // NewError returns a merge of given err with new http error.
 func NewError(httpError Error, err error) error {
 	// if the given error is nil do nothing
@@ -584,7 +540,6 @@ func NewError(httpError Error, err error) error {
 	// if it's a library error create a new error with stack
 	return errorWithStack{
 		root:      errors.WithStack(err),
-		stack:     callers(),
 		httpError: httpError,
 	}
 }
@@ -624,7 +579,6 @@ func WrapError(err error, format string, args ...interface{}) error {
 	if e, ok := err.(Error); ok {
 		return errorWithStack{
 			root:      errors.WithStack(fmt.Errorf(format, args...)),
-			stack:     callers(),
 			httpError: e,
 		}
 	}
@@ -641,7 +595,6 @@ func WrapError(err error, format string, args ...interface{}) error {
 
 	return errorWithStack{
 		root:      errors.Wrap(err, m),
-		stack:     callers(),
 		httpError: httpError,
 	}
 }
@@ -667,14 +620,12 @@ func WithStack(err error) error {
 	if e, ok := err.(Error); ok {
 		return errorWithStack{
 			root:      errors.New(e.Translate()),
-			stack:     callers(),
 			httpError: e,
 		}
 	}
 
 	return errorWithStack{
 		root:      errors.WithStack(err),
-		stack:     callers(),
 		httpError: ErrUnknownError,
 	}
 }
@@ -735,7 +686,7 @@ func Exit(format string, args ...interface{}) {
 func DecodeError(data []byte) error {
 	var e Error
 
-	if err := json.Unmarshal(data, &e); err != nil {
+	if err := JSONUnmarshal(data, &e); err != nil {
 		return nil
 	}
 
diff --git a/sdk/event/kafka_listener.go b/sdk/event/kafka_listener.go
index 4265d7923..071c7e803 100644
--- a/sdk/event/kafka_listener.go
+++ b/sdk/event/kafka_listener.go
@@ -2,7 +2,6 @@ package event
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -77,7 +76,7 @@ func (h *handler) Cleanup(sarama.ConsumerGroupSession) error {
 func (h *handler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
 	for message := range claim.Messages() {
 		var event sdk.Event
-		json.Unmarshal(message.Value, &event)
+		sdk.JSONUnmarshal(message.Value, &event)
 		if err := h.processEventFunc(event); err != nil {
 			h.errorLogFunc("Error on ProcessEventFunc:%s", err)
 		}
diff --git a/sdk/exportentities/common.go b/sdk/exportentities/common.go
index 42b989702..8d2b8e6c0 100644
--- a/sdk/exportentities/common.go
+++ b/sdk/exportentities/common.go
@@ -59,7 +59,7 @@ func GetFormatFromContentType(ct string) (Format, error) {
 func Unmarshal(btes []byte, f Format, i interface{}) error {
 	switch f {
 	case FormatJSON:
-		if err := json.Unmarshal(btes, i); err != nil {
+		if err := sdk.JSONUnmarshal(btes, i); err != nil {
 			return sdk.NewErrorWithStack(err, sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot unmarshal given data as json"))
 		}
 		return nil
@@ -77,7 +77,7 @@ func UnmarshalStrict(btes []byte, f Format, i interface{}) error {
 	var err error
 	switch f {
 	case FormatJSON:
-		err = json.Unmarshal(btes, i)
+		err = sdk.JSONUnmarshal(btes, i)
 	case FormatYAML:
 		err = yaml.UnmarshalStrict(btes, i)
 	default:
diff --git a/sdk/exportentities/step.go b/sdk/exportentities/step.go
index ca2897f49..ae0709e53 100644
--- a/sdk/exportentities/step.go
+++ b/sdk/exportentities/step.go
@@ -380,7 +380,7 @@ func (s Step) MarshalJSON() ([]byte, error) {
 	}
 
 	var m map[string]json.RawMessage
-	if err := json.Unmarshal(b, &m); err != nil {
+	if err := sdk.JSONUnmarshal(b, &m); err != nil {
 		return nil, err
 	}
 
@@ -404,13 +404,13 @@ func (s Step) MarshalJSON() ([]byte, error) {
 func (s *Step) UnmarshalJSON(data []byte) error {
 	type StepAlias Step // prevent recursion
 	var sa StepAlias
-	if err := json.Unmarshal(data, &sa); err != nil {
+	if err := sdk.JSONUnmarshal(data, &sa); err != nil {
 		return err
 	}
 	*s = Step(sa)
 
 	var m map[string]json.RawMessage
-	if err := json.Unmarshal(data, &m); err != nil {
+	if err := sdk.JSONUnmarshal(data, &m); err != nil {
 		return err
 	}
 
@@ -428,7 +428,7 @@ func (s *Step) UnmarshalJSON(data []byte) error {
 	for k, v := range m {
 		if _, ok := jsonFields[k]; !ok {
 			var sp StepParameters
-			if err := json.Unmarshal(v, &sp); err != nil {
+			if err := sdk.JSONUnmarshal(v, &sp); err != nil {
 				return err
 			}
 			if s.StepCustom == nil {
@@ -830,7 +830,7 @@ func stepToMap(i interface{}) (map[string]string, error) {
 		return nil, sdk.WithStack(err)
 	}
 	var m map[string]string
-	if err := json.Unmarshal(buf, &m); err != nil {
+	if err := sdk.JSONUnmarshal(buf, &m); err != nil {
 		return nil, sdk.WithStack(err)
 	}
 	return m, nil
diff --git a/sdk/hatchery/hatchery.go b/sdk/hatchery/hatchery.go
index 388cc3b53..fd23d64db 100644
--- a/sdk/hatchery/hatchery.go
+++ b/sdk/hatchery/hatchery.go
@@ -20,11 +20,31 @@ import (
 
 var (
 	// Client is a CDS Client
-	Client                 cdsclient.HTTPClient
-	defaultMaxProvisioning = 10
-	models                 []sdk.Model
+	Client                         cdsclient.HTTPClient
+	defaultMaxProvisioning         = 10
+	models                         []sdk.Model
+	MaxAttemptsNumberBeforeFailure = 5
+	CacheSpawnIDsTTL               = 10 * time.Second
+	CacheNbAttemptsIDsTTL          = 1 * time.Hour
 )
 
+type CacheNbAttemptsJobIDs struct {
+	cache *cache.Cache
+}
+
+func (c *CacheNbAttemptsJobIDs) Key(id int64) string {
+	return strconv.FormatInt(id, 10)
+}
+
+func (c *CacheNbAttemptsJobIDs) NewAttempt(id int64) int {
+	key := c.Key(id)
+	nbAttempt, err := c.cache.IncrementInt(key, 1)
+	if err != nil {
+		c.cache.SetDefault(key, 1)
+	}
+	return nbAttempt
+}
+
 // Create creates hatchery
 func Create(ctx context.Context, h Interface) error {
 	ctx = telemetry.ContextWithTag(ctx,
@@ -64,9 +84,13 @@ func Create(ctx context.Context, h Interface) error {
 	wjobs := make(chan sdk.WorkflowNodeJobRun, h.Configuration().Provision.MaxConcurrentProvisioning)
 	errs := make(chan error, 1)
 
-	// Create a cache with a default expiration time of 3 second, and which
-	// purges expired items every minute
-	spawnIDs := cache.New(10*time.Second, 60*time.Second)
+	// Create a cache to keep in memory the jobID processed in the last 10s.
+	cacheSpawnIDs := cache.New(CacheSpawnIDsTTL, 2*CacheSpawnIDsTTL)
+
+	// Create a cache to only process each jobID only a number of attenmpts before force to fail the job
+	cacheNbAttemptsIDs := &CacheNbAttemptsJobIDs{
+		cache: cache.New(CacheNbAttemptsIDsTTL, 2*CacheNbAttemptsIDsTTL),
+	}
 
 	h.GetGoRoutines().Run(ctx, "queuePolling", func(ctx context.Context) {
 		log.Debug(ctx, "starting queue polling")
@@ -179,14 +203,14 @@ func Create(ctx context.Context, h Interface) error {
 				}
 
 				//Check if the jobs is concerned by a pending worker creation
-				if _, exist := spawnIDs.Get(strconv.FormatInt(j.ID, 10)); exist {
+				if _, exist := cacheSpawnIDs.Get(strconv.FormatInt(j.ID, 10)); exist {
 					log.Debug(ctx, "job %d already spawned in previous routine", j.ID)
 					endTrace("already spawned")
 					continue
 				}
 
 				//Before doing anything, push in cache
-				spawnIDs.SetDefault(strconv.FormatInt(j.ID, 10), j.ID)
+				cacheSpawnIDs.SetDefault(strconv.FormatInt(j.ID, 10), j.ID)
 
 				//Check bookedBy current hatchery
 				if j.BookedBy.ID != 0 {
@@ -202,6 +226,26 @@ func Create(ctx context.Context, h Interface) error {
 					continue
 				}
 
+				//Check if we already try to start a worker for this job
+				nbAttempts := cacheNbAttemptsIDs.NewAttempt(j.ID)
+				if nbAttempts > MaxAttemptsNumberBeforeFailure {
+					if err := h.CDSClient().
+						QueueSendResult(ctx,
+							j.ID,
+							sdk.Result{
+								ID:         j.ID,
+								BuildID:    j.ID,
+								Status:     sdk.StatusFail,
+								RemoteTime: time.Now(),
+								Reason:     fmt.Sprintf("hatchery %q failed to start worker after %d attempts", h.Configuration().Name, MaxAttemptsNumberBeforeFailure),
+							}); err != nil {
+						log.ErrorWithStackTrace(ctx, err)
+					}
+					log.Info(ctx, "hatchery %q failed to start worker after %d attempts", h.Configuration().Name, MaxAttemptsNumberBeforeFailure)
+					endTrace("maximum attempts")
+					continue
+				}
+
 				workerRequest := workerStarterRequest{
 					ctx:               currentCtx,
 					cancel:            endTrace,
diff --git a/sdk/hatchery/mock_hatchery/interface_mock.go b/sdk/hatchery/mock_hatchery/interface_mock.go
new file mode 100644
index 000000000..44f9b15b7
--- /dev/null
+++ b/sdk/hatchery/mock_hatchery/interface_mock.go
@@ -0,0 +1,486 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: types.go
+
+// Package mock_hatchery is a generated GoMock package.
+package mock_hatchery
+
+import (
+	context "context"
+	rsa "crypto/rsa"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	service "github.com/ovh/cds/engine/service"
+	sdk "github.com/ovh/cds/sdk"
+	cdsclient "github.com/ovh/cds/sdk/cdsclient"
+	hatchery "github.com/ovh/cds/sdk/hatchery"
+	logrus "github.com/sirupsen/logrus"
+)
+
+// MockInterface is a mock of Interface interface.
+type MockInterface struct {
+	ctrl     *gomock.Controller
+	recorder *MockInterfaceMockRecorder
+}
+
+// MockInterfaceMockRecorder is the mock recorder for MockInterface.
+type MockInterfaceMockRecorder struct {
+	mock *MockInterface
+}
+
+// NewMockInterface creates a new mock instance.
+func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
+	mock := &MockInterface{ctrl: ctrl}
+	mock.recorder = &MockInterfaceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
+	return m.recorder
+}
+
+// CDSClient mocks base method.
+func (m *MockInterface) CDSClient() cdsclient.Interface {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CDSClient")
+	ret0, _ := ret[0].(cdsclient.Interface)
+	return ret0
+}
+
+// CDSClient indicates an expected call of CDSClient.
+func (mr *MockInterfaceMockRecorder) CDSClient() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CDSClient", reflect.TypeOf((*MockInterface)(nil).CDSClient))
+}
+
+// CanSpawn mocks base method.
+func (m *MockInterface) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CanSpawn", ctx, model, jobID, requirements)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// CanSpawn indicates an expected call of CanSpawn.
+func (mr *MockInterfaceMockRecorder) CanSpawn(ctx, model, jobID, requirements interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSpawn", reflect.TypeOf((*MockInterface)(nil).CanSpawn), ctx, model, jobID, requirements)
+}
+
+// Configuration mocks base method.
+func (m *MockInterface) Configuration() service.HatcheryCommonConfiguration {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Configuration")
+	ret0, _ := ret[0].(service.HatcheryCommonConfiguration)
+	return ret0
+}
+
+// Configuration indicates an expected call of Configuration.
+func (mr *MockInterfaceMockRecorder) Configuration() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configuration", reflect.TypeOf((*MockInterface)(nil).Configuration))
+}
+
+// GetGoRoutines mocks base method.
+func (m *MockInterface) GetGoRoutines() *sdk.GoRoutines {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetGoRoutines")
+	ret0, _ := ret[0].(*sdk.GoRoutines)
+	return ret0
+}
+
+// GetGoRoutines indicates an expected call of GetGoRoutines.
+func (mr *MockInterfaceMockRecorder) GetGoRoutines() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGoRoutines", reflect.TypeOf((*MockInterface)(nil).GetGoRoutines))
+}
+
+// GetLogger mocks base method.
+func (m *MockInterface) GetLogger() *logrus.Logger {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLogger")
+	ret0, _ := ret[0].(*logrus.Logger)
+	return ret0
+}
+
+// GetLogger indicates an expected call of GetLogger.
+func (mr *MockInterfaceMockRecorder) GetLogger() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockInterface)(nil).GetLogger))
+}
+
+// GetPrivateKey mocks base method.
+func (m *MockInterface) GetPrivateKey() *rsa.PrivateKey {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPrivateKey")
+	ret0, _ := ret[0].(*rsa.PrivateKey)
+	return ret0
+}
+
+// GetPrivateKey indicates an expected call of GetPrivateKey.
+func (mr *MockInterfaceMockRecorder) GetPrivateKey() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateKey", reflect.TypeOf((*MockInterface)(nil).GetPrivateKey))
+}
+
+// InitHatchery mocks base method.
+func (m *MockInterface) InitHatchery(ctx context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InitHatchery", ctx)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// InitHatchery indicates an expected call of InitHatchery.
+func (mr *MockInterfaceMockRecorder) InitHatchery(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitHatchery", reflect.TypeOf((*MockInterface)(nil).InitHatchery), ctx)
+}
+
+// Name mocks base method.
+func (m *MockInterface) Name() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Name")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockInterfaceMockRecorder) Name() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockInterface)(nil).Name))
+}
+
+// Serve mocks base method.
+func (m *MockInterface) Serve(ctx context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Serve", ctx)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Serve indicates an expected call of Serve.
+func (mr *MockInterfaceMockRecorder) Serve(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockInterface)(nil).Serve), ctx)
+}
+
+// Service mocks base method.
+func (m *MockInterface) Service() *sdk.Service {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Service")
+	ret0, _ := ret[0].(*sdk.Service)
+	return ret0
+}
+
+// Service indicates an expected call of Service.
+func (mr *MockInterfaceMockRecorder) Service() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Service", reflect.TypeOf((*MockInterface)(nil).Service))
+}
+
+// SpawnWorker mocks base method.
+func (m *MockInterface) SpawnWorker(ctx context.Context, spawnArgs hatchery.SpawnArguments) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SpawnWorker", ctx, spawnArgs)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// SpawnWorker indicates an expected call of SpawnWorker.
+func (mr *MockInterfaceMockRecorder) SpawnWorker(ctx, spawnArgs interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpawnWorker", reflect.TypeOf((*MockInterface)(nil).SpawnWorker), ctx, spawnArgs)
+}
+
+// Type mocks base method.
+func (m *MockInterface) Type() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Type")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Type indicates an expected call of Type.
+func (mr *MockInterfaceMockRecorder) Type() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockInterface)(nil).Type))
+}
+
+// WorkersStarted mocks base method.
+func (m *MockInterface) WorkersStarted(ctx context.Context) []string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WorkersStarted", ctx)
+	ret0, _ := ret[0].([]string)
+	return ret0
+}
+
+// WorkersStarted indicates an expected call of WorkersStarted.
+func (mr *MockInterfaceMockRecorder) WorkersStarted(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkersStarted", reflect.TypeOf((*MockInterface)(nil).WorkersStarted), ctx)
+}
+
+// MockInterfaceWithModels is a mock of InterfaceWithModels interface.
+type MockInterfaceWithModels struct {
+	ctrl     *gomock.Controller
+	recorder *MockInterfaceWithModelsMockRecorder
+}
+
+// MockInterfaceWithModelsMockRecorder is the mock recorder for MockInterfaceWithModels.
+type MockInterfaceWithModelsMockRecorder struct {
+	mock *MockInterfaceWithModels
+}
+
+// NewMockInterfaceWithModels creates a new mock instance.
+func NewMockInterfaceWithModels(ctrl *gomock.Controller) *MockInterfaceWithModels {
+	mock := &MockInterfaceWithModels{ctrl: ctrl}
+	mock.recorder = &MockInterfaceWithModelsMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInterfaceWithModels) EXPECT() *MockInterfaceWithModelsMockRecorder {
+	return m.recorder
+}
+
+// CDSClient mocks base method.
+func (m *MockInterfaceWithModels) CDSClient() cdsclient.Interface {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CDSClient")
+	ret0, _ := ret[0].(cdsclient.Interface)
+	return ret0
+}
+
+// CDSClient indicates an expected call of CDSClient.
+func (mr *MockInterfaceWithModelsMockRecorder) CDSClient() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CDSClient", reflect.TypeOf((*MockInterfaceWithModels)(nil).CDSClient))
+}
+
+// CanSpawn mocks base method.
+func (m *MockInterfaceWithModels) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CanSpawn", ctx, model, jobID, requirements)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// CanSpawn indicates an expected call of CanSpawn.
+func (mr *MockInterfaceWithModelsMockRecorder) CanSpawn(ctx, model, jobID, requirements interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSpawn", reflect.TypeOf((*MockInterfaceWithModels)(nil).CanSpawn), ctx, model, jobID, requirements)
+}
+
+// Configuration mocks base method.
+func (m *MockInterfaceWithModels) Configuration() service.HatcheryCommonConfiguration {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Configuration")
+	ret0, _ := ret[0].(service.HatcheryCommonConfiguration)
+	return ret0
+}
+
+// Configuration indicates an expected call of Configuration.
+func (mr *MockInterfaceWithModelsMockRecorder) Configuration() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configuration", reflect.TypeOf((*MockInterfaceWithModels)(nil).Configuration))
+}
+
+// GetGoRoutines mocks base method.
+func (m *MockInterfaceWithModels) GetGoRoutines() *sdk.GoRoutines {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetGoRoutines")
+	ret0, _ := ret[0].(*sdk.GoRoutines)
+	return ret0
+}
+
+// GetGoRoutines indicates an expected call of GetGoRoutines.
+func (mr *MockInterfaceWithModelsMockRecorder) GetGoRoutines() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGoRoutines", reflect.TypeOf((*MockInterfaceWithModels)(nil).GetGoRoutines))
+}
+
+// GetLogger mocks base method.
+func (m *MockInterfaceWithModels) GetLogger() *logrus.Logger {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLogger")
+	ret0, _ := ret[0].(*logrus.Logger)
+	return ret0
+}
+
+// GetLogger indicates an expected call of GetLogger.
+func (mr *MockInterfaceWithModelsMockRecorder) GetLogger() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockInterfaceWithModels)(nil).GetLogger))
+}
+
+// GetPrivateKey mocks base method.
+func (m *MockInterfaceWithModels) GetPrivateKey() *rsa.PrivateKey {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPrivateKey")
+	ret0, _ := ret[0].(*rsa.PrivateKey)
+	return ret0
+}
+
+// GetPrivateKey indicates an expected call of GetPrivateKey.
+func (mr *MockInterfaceWithModelsMockRecorder) GetPrivateKey() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateKey", reflect.TypeOf((*MockInterfaceWithModels)(nil).GetPrivateKey))
+}
+
+// InitHatchery mocks base method.
+func (m *MockInterfaceWithModels) InitHatchery(ctx context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "InitHatchery", ctx)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// InitHatchery indicates an expected call of InitHatchery.
+func (mr *MockInterfaceWithModelsMockRecorder) InitHatchery(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitHatchery", reflect.TypeOf((*MockInterfaceWithModels)(nil).InitHatchery), ctx)
+}
+
+// ModelType mocks base method.
+func (m *MockInterfaceWithModels) ModelType() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ModelType")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// ModelType indicates an expected call of ModelType.
+func (mr *MockInterfaceWithModelsMockRecorder) ModelType() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModelType", reflect.TypeOf((*MockInterfaceWithModels)(nil).ModelType))
+}
+
+// Name mocks base method.
+func (m *MockInterfaceWithModels) Name() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Name")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockInterfaceWithModelsMockRecorder) Name() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockInterfaceWithModels)(nil).Name))
+}
+
+// NeedRegistration mocks base method.
+func (m *MockInterfaceWithModels) NeedRegistration(ctx context.Context, model *sdk.Model) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NeedRegistration", ctx, model)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// NeedRegistration indicates an expected call of NeedRegistration.
+func (mr *MockInterfaceWithModelsMockRecorder) NeedRegistration(ctx, model interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NeedRegistration", reflect.TypeOf((*MockInterfaceWithModels)(nil).NeedRegistration), ctx, model)
+}
+
+// Serve mocks base method.
+func (m *MockInterfaceWithModels) Serve(ctx context.Context) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Serve", ctx)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Serve indicates an expected call of Serve.
+func (mr *MockInterfaceWithModelsMockRecorder) Serve(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockInterfaceWithModels)(nil).Serve), ctx)
+}
+
+// Service mocks base method.
+func (m *MockInterfaceWithModels) Service() *sdk.Service {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Service")
+	ret0, _ := ret[0].(*sdk.Service)
+	return ret0
+}
+
+// Service indicates an expected call of Service.
+func (mr *MockInterfaceWithModelsMockRecorder) Service() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Service", reflect.TypeOf((*MockInterfaceWithModels)(nil).Service))
+}
+
+// SpawnWorker mocks base method.
+func (m *MockInterfaceWithModels) SpawnWorker(ctx context.Context, spawnArgs hatchery.SpawnArguments) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SpawnWorker", ctx, spawnArgs)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// SpawnWorker indicates an expected call of SpawnWorker.
+func (mr *MockInterfaceWithModelsMockRecorder) SpawnWorker(ctx, spawnArgs interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpawnWorker", reflect.TypeOf((*MockInterfaceWithModels)(nil).SpawnWorker), ctx, spawnArgs)
+}
+
+// Type mocks base method.
+func (m *MockInterfaceWithModels) Type() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Type")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Type indicates an expected call of Type.
+func (mr *MockInterfaceWithModelsMockRecorder) Type() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockInterfaceWithModels)(nil).Type))
+}
+
+// WorkerModelSecretList mocks base method.
+func (m *MockInterfaceWithModels) WorkerModelSecretList(arg0 sdk.Model) (sdk.WorkerModelSecrets, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WorkerModelSecretList", arg0)
+	ret0, _ := ret[0].(sdk.WorkerModelSecrets)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WorkerModelSecretList indicates an expected call of WorkerModelSecretList.
+func (mr *MockInterfaceWithModelsMockRecorder) WorkerModelSecretList(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkerModelSecretList", reflect.TypeOf((*MockInterfaceWithModels)(nil).WorkerModelSecretList), arg0)
+}
+
+// WorkerModelsEnabled mocks base method.
+func (m *MockInterfaceWithModels) WorkerModelsEnabled() ([]sdk.Model, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WorkerModelsEnabled")
+	ret0, _ := ret[0].([]sdk.Model)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WorkerModelsEnabled indicates an expected call of WorkerModelsEnabled.
+func (mr *MockInterfaceWithModelsMockRecorder) WorkerModelsEnabled() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkerModelsEnabled", reflect.TypeOf((*MockInterfaceWithModels)(nil).WorkerModelsEnabled))
+}
+
+// WorkersStarted mocks base method.
+func (m *MockInterfaceWithModels) WorkersStarted(ctx context.Context) []string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WorkersStarted", ctx)
+	ret0, _ := ret[0].([]string)
+	return ret0
+}
+
+// WorkersStarted indicates an expected call of WorkersStarted.
+func (mr *MockInterfaceWithModelsMockRecorder) WorkersStarted(ctx interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkersStarted", reflect.TypeOf((*MockInterfaceWithModels)(nil).WorkersStarted), ctx)
+}
diff --git a/sdk/hatchery/starter.go b/sdk/hatchery/starter.go
index acc83ae0f..95710226b 100644
--- a/sdk/hatchery/starter.go
+++ b/sdk/hatchery/starter.go
@@ -210,16 +210,6 @@ func spawnWorkerForJob(ctx context.Context, h Interface, j workerStarterRequest)
 		return false
 	}
 
-	ctxSendSpawnInfo, next = telemetry.Span(ctxJob, "hatchery.SendSpawnInfo", telemetry.Tag("msg", sdk.MsgSpawnInfoHatcheryStartsSuccessfully.ID))
-	SendSpawnInfo(ctxSendSpawnInfo, h, j.id, sdk.SpawnMsg{
-		ID: sdk.MsgSpawnInfoHatcheryStartsSuccessfully.ID,
-		Args: []interface{}{
-			h.Service().Name,
-			arg.WorkerName,
-			sdk.Round(time.Since(start), time.Second).String()},
-	})
-	next()
-
 	if j.model != nil && j.model.IsDeprecated {
 		ctxSendSpawnInfo, next = telemetry.Span(ctxJob, "hatchery.SendSpawnInfo", telemetry.Tag("msg", sdk.MsgSpawnInfoDeprecatedModel.ID))
 		SendSpawnInfo(ctxSendSpawnInfo, h, j.id, sdk.SpawnMsg{
diff --git a/sdk/integration.go b/sdk/integration.go
index 13ab82bb7..612bc3a2e 100644
--- a/sdk/integration.go
+++ b/sdk/integration.go
@@ -256,7 +256,7 @@ func (config *IntegrationConfig) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, config), "cannot unmarshal IntegrationConfig")
+	return WrapError(JSONUnmarshal(source, config), "cannot unmarshal IntegrationConfig")
 }
 
 // EncryptSecrets encrypt secrets given a cypher func
@@ -355,7 +355,7 @@ func (config *IntegrationConfigMap) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, config), "cannot unmarshal IntegrationConfigMap")
+	return WrapError(JSONUnmarshal(source, config), "cannot unmarshal IntegrationConfigMap")
 }
 
 // IntegrationModel represent a integration model with its default configuration
diff --git a/sdk/jws/jws.go b/sdk/jws/jws.go
index 3ebbdd0d3..072a0942a 100644
--- a/sdk/jws/jws.go
+++ b/sdk/jws/jws.go
@@ -116,7 +116,7 @@ func Verify(key interface{}, s string, i interface{}) error {
 	if err != nil {
 		return sdk.WithStack(fmt.Errorf("unable to verify payload: %v signature: %v", string(object.UnsafePayloadWithoutVerification()), err))
 	}
-	return sdk.WithStack(json.Unmarshal(output, i))
+	return sdk.WithStack(sdk.JSONUnmarshal(output, i))
 }
 
 func UnsafeParse(s string, i interface{}) error {
@@ -125,5 +125,5 @@ func UnsafeParse(s string, i interface{}) error {
 		return sdk.WithStack(err)
 	}
 	output := object.UnsafePayloadWithoutVerification()
-	return sdk.WithStack(json.Unmarshal(output, i))
+	return sdk.WithStack(sdk.JSONUnmarshal(output, i))
 }
diff --git a/sdk/messages.go b/sdk/messages.go
index 7d1ae34b7..a30544317 100644
--- a/sdk/messages.go
+++ b/sdk/messages.go
@@ -60,14 +60,13 @@ var (
 	MsgWorkflowDetached                     = &Message{"MsgWorkflowDetached", trad{FR: "Le workflow %s est détaché du repository %s", EN: "The workflow %s is detached from repository %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryStarts              = &Message{"MsgSpawnInfoHatcheryStarts", trad{FR: "La Hatchery %s a démarré le lancement du worker avec le modèle %s", EN: "Hatchery %s starts spawn worker with model %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryErrorSpawn          = &Message{"MsgSpawnInfoHatcheryErrorSpawn", trad{FR: "Une erreur est survenue lorsque la Hatchery %s a démarré un worker avec le modèle %s après %s, err:%s", EN: "Error while Hatchery %s spawns worker with model %s after %s, err:%s"}, nil, RunInfoTypeError}
-	MsgSpawnInfoHatcheryStartsSuccessfully  = &Message{"MsgSpawnInfoHatcheryStartsSuccessfully", trad{FR: "La Hatchery %s a démarré le worker %s avec succès en %s", EN: "Hatchery %s spawns worker %s successfully in %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryStartDockerPull     = &Message{"MsgSpawnInfoHatcheryStartDockerPull", trad{FR: "La Hatchery %s a démarré le docker pull de l'image %s...", EN: "Hatchery %s starts docker pull %s..."}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryEndDockerPull       = &Message{"MsgSpawnInfoHatcheryEndDockerPull", trad{FR: "La Hatchery %s a terminé le docker pull de l'image %s", EN: "Hatchery %s docker pull %s done"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryEndDockerPullErr    = &Message{"MsgSpawnInfoHatcheryEndDockerPullErr", trad{FR: "⚠ La Hatchery %s a terminé le docker pull de l'image %s en erreur: %s", EN: "⚠ Hatchery %s - docker pull %s done with error: %v"}, nil, RunInfoTypeError}
 	MsgSpawnInfoDeprecatedModel             = &Message{"MsgSpawnInfoDeprecatedModel", trad{FR: "⚠ Attention vous utilisez un worker model (%s) déprécié", EN: "⚠ Pay attention you are using a deprecated worker model (%s)"}, nil, RunInfoTypeWarning}
-	MsgSpawnInfoWorkerEnd                   = &Message{"MsgSpawnInfoWorkerEnd", trad{FR: "✓ Le worker %s a terminé et a passé %s à travailler sur les étapes", EN: "✓ Worker %s finished working on this job and took %s to work on the steps"}, nil, RunInfoTypInfo}
+	MsgSpawnInfoWorkerEnd                   = &Message{"MsgSpawnInfoWorkerEnd", trad{FR: "✓ Le worker %s a terminé et a passé %s à travailler sur les étapes", EN: "✓ Worker %s finished working on this job"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoJobInQueue                  = &Message{"MsgSpawnInfoJobInQueue", trad{FR: "✓ Le job a été mis en file d'attente", EN: "✓ Job has been queued"}, nil, RunInfoTypInfo}
-	MsgSpawnInfoJobTaken                    = &Message{"MsgSpawnInfoJobTaken", trad{FR: "Le job %s a été pris par le worker %s", EN: "Job %s was taken by worker %s"}, nil, RunInfoTypInfo}
+	MsgSpawnInfoJobTaken                    = &Message{"MsgSpawnInfoJobTaken", trad{FR: "Le job %s a été pris par le worker %s", EN: "Job %s has been taken by worker %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoJobTakenWorkerVersion       = &Message{"MsgSpawnInfoJobTakenWorkerVersion", trad{FR: "Worker %s version:%s os:%s arch:%s", EN: "Worker %s version:%s os:%s arch:%s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoWorkerForJob                = &Message{"MsgSpawnInfoWorkerForJob", trad{FR: "Ce worker %s a été créé pour lancer ce job", EN: "This worker %s was created to take this action"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoWorkerForJobError           = &Message{"MsgSpawnInfoWorkerForJobError", trad{FR: "⚠ Ce worker %s a été créé pour lancer ce job, mais ne possède pas tous les pré-requis. Vérifiez que les prérequis suivants:%s", EN: "⚠ This worker %s was created to take this action, but does not have all prerequisites. Please verify the following prerequisites:%s"}, nil, RunInfoTypeError}
@@ -94,6 +93,7 @@ var (
 	MsgWorkflowDeprecatedVersion            = &Message{"MsgWorkflowDeprecatedVersion", trad{FR: "La configuration yaml de votre workflow est dans un format déprécié. Exportez le avec la CLI `cdsctl workflow export %s %s`", EN: "The yaml workflow configuration format is deprecated. Export your workflow with CLI `cdsctl workflow export %s %s`"}, nil, RunInfoTypeWarning}
 	MsgWorkflowGeneratedFromTemplateVersion = &Message{"MsgWorkflowGeneratedFromTemplateVersion", trad{FR: "Le workflow a été généré à partir du modèle de workflow: %s.", EN: "The workflow was generated from the template: %s"}, nil, RunInfoTypInfo}
 	MsgTooMuchWorkflowRun                   = &Message{"MsgTooMuchWorkflowRun", trad{FR: "L'exécution de ce workflow est suspendu. Vous dépassez le nombre maximum d'éxécution autorisé (%.f). Merci de revoir la politique de retention de ce workflow", EN: "Workflow run is delayed. The maximum number of runs for this workflow has been reached ( %.f ). Please update your workflow retention policy"}, nil, RunInfoTypeWarning}
+	MsgSpawnErrorHatcheryRetryAttempt       = &Message{"MsgSpawnErrorHatcheryRetryAttempt", trad{EN: "Job execution failed by hatchery %s. Reason: %s"}, nil, RunInfoTypeError}
 )
 
 // Messages contains all sdk Messages
@@ -138,7 +138,6 @@ var Messages = map[string]*Message{
 	MsgWorkflowDetached.ID:                     MsgWorkflowDetached,
 	MsgSpawnInfoHatcheryStarts.ID:              MsgSpawnInfoHatcheryStarts,
 	MsgSpawnInfoHatcheryErrorSpawn.ID:          MsgSpawnInfoHatcheryErrorSpawn,
-	MsgSpawnInfoHatcheryStartsSuccessfully.ID:  MsgSpawnInfoHatcheryStartsSuccessfully,
 	MsgSpawnInfoHatcheryStartDockerPull.ID:     MsgSpawnInfoHatcheryStartDockerPull,
 	MsgSpawnInfoHatcheryEndDockerPull.ID:       MsgSpawnInfoHatcheryEndDockerPull,
 	MsgSpawnInfoHatcheryEndDockerPullErr.ID:    MsgSpawnInfoHatcheryEndDockerPullErr,
@@ -172,6 +171,7 @@ var Messages = map[string]*Message{
 	MsgWorkflowDeprecatedVersion.ID:            MsgWorkflowDeprecatedVersion,
 	MsgWorkflowGeneratedFromTemplateVersion.ID: MsgWorkflowGeneratedFromTemplateVersion,
 	MsgTooMuchWorkflowRun.ID:                   MsgTooMuchWorkflowRun,
+	MsgSpawnErrorHatcheryRetryAttempt.ID:       MsgSpawnErrorHatcheryRetryAttempt,
 }
 
 //Message represent a struc format translated messages
diff --git a/sdk/notif.go b/sdk/notif.go
index 03ceeb33c..6d466a04e 100644
--- a/sdk/notif.go
+++ b/sdk/notif.go
@@ -64,7 +64,7 @@ func (a *UserNotificationSettings) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal UserNotificationSettings")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal UserNotificationSettings")
 }
 
 // UserNotificationTemplate is the notification content
diff --git a/sdk/project.go b/sdk/project.go
index b6c0d5e97..a25e428f5 100644
--- a/sdk/project.go
+++ b/sdk/project.go
@@ -279,7 +279,7 @@ func (a *Metadata) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal Metadata")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal Metadata")
 }
 
 //LastModification is stored in cache and used for ProjectLastUpdates computing
diff --git a/sdk/services.go b/sdk/services.go
index bbe336fe9..edce62e4d 100644
--- a/sdk/services.go
+++ b/sdk/services.go
@@ -75,7 +75,7 @@ func (c *ServiceConfig) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, c), "cannot unmarshal ServiceConfig")
+	return WrapError(JSONUnmarshal(source, c), "cannot unmarshal ServiceConfig")
 }
 
 // ServiceConfiguration is the configuration of service
diff --git a/sdk/stage.go b/sdk/stage.go
index a5b70ec07..2ec4b23e6 100644
--- a/sdk/stage.go
+++ b/sdk/stage.go
@@ -1,7 +1,6 @@
 package sdk
 
 import (
-	json "encoding/json"
 	"strings"
 	"time"
 )
@@ -37,7 +36,7 @@ func (s *Stage) UnmarshalJSON(data []byte) error {
 		Warnings      []PipelineBuildWarning `json:"warnings"`
 	}
 
-	if err := json.Unmarshal(data, &tmp); err != nil {
+	if err := JSONUnmarshal(data, &tmp); err != nil {
 		return err
 	}
 	s.ID = tmp.ID
@@ -53,7 +52,7 @@ func (s *Stage) UnmarshalJSON(data []byte) error {
 	s.Warnings = tmp.Warnings
 
 	var v map[string]interface{}
-	if err := json.Unmarshal(data, &v); err != nil {
+	if err := JSONUnmarshal(data, &v); err != nil {
 		return err
 	}
 	if lastModifiedNumber, ok := v["last_modified"].(float64); ok {
diff --git a/sdk/status.go b/sdk/status.go
index 770e6c825..1940cc241 100644
--- a/sdk/status.go
+++ b/sdk/status.go
@@ -40,7 +40,7 @@ func (m *MonitoringStatus) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, m), "cannot unmarshal MonitoringStatus")
+	return WrapError(JSONUnmarshal(source, m), "cannot unmarshal MonitoringStatus")
 }
 
 // AddLine adds line to MonitoringStatus, including the Type of component
diff --git a/sdk/token.go b/sdk/token.go
index 7fe7a9cdf..17ec8e0e4 100644
--- a/sdk/token.go
+++ b/sdk/token.go
@@ -203,7 +203,7 @@ func (s *AuthConsumerScopeDetails) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, s), "cannot unmarshal AuthConsumerScopeDetails")
+	return WrapError(JSONUnmarshal(source, s), "cannot unmarshal AuthConsumerScopeDetails")
 }
 
 // Value returns driver.Value from scope detail slice.
@@ -221,7 +221,7 @@ func (s *AuthConsumerScopeSlice) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, s), "cannot unmarshal AuthConsumerScopeSlice")
+	return WrapError(JSONUnmarshal(source, s), "cannot unmarshal AuthConsumerScopeSlice")
 }
 
 // Value returns driver.Value from scope slice.
@@ -314,7 +314,7 @@ func (d *AuthConsumerData) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, d), "cannot unmarshal AuthConsumerData")
+	return WrapError(JSONUnmarshal(source, d), "cannot unmarshal AuthConsumerData")
 }
 
 // Value returns driver.Value from consumer data.
@@ -372,7 +372,7 @@ func (w *AuthConsumerWarnings) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(errors.New("type assertion .([]byte) failed"))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal AuthConsumerWarnings")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal AuthConsumerWarnings")
 }
 
 // Value returns driver.Value from consumer warnings.
@@ -434,7 +434,7 @@ func (p *AuthConsumerValidityPeriods) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, p), "cannot unmarshal AuthConsumerValidityPeriods")
+	return WrapError(JSONUnmarshal(source, p), "cannot unmarshal AuthConsumerValidityPeriods")
 }
 
 func (p AuthConsumerValidityPeriods) Latest() *AuthConsumerValidityPeriod {
diff --git a/sdk/user.go b/sdk/user.go
index 7c6df3645..3a6d3ee50 100644
--- a/sdk/user.go
+++ b/sdk/user.go
@@ -79,7 +79,7 @@ func (u *AuthentifiedUser) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, u), "cannot unmarshal AuthentifiedUser")
+	return WrapError(JSONUnmarshal(source, u), "cannot unmarshal AuthentifiedUser")
 }
 
 // GetGroupIDs returns groups ids for user
diff --git a/sdk/worker_model.go b/sdk/worker_model.go
index 541c3a931..4534bdfea 100644
--- a/sdk/worker_model.go
+++ b/sdk/worker_model.go
@@ -109,7 +109,7 @@ func (a *Author) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal Author")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal Author")
 }
 
 // Update workflow template field from new data.
@@ -219,7 +219,7 @@ func (m *ModelVirtualMachine) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, m), "cannot unmarshal ModelVirtualMachine")
+	return WrapError(JSONUnmarshal(source, m), "cannot unmarshal ModelVirtualMachine")
 }
 
 // ModelDocker for swarm, marathon and kubernetes.
@@ -250,7 +250,7 @@ func (m *ModelDocker) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, m), "cannot unmarshal ModelDocker")
+	return WrapError(JSONUnmarshal(source, m), "cannot unmarshal ModelDocker")
 }
 
 // ModelPattern represent patterns for users and admin when creating a worker model
@@ -285,7 +285,7 @@ func (m *ModelCmds) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, m), "cannot unmarshal ModelCmds")
+	return WrapError(JSONUnmarshal(source, m), "cannot unmarshal ModelCmds")
 }
 
 // ModelsToGroupIDs returns group ids of given worker models.
diff --git a/sdk/workflow.go b/sdk/workflow.go
index 858781c3c..fb00bd415 100644
--- a/sdk/workflow.go
+++ b/sdk/workflow.go
@@ -82,7 +82,7 @@ func (w *Workflow) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal Workflow")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal Workflow")
 }
 
 type PurgeTags []string
@@ -102,7 +102,7 @@ func (a *PurgeTags) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal PurgeTags")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal PurgeTags")
 }
 
 // Value returns driver.Value from WorkflowData.
@@ -120,7 +120,7 @@ func (a *WorkflowData) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal WorkflowData")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal WorkflowData")
 }
 
 type Workflows []Workflow
@@ -402,7 +402,7 @@ func (w *WorkflowNodeConditions) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowNodeConditions")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowNodeConditions")
 }
 
 //WorkflowNodeCondition represents a condition to trigger ot not a pipeline in a workflow. Operator can be =, !=, regex
diff --git a/sdk/workflow_hook.go b/sdk/workflow_hook.go
index c0dded13d..6c57cf435 100644
--- a/sdk/workflow_hook.go
+++ b/sdk/workflow_hook.go
@@ -172,7 +172,7 @@ func (w *WorkflowNodeHookConfig) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowNodeHookConfig")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowNodeHookConfig")
 }
 
 func (w WorkflowNodeHookConfig) Equals(o WorkflowNodeHookConfig) bool {
diff --git a/sdk/workflow_run.go b/sdk/workflow_run.go
index 5d4c01876..6aebf2ad2 100644
--- a/sdk/workflow_run.go
+++ b/sdk/workflow_run.go
@@ -50,7 +50,7 @@ func (h *WorkflowRunHeaders) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, h), "cannot unmarshal WorkflowRunHeaders")
+	return WrapError(JSONUnmarshal(source, h), "cannot unmarshal WorkflowRunHeaders")
 }
 
 //WorkflowRun is an execution instance of a run
@@ -106,7 +106,7 @@ func (a *WorkflowRunInfos) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal WorkflowRunInfos")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal WorkflowRunInfos")
 }
 
 type WorkflowNodeTriggerRuns map[int64]WorkflowNodeTriggerRun
@@ -124,7 +124,7 @@ func (a *WorkflowNodeTriggerRuns) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal WorkflowNodeTriggerRuns")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal WorkflowNodeTriggerRuns")
 }
 
 type WorkflowRunSecret struct {
@@ -168,7 +168,7 @@ func (a *WorkflowRunPostHandlerOption) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal WorkflowRunPostHandlerOption")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal WorkflowRunPostHandlerOption")
 }
 
 //WorkflowRunNumber contains a workflow run number
@@ -364,6 +364,19 @@ type WorkflowNodeRun struct {
 	VCSReport              string                               `json:"vcs_report,omitempty"`
 }
 
+func (nodeRun *WorkflowNodeRun) GetStageIndex(job *WorkflowNodeJobRun) int {
+	var stageIndex = -1
+	for i := range nodeRun.Stages {
+		s := &nodeRun.Stages[i]
+		for _, j := range s.Jobs {
+			if j.Action.ID == job.Job.Job.Action.ID {
+				stageIndex = i
+			}
+		}
+	}
+	return stageIndex
+}
+
 // WorkflowNodeOutgoingHookRunCallback is the callback coming from hooks uservice avec an outgoing hook execution
 type WorkflowNodeOutgoingHookRunCallback struct {
 	NodeHookID        int64     `json:"workflow_node_outgoing_hook_id"`
diff --git a/sdk/workflow_run_result.go b/sdk/workflow_run_result.go
index 86d5d98b3..f7ef4d1bd 100644
--- a/sdk/workflow_run_result.go
+++ b/sdk/workflow_run_result.go
@@ -27,7 +27,7 @@ type WorkflowRunResult struct {
 
 func (r *WorkflowRunResult) GetArtifact() (WorkflowRunResultArtifact, error) {
 	var data WorkflowRunResultArtifact
-	if err := json.Unmarshal(r.DataRaw, &data); err != nil {
+	if err := JSONUnmarshal(r.DataRaw, &data); err != nil {
 		return data, WithStack(err)
 	}
 	return data, nil
@@ -35,7 +35,7 @@ func (r *WorkflowRunResult) GetArtifact() (WorkflowRunResultArtifact, error) {
 
 func (r *WorkflowRunResult) GetCoverage() (WorkflowRunResultCoverage, error) {
 	var data WorkflowRunResultCoverage
-	if err := json.Unmarshal(r.DataRaw, &data); err != nil {
+	if err := JSONUnmarshal(r.DataRaw, &data); err != nil {
 		return data, WithStack(err)
 	}
 	return data, nil
@@ -43,7 +43,7 @@ func (r *WorkflowRunResult) GetCoverage() (WorkflowRunResultCoverage, error) {
 
 func (r *WorkflowRunResult) GetArtifactManager() (WorkflowRunResultArtifactManager, error) {
 	var data WorkflowRunResultArtifactManager
-	if err := json.Unmarshal(r.DataRaw, &data); err != nil {
+	if err := JSONUnmarshal(r.DataRaw, &data); err != nil {
 		return data, WithStack(err)
 	}
 	return data, nil
diff --git a/sdk/workflow_template.go b/sdk/workflow_template.go
index d8d074f85..271f69531 100644
--- a/sdk/workflow_template.go
+++ b/sdk/workflow_template.go
@@ -33,7 +33,7 @@ func (w *WorkflowTemplateRequest) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowTemplateRequest")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowTemplateRequest")
 }
 
 // WorkflowTemplateParsed struct.
@@ -81,7 +81,7 @@ func (w *WorkflowTemplate) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowTemplate")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowTemplate")
 }
 
 // IsValid returns workflow template validity.
@@ -165,7 +165,7 @@ func (w *WorkflowTemplate) CheckParams(r WorkflowTemplateRequest) error {
 			case ParameterTypeJSON:
 				if v != "" {
 					var res interface{}
-					if err := json.Unmarshal([]byte(v), &res); err != nil {
+					if err := JSONUnmarshal([]byte(v), &res); err != nil {
 						return NewErrorFrom(ErrInvalidData, "Given value it's not json for %s", p.Key)
 					}
 				}
@@ -306,7 +306,7 @@ func (w *WorkflowTemplateParameters) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowTemplateParameters")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowTemplateParameters")
 }
 
 // PipelineTemplates struct.
@@ -327,7 +327,7 @@ func (p *PipelineTemplates) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, p), "cannot unmarshal PipelineTemplates")
+	return WrapError(JSONUnmarshal(source, p), "cannot unmarshal PipelineTemplates")
 }
 
 // ApplicationTemplates struct.
@@ -348,7 +348,7 @@ func (a *ApplicationTemplates) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, a), "cannot unmarshal ApplicationTemplates")
+	return WrapError(JSONUnmarshal(source, a), "cannot unmarshal ApplicationTemplates")
 }
 
 // EnvironmentTemplates struct.
@@ -369,7 +369,7 @@ func (e *EnvironmentTemplates) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, e), "cannot unmarshal EnvironmentTemplates")
+	return WrapError(JSONUnmarshal(source, e), "cannot unmarshal EnvironmentTemplates")
 }
 
 // IsValid returns pipeline template validity.
@@ -469,7 +469,7 @@ func (w *WorkflowTemplateBulkOperations) Scan(src interface{}) error {
 	if !ok {
 		return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src))
 	}
-	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowTemplateBulkOperations")
+	return WrapError(JSONUnmarshal(source, w), "cannot unmarshal WorkflowTemplateBulkOperations")
 }
 
 // WorkflowTemplateError contains info about template parsing error.


Test patch:
diff --git a/engine/api/group/dao_user_link_test.go b/engine/api/group/dao_user_link_test.go
index db224c6e6..78bcafc08 100644
--- a/engine/api/group/dao_user_link_test.go
+++ b/engine/api/group/dao_user_link_test.go
@@ -50,7 +50,7 @@ func TestDAO_LinkGroupUser(t *testing.T) {
 	assert.Len(t, grp.Members, 2)
 
 	err = group.DeleteUserFromGroup(context.TODO(), db, grp.ID, u1.ID)
-	require.EqualError(t, err, "TestDAO_LinkGroupUser>DeleteUserFromGroup: not enough group admin left")
+	require.EqualError(t, err, "not enough group admin left")
 
 	grp, err = group.LoadByName(context.TODO(), db, groupName, group.LoadOptions.WithMembers)
 	require.NoError(t, err)
diff --git a/engine/hatchery/hatchery_helper_test.go b/engine/hatchery/hatchery_helper_test.go
index 2ff0a6bcf..22317ba6d 100644
--- a/engine/hatchery/hatchery_helper_test.go
+++ b/engine/hatchery/hatchery_helper_test.go
@@ -141,7 +141,7 @@ func InitMock(t *testing.T, url string) {
 			},
 		})
 
-	gock.New(url).Post("/queue/workflows/1/spawn/infos").Times(2).Reply(200)
+	gock.New(url).Post("/queue/workflows/1/spawn/infos").Times(1).Reply(200)
 
 	gock.New(url).Post("/queue/workflows/1/book").
 		Reply(204)
diff --git a/engine/hatchery/marathon/marathon_test.go b/engine/hatchery/marathon/marathon_test.go
index 10e56fd51..d783e9f6d 100644
--- a/engine/hatchery/marathon/marathon_test.go
+++ b/engine/hatchery/marathon/marathon_test.go
@@ -321,7 +321,7 @@ func TestSpawnWorkerTimeout(t *testing.T) {
 	})
 	t.Logf("%+v\n", err)
 	assert.Error(t, err)
-	assert.Equal(t, "spawnMarathonDockerWorker> TestSpawnWorkerTimeout>SpawnWorker: internal server error (caused by: deployment for aaa timeout)", err.Error())
+	assert.Equal(t, "internal server error (caused by: deployment for aaa timeout)", err.Error())
 }
 
 func TestCanSpawn(t *testing.T) {
diff --git a/engine/hatchery/openstack/spawn_test.go b/engine/hatchery/openstack/spawn_test.go
index bf4a3d498..446910ace 100644
--- a/engine/hatchery/openstack/spawn_test.go
+++ b/engine/hatchery/openstack/spawn_test.go
@@ -159,5 +159,5 @@ func TestHatcheryOpenstack_checkSpawnLimits_CountSmallerFlavorToKeep(t *testing.
 
 	err = h.checkSpawnLimits(context.TODO(), m1)
 	require.Error(t, err, "0 CPUs left to start new flavor")
-	assert.Contains(t, err.Error(), "CountSmallerFlavorToKeep")
+	assert.Contains(t, err.Error(), "MaxCPUs limit")
 }
diff --git a/engine/test/config/load.go b/engine/test/config/load.go
index ee81f47ca..71e725c1d 100644
--- a/engine/test/config/load.go
+++ b/engine/test/config/load.go
@@ -1,12 +1,12 @@
 package config
 
 import (
-	"encoding/json"
 	"io/ioutil"
 	"os"
 	"os/user"
 	"path"
 
+	"github.com/ovh/cds/sdk"
 	"github.com/stretchr/testify/require"
 )
 
@@ -25,7 +25,7 @@ func LoadTestingConf(t require.TestingT, serviceType string) map[string]string {
 	require.NoError(t, err, "error reading test configuration file from %s", f)
 	if len(btes) != 0 {
 		cfg := map[string]string{}
-		require.NoError(t, json.Unmarshal(btes, &cfg), "error to unmarshal test configuration file from %s", f)
+		require.NoError(t, sdk.JSONUnmarshal(btes, &cfg), "error to unmarshal test configuration file from %s", f)
 		return cfg
 	}
 
diff --git a/sdk/environment_test.go b/sdk/environment_test.go
index bb7de499e..411ebd991 100644
--- a/sdk/environment_test.go
+++ b/sdk/environment_test.go
@@ -21,12 +21,12 @@ func TestEnvironmentUnmarshal(t *testing.T) {
 	data2 := []byte(fmt.Sprintf("{\"name\":\"two\",\"last_modified\":%d}", now.Unix()))
 
 	var one sdk.Environment
-	require.NoError(t, json.Unmarshal(data1, &one))
+	require.NoError(t, sdk.JSONUnmarshal(data1, &one))
 	assert.Equal(t, "one", one.Name)
 	assert.True(t, now.Equal(one.LastModified))
 
 	var two sdk.Environment
-	require.NoError(t, json.Unmarshal(data2, &two))
+	require.NoError(t, sdk.JSONUnmarshal(data2, &two))
 	assert.Equal(t, "two", two.Name)
 	assert.True(t, now.Equal(two.LastModified))
 }
diff --git a/sdk/error_test.go b/sdk/error_test.go
index 03adfa16c..84ff963aa 100644
--- a/sdk/error_test.go
+++ b/sdk/error_test.go
@@ -62,7 +62,7 @@ func TestErrorIs(t *testing.T) {
 
 func TestNewError(t *testing.T) {
 	err := NewError(ErrWrongRequest, fmt.Errorf("this is an error generated from vendor"))
-	assert.Equal(t, "TestNewError: wrong request (from: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", err.Error())
 
 	// print the error call stack
 	t.Log(err)
@@ -79,10 +79,10 @@ func TestNewError(t *testing.T) {
 
 func TestWrapError(t *testing.T) {
 	err := fourForStackTest()
-	assert.Equal(t, "TestWrapError>fourForStackTest>fiveForStackTest: internal server error (caused by: four: five: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "internal server error (caused by: four: five: this is an error generated from vendor)", err.Error())
 
 	err = oneForStackTest()
-	assert.Equal(t, "TestWrapError>oneForStackTest>twoForStackTest>threeForStackTest>fourForStackTest>fiveForStackTest: action definition contains a recursive loop (caused by: one: two: three: four: five: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "action definition contains a recursive loop (caused by: one: two: three: four: five: this is an error generated from vendor)", err.Error())
 
 	// print the error call stack
 	t.Log(err)
@@ -117,22 +117,22 @@ func TestCause(t *testing.T) {
 
 func TestNewAdvancedError(t *testing.T) {
 	err := NewErrorFrom(ErrWrongRequest, "this is an error generated from vendor")
-	assert.Equal(t, "TestNewAdvancedError: wrong request (from: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", err.Error())
 	httpErr := ExtractHTTPError(err)
 	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = WrapError(err, "Something no visible for http error")
-	assert.Equal(t, "TestNewAdvancedError: wrong request (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
 	httpErr = ExtractHTTPError(err)
 	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = NewError(ErrAlreadyTaken, err)
-	assert.Equal(t, "TestNewAdvancedError: This job is already taken by another worker (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "This job is already taken by another worker (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
 	httpErr = ExtractHTTPError(err)
 	assert.Equal(t, "This job is already taken by another worker (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = NewErrorWithStack(err, NewErrorFrom(ErrNotFound, "can't found this"))
-	assert.Equal(t, "TestNewAdvancedError: resource not found (from: can't found this) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
+	assert.Equal(t, "resource not found (from: can't found this) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
 	httpErr = ExtractHTTPError(err)
 	assert.Equal(t, "resource not found (from: can't found this)", httpErr.Error())
 
@@ -167,22 +167,22 @@ func TestMultiError(t *testing.T) {
 	mError.Append(errThree)
 	mError.Append(errFourth)
 
-	assert.Equal(t, "TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error)", mError.Error())
+	assert.Equal(t, "internal server error (caused by: my first error), wrong request (from: my second error), internal server error (caused by: hidden info: my third error), already exists (from: my fourth error)", mError.Error())
 	httpErr := ExtractHTTPError(mError)
 	assert.Equal(t, "internal server error, wrong request: my second error, internal server error, already exists: my fourth error", httpErr.Error())
 
 	wrappedErr := NewError(ErrWrongRequest, mError)
-	assert.Equal(t, "TestMultiError: wrong request (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", wrappedErr.Error())
+	assert.Equal(t, "wrong request (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: internal server error (caused by: my first error), wrong request (from: my second error), internal server error (caused by: hidden info: my third error), already exists (from: my fourth error))", wrappedErr.Error())
 	httpErr = ExtractHTTPError(wrappedErr)
 	assert.Equal(t, "wrong request (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 
 	stackErr := WithStack(mError)
-	assert.Equal(t, "TestMultiError: internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", stackErr.Error())
+	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: internal server error (caused by: my first error), wrong request (from: my second error), internal server error (caused by: hidden info: my third error), already exists (from: my fourth error))", stackErr.Error())
 	httpErr = ExtractHTTPError(stackErr)
 	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 
 	stackErr = WrapError(mError, "more info")
-	assert.Equal(t, "TestMultiError: internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: more info: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", stackErr.Error())
+	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: more info: internal server error (caused by: my first error), wrong request (from: my second error), internal server error (caused by: hidden info: my third error), already exists (from: my fourth error))", stackErr.Error())
 	httpErr = ExtractHTTPError(stackErr)
 	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 }
@@ -205,5 +205,5 @@ func Test_cause(t *testing.T) {
 	mError.Append(err2)
 	mError.Append(err3)
 	mError.Append(err4)
-	assert.Equal(t, "Test_cause: internal server error (from: my error) (caused by: my lib error), Test_cause: wrong request (from: my error), Test_cause: wrong request (from: my error), Test_cause: wrong request", Cause(mError).Error())
+	assert.Equal(t, "internal server error (from: my error) (caused by: my lib error), wrong request (from: my error), wrong request (from: my error), wrong request", Cause(mError).Error())
 }
diff --git a/sdk/hatchery/hatchery_test.go b/sdk/hatchery/hatchery_test.go
new file mode 100644
index 000000000..44bc310f6
--- /dev/null
+++ b/sdk/hatchery/hatchery_test.go
@@ -0,0 +1,117 @@
+package hatchery_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+	"github.com/rockbears/log"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ovh/cds/engine/service"
+	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/cdsclient/mock_cdsclient"
+	"github.com/ovh/cds/sdk/hatchery"
+	"github.com/ovh/cds/sdk/hatchery/mock_hatchery"
+	"github.com/ovh/cds/sdk/jws"
+)
+
+func TestCreate(t *testing.T) {
+	log.Factory = log.NewTestingWrapper(t)
+	ctx := context.TODO()
+	ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
+	defer cancel()
+	ctrl1 := gomock.NewController(t)
+	ctrl2 := gomock.NewController(t)
+
+	t.Cleanup(func() {
+		ctrl1.Finish()
+		ctrl2.Finish()
+	})
+
+	mockHatchery := mock_hatchery.NewMockInterface(ctrl1)
+	mockCDSClient := mock_cdsclient.NewMockInterface(ctrl2)
+
+	grtn := sdk.NewGoRoutines(ctx)
+	hatcheryConfig := service.HatcheryCommonConfiguration{
+		Name: t.Name(),
+	}
+	hatcheryConfig.Provision.MaxWorker = 1
+
+	hatchery.CacheSpawnIDsTTL = 2 * time.Second // decrease this cache TTL to speedup the test
+	hatchery.MaxAttemptsNumberBeforeFailure = 2 // decrease this value to speedup the test
+
+	mockHatchery.EXPECT().Name().Return(t.Name()).AnyTimes()
+	mockHatchery.EXPECT().Type().Return(sdk.TypeHatchery).AnyTimes()
+	mockHatchery.EXPECT().Service().Return(&sdk.Service{}).AnyTimes()
+	mockHatchery.EXPECT().InitHatchery(gomock.Any()).Return(nil)
+	mockHatchery.EXPECT().Configuration().Return(hatcheryConfig).AnyTimes()
+	mockHatchery.EXPECT().GetGoRoutines().Return(grtn).AnyTimes()
+	mockHatchery.EXPECT().CDSClient().Return(mockCDSClient).AnyTimes()
+	mockHatchery.EXPECT().GetLogger().Return(getMockLogger()).AnyTimes()
+	mockCDSClient.EXPECT().QueuePolling(gomock.Any(), grtn, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), nil).DoAndReturn(
+		func(ctx context.Context, goRoutines *sdk.GoRoutines, jobs chan<- sdk.WorkflowNodeJobRun, errs chan<- error, delay time.Duration, modelType string, ratioService *int) error {
+			j := sdk.WorkflowNodeJobRun{
+				ProjectID:         1,
+				ID:                666,
+				WorkflowNodeRunID: 1,
+				Status:            sdk.StatusWaiting,
+				Job: sdk.ExecutedJob{
+					Job: sdk.Job{},
+				},
+				Start: time.Now(),
+			}
+
+			jobs <- j                   // Send the job a first time, it will trigger the first call on SpawnWorker
+			time.Sleep(1 * time.Second) // Wait
+			jobs <- j                   // This one must be ignored with a log "already spawned in previous routine"
+			time.Sleep(2 * time.Second) // Wait
+			jobs <- j                   // This will trigger a second call on SpawnWorker
+			time.Sleep(3 * time.Second) // Wait
+			jobs <- j                   // This shoud not trigger the call on SpawnWorker but should fail the job
+
+			<-ctx.Done()
+			return ctx.Err()
+		},
+	)
+
+	// This calls are expected for each job received in the channel
+	mockCDSClient.EXPECT().WorkerList(gomock.Any()).Return(nil, nil).AnyTimes()
+	mockHatchery.EXPECT().WorkersStarted(gomock.Any()).Return(nil).AnyTimes()
+	mockHatchery.EXPECT().CanSpawn(gomock.Any(), gomock.Any(), int64(666), gomock.Any()).Return(true).AnyTimes()
+	mockCDSClient.EXPECT().QueueJobBook(gomock.Any(), int64(666)).Return(sdk.WorkflowNodeJobRunBooked{}, nil).AnyTimes()
+	mockCDSClient.EXPECT().QueueJobSendSpawnInfo(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+	privateKey, err := jws.NewRandomRSAKey()
+	require.NoError(t, err)
+	mockHatchery.EXPECT().GetPrivateKey().Return(privateKey).AnyTimes()
+
+	// Call to SpawnWorker regarding what append in "QueuePolling"
+	mockHatchery.EXPECT().SpawnWorker(gomock.Any(), gomock.Any()).Return(nil).Times(2)
+
+	// Expecing a call to QueueSendResult
+	mockCDSClient.EXPECT().QueueSendResult(gomock.Any(), int64(666), gomock.Any()).Return(nil)
+
+	hatchery.Create(ctx, mockHatchery)
+
+	<-ctx.Done()
+
+}
+
+func getMockLogger() *logrus.Logger {
+	log := logrus.New()
+	log.AddHook(&HookMock{})
+	return log
+}
+
+type HookMock struct{}
+
+func (h *HookMock) Levels() []logrus.Level {
+	return []logrus.Level{
+		logrus.InfoLevel,
+	}
+}
+func (h *HookMock) Fire(e *logrus.Entry) error {
+	return nil
+}