Repository: Riminder/hrflow-connectors (language: python)

Issue:
Architecture/v 1.1.0
This merge request will:
- Change logics to allow them to return a modified object or None. If None, remove from list.
- In Connector, add hrflow_client in the method params
-  Remove Job and Profile from utils/hrflow


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 71ba89981..648e757f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,7 @@ MANIFEST
 # Per-project virtualenvs
 .venv*/
 .conda*/
+.env
 
 # Credentials
 credentials
diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index 041251824..fc9084449 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -19,9 +19,9 @@ In the action execution pipeline, here is where the `logics` are located:
   
 **The definition of a logical function:**
 * The function takes only one parameter. This is an element of the stream.
-* The function returns `bool` :
-  * `True` indicates that the element is kept for the rest of the processing
-  * `False` indicates that the element is not processed in the rest of the pipeline
+* The function returns ``Optional(element)` :
+  * `element` the modified element if a condition is fullfilled
+  * `None` indicates that the element does not fullfill the condition so the element is removed from the data stream and the function returns None.
 
 `global_scope` and `local_scope` are only used to tell the connector how to `eval` the function names given in `logics`.
 Most of the time, you would just write `global_scope=globals()` and `local_scope=locals()`.
@@ -33,7 +33,10 @@ Most of the time, you would just write `global_scope=globals()` and `local_scope
 from hrflow_connectors.core.action import BaseAction
 
 def filter_element1_with_value1(element):
-    return element.get("element1") == "value1"
+    if element.get("element1") == "value1":
+        return element
+    else:
+        return None
 
 action = BaseAction(
     logics=["filter_element1_with_value1"],
diff --git a/LICENSE b/LICENSE
index fb3ef8f0d..9bb69e96e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright [2021] [Hrflow.ai]
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 190f752bf..24bee1bb6 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ to democratize Talent Data integration within the HR Tech landscape.
 | Name | Type | Available |
 | - | - | - |
 | **ADP** |  | :hourglass: |
+| [**Bullhorn**](src/hrflow_connectors/connectors/bullhorn/) | ATS | :heavy_check_mark: |
 | [**Breezy.hr**](src/hrflow_connectors/connectors/breezyhr) | ATS | :heavy_check_mark: |
 | **Cegid (Meta4)** |  | :hourglass: |
 | [**Ceridian**](src/hrflow_connectors/connectors/ceridian) | HCM |:heavy_check_mark: |
@@ -60,6 +61,7 @@ to democratize Talent Data integration within the HR Tech landscape.
 | [**Taleez**](src/hrflow_connectors/connectors/taleez/)| ATS | :heavy_check_mark: |
 | **Talentsoft** |  | :hourglass: |
 | **Talentlink** |  | :hourglass: |
+| [**Teamtailor**](src/hrflow_connectors/connectors/teamtailor) | ATS | :heavy_check_mark: |
 | **Twilio** | Marketing tools | :hourglass: |
 | **Ultimate Software (UKG)** |  | :hourglass: |
 | [**Workable**](src/hrflow_connectors/connectors/workable/) | ATS | :heavy_check_mark: |
diff --git a/VERSION b/VERSION
index e6d5cb833..1cc5f657e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.0.2
\ No newline at end of file
+1.1.0
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index b159233d4..207b9d498 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -794,6 +794,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-dotenv"
+version = "0.19.2"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
 [[package]]
 name = "python-magic"
 version = "0.4.24"
@@ -1069,7 +1080,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "e356606c4d37d4db0a27b1f616246c4852f185d882175210591b392a4c15dfda"
+content-hash = "6558d66d9f376bd3468b47f501a33ae1b835aaa2c4b6d18f46bea9bc009a04ef"
 
 [metadata.files]
 appdirs = [
@@ -1517,6 +1528,10 @@ python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
+python-dotenv = [
+    {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
+    {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
+]
 python-magic = [
     {file = "python-magic-0.4.24.tar.gz", hash = "sha256:de800df9fb50f8ec5974761054a708af6e4246b03b4bdaee993f948947b0ebcf"},
     {file = "python_magic-0.4.24-py2.py3-none-any.whl", hash = "sha256:4fec8ee805fea30c07afccd1592c0f17977089895bdfaae5fec870a84e997626"},
diff --git a/pyproject.toml b/pyproject.toml
index 3d8c6de93..f8cde15bb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,6 +17,7 @@ pytest-cov = "^3.0.0"
 black = "20.8b0"
 unittest = "^0.0"
 responses = "^0.16.0"
+python-dotenv = "^0.19.2"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/setup.cfg b/setup.cfg
index 3f438c8bf..92694d9f7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -49,7 +49,6 @@ install_requires =
     requests==2.26.0
     pydantic==1.7.4
     hrflow==1.9.0
-    selenium==3.141.0
 
 [options.package_data]
 hrflow_connectors = data/*
diff --git a/src/hrflow_connectors/connectors/__init__.py b/src/hrflow_connectors/connectors/__init__.py
index 0f140b60b..390ab0251 100644
--- a/src/hrflow_connectors/connectors/__init__.py
+++ b/src/hrflow_connectors/connectors/__init__.py
@@ -9,4 +9,6 @@
 from .sapsuccessfactors import *
 from .taleez import *
 from .monster import *
+from .bullhorn import *
 from .breezyhr import *
+from .teamtailor import *
diff --git a/src/hrflow_connectors/connectors/breezyhr/actions.py b/src/hrflow_connectors/connectors/breezyhr/actions.py
index 73781a79f..79ef7fe94 100644
--- a/src/hrflow_connectors/connectors/breezyhr/actions.py
+++ b/src/hrflow_connectors/connectors/breezyhr/actions.py
@@ -2,12 +2,15 @@
 from pydantic import Field
 import requests
 
+from ...core.error import PullError, PushError
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...core.auth import OAuth2EmailPasswordBody
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
 from ...utils.hrflow import generate_workflow_response
 from ...utils.datetime_converter import from_str_to_datetime
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import BreezyJobModel, BreezyProfileModel
 
 logger = get_logger()
 
@@ -23,7 +26,7 @@ class PullJobsAction(PullJobsBaseAction):
         None, description="the company associated with the authenticated user"
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[BreezyJobModel]:
         """
         Pull jobs from a Taleez jobs owner endpoint
         Returns list of all jobs that have been pulled
@@ -43,8 +46,7 @@ def get_company_id() -> str:
                 prepared_request = get_company_id_request.prepare()
                 response = session.send(prepared_request)
                 if not response.ok:
-                    error_message = "Couldn't get company id ! Reason : `{}`"
-                    raise RuntimeError(error_message.format(response.content))
+                    raise PullError(response, message="Couldn't get company id")
                 company_list = response.json()
                 logger.debug("Retrieving company id")
                 for company in company_list:
@@ -65,27 +67,26 @@ def get_company_id() -> str:
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(f"Failed to get jobs from this endpoint")
-            error_message = "Unable to pull the data ! Reason : `{}` `{}`"
-            raise RuntimeError(
-                error_message.format(response.status_code, response.content)
-            )
+            raise PullError(response, message="Failed to get jobs from this endpoint")
+        job_json_list = response.json()
+        job_obj_iter = map(BreezyJobModel.parse_obj, job_json_list)
 
-        return response.json()
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: BreezyJobModel) -> HrflowJob:
         """
         Format a Breezy Hr job object into a hrflow job object
 
         Returns:
-            Dict[str, Any]: a job object in the hrflow job format
+            HrflowJob: a job object in the hrflow job format
         """
+        data = data.dict()
+        print(data)
         job = dict()
-
         # Basic information
         job["name"] = data.get("name")
         logger.info(job["name"])
-        job["reference"] = data.get("_id")
+        job["reference"] = data.get("friendly_id")
         logger.info(job["reference"])
         job["summary"] = None
 
@@ -136,9 +137,9 @@ def create_tag(field_name: str):
 
         job["created_at"] = data.get("creation_date")
         job["updated_at"] = data.get("updated_date")
+        job_obj = HrflowJob.parse_obj(job)
 
-        job["metadatas"] = data.get("tags")
-        return job
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -160,18 +161,19 @@ class PushProfileAction(PushProfileBaseAction):
     )
     cover_letter: Optional[str] = None
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: HrflowProfile) -> BreezyProfileModel:
         """
         Format a Hrflow profile object into a breezy hr profile object
 
         Args:
-            data (Dict[str, Any]): Hrflow Profile to format
+            data (HrflowProfile): Hrflow Profile to format
 
         Returns:
-            Dict[str, Any]: a BreezyHr formatted profile object
+            BreezyProfileModel: a BreezyHr formatted profile object
         """
 
         profile = dict()
+        data = data.dict()
         info = data.get("info")
         profile["name"] = info.get("full_name")
         profile["address"] = info.get("location").get("text")
@@ -188,11 +190,10 @@ def format_experiences():
             experiences = data.get("experiences")
             for experience in experiences:
                 format_experience = dict()
-                format_experience["company_name"] = (
-                    experience["company"]
-                    if experience["company"] not in ["", None]
-                    else "Undefined"
-                )
+                if experience["company"] not in ["", None]:
+                    format_experience["company_name"] = experience["company"]
+                else:
+                    format_experience["company_name"] = "Undefined"
                 format_experience["title"] = experience["title"]
                 format_experience["summary"] = experience["description"]
                 if experience["date_start"] is not None:
@@ -218,8 +219,12 @@ def format_educations():
                     education["school"] = "Undefined"
                 format_education["school_name"] = education["school"]
                 format_education["field_of_study"] = education["title"]
-                format_education["start_year"] = education["date_start"]
-                format_education["end_year"] = education["date_end"]
+                if education["date_start"] is not None:
+                    date_iso = from_str_to_datetime((education["date_start"]))
+                    format_education["start_year"] = date_iso.year
+                if education["date_end"] is not None:
+                    date_end_iso = from_str_to_datetime((education["date_end"]))
+                    format_education["end_year"] = date_end_iso.year
                 profile["education"].append(format_education)
 
         format_educations()
@@ -257,112 +262,95 @@ def format_urls() -> None:
                 if isinstance(skill, dict):
                     profile["tags"].append(skill["name"])
 
-        return profile
+        profile_obj = BreezyProfileModel.parse_obj(profile)
+        return profile_obj
 
-    def push(self, data: Dict[str, Any]) -> None:
+    def push(self, data: BreezyProfileModel) -> None:
         """
         Push a Hrflow profile object to a BreezyHr candidate pool for a position
 
         Args:
-            data (Dict[str, Any]): profile to push
+            data (BreezyProfileModel): profile to push
         """
         profile = next(data)
         auth = self.auth
         session = requests.Session()
 
-        def get_company_id() -> str:
+        def send_request(
+            error_message: str,
+            method: str,
+            url: str,
+            json: Optional[Dict[str, Any]] = None,
+        ) -> requests.Response:
             """
-            Get the company id associated with the authenticated user company, required for authentification
-
-            Returns:
-                str: company id
-            """
-            if self.company_id is not None:
-                return self.company_id
-            else:
-                get_company_id_request = requests.Request()
-                get_company_id_request.method = "GET"
-                get_company_id_request.url = "https://api.breezy.hr/v3/companies"
-                get_company_id_request.auth = auth
-                prepared_request = get_company_id_request.prepare()
-                response = session.send(prepared_request)
-                if not response.ok:
-                    error_message = "Couldn't get company id ! Reason : `{}`"
-                    raise RuntimeError(error_message.format(response.content))
-                company_list = response.json()
-                logger.debug("Retrieving company id")
-                for company in company_list:
-                    if company["name"] == self.company_name:
-                        return company["_id"]
+            Send a HTTPS request to the specified url using the specified paramters
 
-        self.company_id = get_company_id()
-
-        def get_candidate() -> Optional[Dict[str, Any]]:
-            """
-            Get candidate. If candidate does not exists, return None.
+            Args:
+                error_message (str): message to be displayed when a PushError is raised
+                method (str): request method: "GET", "PUT", "POST"...
+                url (str): url endpoint to receive the request
+                json (optional): data to be sent to the endpoint. Defaults to None.
+                return_response (optional): In case we want to the function to return the response. Defaults to None.
 
             Returns:
-                Optional[Dict[str, Any]]: a dictionary that contains candidate id and name, if candidate doesn't exist it returns None
+                response (requests.Response)
             """
-            verify_candidate_request = requests.Request()
-            verify_candidate_request.method = "GET"
-            verify_candidate_request.url = f"https://api.breezy.hr/v3/company/{self.company_id}/candidates/search?email_address={profile['email_address']}"
-            verify_candidate_request.auth = auth
-            prepared_request = verify_candidate_request.prepare()
+            request = requests.Request()
+            request.method = method
+            request.url = url
+            request.auth = auth
+            if json is not None:
+                request.json = json
+            prepared_request = request.prepare()
             response = session.send(prepared_request)
             if not response.ok:
-                error_message = "Couldn't get candidate ! Reason : `{}`"
-                raise RuntimeError(error_message.format(response.content))
-            if response.json() == []:
-                return None
-            else:
-                candidate = response.json()[0]
-                return candidate
-
-        candidate = get_candidate()
+                raise PushError(response, message=error_message)
+            return response
+
+        # if the user doesn't specify a company id, we send a request to retrieve it using company_name
+        if self.company_id is None:
+            get_company_id_response = send_request(
+                method="GET",
+                url="https://api.breezy.hr/v3/companies",
+                error_message="Couldn't get company id",
+            )
+            company_list = get_company_id_response.json()
+            for company in company_list:
+                if company["name"] == self.company_name:
+                    self.company_id = company["_id"]
+
+        # a request to verify if the candidate profile already exist
+        get_candidate_url = f"https://api.breezy.hr/v3/company/{self.company_id}/candidates/search?email_address={profile.email_address}"
+        get_candidate_response = send_request(
+            method="GET",
+            url=get_candidate_url,
+            error_message="Couldn't get candidate",
+        )
+        candidate_list = get_candidate_response.json()
+        candidate = None
+        if candidate_list != []:
+            candidate = candidate_list[0]
 
+        # In case the candidate exists we retrieve his id to update his profile with a "PUT" request
         if candidate is not None:
             candidate_id = candidate["_id"]
             logger.info(f"Candidate Already exists with the id {candidate_id}")
-
-            def update_profile_request():
-                """
-                Send a put request to update the candidate profile
-                """
-                update_candidate_request = requests.Request()
-                update_candidate_request.method = "PUT"
-                update_candidate_request.url = f"https://api.breezy.hr/v3/company/{self.company_id}/position/{self.position_id}/candidate/{candidate_id}"
-                update_candidate_request.auth = auth
-                update_candidate_request.headers = {"content-type": "application/json"}
-                update_candidate_request.json = profile
-                prepared_request = update_candidate_request.prepare()
-
-                response = session.send(prepared_request)
-                logger.info("Updating Candidate profile")
-                logger.debug(f"`{response.content}`, `{response.status_code}`")
-                if not response.ok:
-                    error_message = "Couldn't update candidate ! Reason :{}, `{}`"
-                    raise RuntimeError(
-                        error_message.format(response.status_code, response.content)
-                    )
-
-            update_profile_request()
-
+            update_profile_url = f"https://api.breezy.hr/v3/company/{self.company_id}/position/{self.position_id}/candidate/{candidate_id}"
+            logger.info("Updating Candidate profile")
+            send_request(
+                method="PUT",
+                url=update_profile_url,
+                json=profile.dict(),
+                error_message="Couldn't update candidate profile'",
+            )
+        # If the candidate doesn't already exist we "POST" his profile
         else:
             # Post profile request
-            # Prepare request
-            logger.info("Preparing resuest to push candidate profile")
-            push_profile_request = requests.Request()
-            push_profile_request.method = "POST"
-            push_profile_request.url = f"https://api.breezy.hr/v3/company/{self.company_id}/position/{self.position_id}/candidates"
-            push_profile_request.auth = auth
-            push_profile_request.json = profile
-            prepared_request = push_profile_request.prepare()
-
-            # Send request
-            response = session.send(prepared_request)
-            logger.debug(f"`{response.status_code}`,`{response.content}`")
-            if not response.ok:
-                raise RuntimeError(
-                    f"Push profile to Breezy Hr failed :`{response.status_code}` `{response.content}`"
-                )
+            logger.info("Preparing request to push candidate profile")
+            push_profile_url = f"https://api.breezy.hr/v3/company/{self.company_id}/position/{self.position_id}/candidates"
+            send_request(
+                method="POST",
+                url=push_profile_url,
+                json=profile.dict(),
+                error_message="Push Profile to BreezyHr failed",
+            )
diff --git a/src/hrflow_connectors/connectors/breezyhr/connector.py b/src/hrflow_connectors/connectors/breezyhr/connector.py
index ffb2ff824..85f0c228a 100644
--- a/src/hrflow_connectors/connectors/breezyhr/connector.py
+++ b/src/hrflow_connectors/connectors/breezyhr/connector.py
@@ -4,7 +4,7 @@
 from ...core.connector import Connector
 from ...core.auth import OAuth2EmailPasswordBody
 
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -24,7 +24,7 @@ def pull_jobs(
             board_key (str): Board key where the jobs to be added will be stored
             company_name Optional[str]: Name of the company associated with the authenticated user, required if you haven't specified your company id. Default value `None`
             company_id Optional[str] : Id of the company associated with the authenticated user, Default value `None`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -43,12 +43,12 @@ def pull_jobs(
     def push_profile(
         auth: OAuth2EmailPasswordBody,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         position_id: str,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
         """
-        `PushProfileAction` pushes a Hrflow.ai profile to `Breezyhr` via their Salesforce API.
+        `PushProfileAction` pushes a Hrflow.ai profile to `Breezyhr` via their Breezy API.
 
         `Hrflow.ai` -> `Breezyhr`
 
@@ -61,7 +61,7 @@ def push_profile(
             company_id Optional[str] : Id of the company associated with the authenticated user, Default value `None`
             origin Optional[str]: Indicates if the candidate is `sourced` or `applied`, Default value `sourced`
             cover_letter Optional[str]: Candidate's cover letter, default value `None`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md b/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md
index 68f12cc91..9742fbd00 100644
--- a/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md b/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md
index 0724155cd..39c982d0e 100644
--- a/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md
@@ -16,7 +16,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -38,10 +38,10 @@
 from hrflow_connectors import Breezyhr
 from hrflow import Hrflow
 from hrflow_connectors import OAuth2EmailPasswordBody
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = OAuth2EmailPasswordBody(
             access_token_url="https://api.breezy.hr/v3/signin",
             email="EMAIL",
diff --git a/src/hrflow_connectors/connectors/breezyhr/schemas.py b/src/hrflow_connectors/connectors/breezyhr/schemas.py
index 304f7aac6..41264ae33 100644
--- a/src/hrflow_connectors/connectors/breezyhr/schemas.py
+++ b/src/hrflow_connectors/connectors/breezyhr/schemas.py
@@ -20,7 +20,7 @@ class Country(BaseModel):
 class Location(BaseModel):
     country: Country
     city: str
-    is_remote: bool
+    is_remote: Optional[bool]
     name: str
 
 
@@ -55,7 +55,7 @@ class BreezyJobModel(BaseModel):
     friendly_id: str = Field(
         ..., description="another id of the job which combines its title and its id"
     )
-    experience: Experience
+    experience: Optional[Experience]
     location: Location
     education: str
     department: str
@@ -78,17 +78,17 @@ class WorkHistoryItem(BaseModel):
     company_name: str
     title: str
     summary: str
-    start_month: int
-    start_year: int
-    end_month: int
-    end_year: int
+    start_month: Optional[int]
+    start_year: Optional[int]
+    end_month: Optional[int]
+    end_year: Optional[int]
 
 
 class EducationItem(BaseModel):
     school_name: str
     field_of_study: str
-    start_year: int
-    end_year: int
+    start_year: Optional[int]
+    end_year: Optional[int]
 
 
 class BreezyProfileModel(BaseModel):
diff --git a/src/hrflow_connectors/connectors/bullhorn/README.md b/src/hrflow_connectors/connectors/bullhorn/README.md
new file mode 100644
index 000000000..1d9228e7e
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/README.md
@@ -0,0 +1,8 @@
+# Bullhorn Connector
+> Bullhorn's recruitment software to grow your business. Bullhorn strengthens European recruitment agencies.
+
+🔗 https://www.bullhorn.com/
+
+| Actions |
+| ------- |
+| [**Push profile**](docs/push_profile.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/bullhorn/__init__.py b/src/hrflow_connectors/connectors/bullhorn/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/bullhorn/actions.py b/src/hrflow_connectors/connectors/bullhorn/actions.py
new file mode 100644
index 000000000..4b1b98b4f
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/actions.py
@@ -0,0 +1,226 @@
+from ...core.action import PushProfileBaseAction
+from ...core.auth import OAuth2Session
+from ...core.error import PushError
+from ...utils.datetime_converter import from_str_to_datetime
+import xml.etree.ElementTree
+from pydantic import Field, BaseModel
+from ...utils.logger import get_logger
+from ...utils.schemas import HrflowProfile
+from .schemas import BullhornEducationEnrichment, BullhornExperienceEnrichment, BullhornAttachmentEnrichment, BullhornProfile
+from typing import Union
+import requests
+import base64
+
+TalentDataType = Union[str, xml.etree.ElementTree.Element, BaseModel]
+logger = get_logger()
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    subdomain: str = Field(
+        ...,
+        description="Subdomain bullhornstaffing just before `bullhornstaffing.com`. For example "
+                    "subdomain=`my_subdomain.my` in "
+        "`http://my_subdomain.my.bullhornstaffing.com/ABC`",
+    )
+
+    auth: OAuth2Session
+
+    def format(self, data: HrflowProfile) -> BaseModel:
+        data = data.dict()
+        info = data.get('info')
+        def get_location():
+            if info is not None :
+                location = info.get("location")
+                if location is None:
+                    location = dict()
+                location_dict = {
+                    "address1": location.get("fields", {}).get("text"),
+                    "city": location.get("fields", {}).get("city"),
+                    "state": location.get("fields", {}).get("country"),
+                    "zip": location.get("fields", {}).get("postcode")
+                }
+                return location_dict
+            return None
+
+        def get_skills(data):
+            skills = ""
+            if data.get("skills") is not None :
+                for i in range(len(data["skills"]) - 1):
+                    skills += data["skills"][i]["name"] + ", "
+                skills += data["skills"][-1]["name"]
+            return skills
+
+        dateOfBirth = None
+        if info is not None and info.get("date_birth"):
+            date_birth_field = info.get("date_birth")
+            date_birth_timestamp = from_str_to_datetime(date_birth_field).timestamp()
+            dateOfBirth = int(date_birth_timestamp)
+
+        create_profile_body = {
+            "id": data.get("reference"),
+            "address": get_location(),
+            "certifications": None,
+            "name": info.get("full_name") if info else None,
+            "firstName": info.get("first_name") if info else None,
+            "lastName": info.get("last_name") if info else None,
+            "email": info.get("email") if info else None,
+            "mobile": info.get("phone") if info else None,
+            "dateOfBirth": dateOfBirth,
+            "experience": int(data.get('experiences_duration')),
+            "skillSet": get_skills(data) if data.get("skills") else None
+        }
+
+        def get_education(education_list):
+            educations_json = []
+            for hrflow_education in education_list:
+                location = hrflow_education["location"]
+                education = {
+                    "id": "0",
+                    "candidate": {
+                        "id": None
+                    },
+                    "school": hrflow_education.get("school"),
+                    "degree": hrflow_education.get("title"),
+                    "comments": hrflow_education.get("description"),
+                    "city": location.get("text") if location else None,
+                    "startDate": int(from_str_to_datetime(hrflow_education.get("date_start")).timestamp()) if hrflow_education.get(
+                        "date_start") else None,
+                    "endDate": int(from_str_to_datetime(hrflow_education.get("date_end")).timestamp()) if hrflow_education.get(
+                        "date_start") else None
+                }
+                education_obj = BullhornEducationEnrichment.parse_obj(education)
+                educations_json.append(education_obj)
+            return educations_json
+
+        def get_experience(experience_list):
+            experience_json = []
+            for hrflow_experience in experience_list:
+                experience = {
+                    "id": "0",
+                    "candidate": {
+                        "id": None
+                    },
+                    "companyName": hrflow_experience.get("company"),
+                    "title": hrflow_experience.get("title"),
+                    "comments": hrflow_experience.get("description"),
+                    "startDate": int(from_str_to_datetime(hrflow_experience.get("date_start")).timestamp()) if hrflow_experience.get(
+                        "date_start") else None,
+                    "endDate": int(from_str_to_datetime(hrflow_experience.get("date_end")).timestamp()) if hrflow_experience.get(
+                        "date_end") else None
+                }
+                experience_obj = BullhornExperienceEnrichment.parse_obj(experience)
+                experience_json.append(experience_obj)
+            return experience_json
+
+        def get_attachments(attachment_list):
+            attachments_json = []
+            for hrflow_attachment in attachment_list:
+                url = hrflow_attachment["public_url"]
+                response = requests.get(url)
+                b64 = base64.b64encode(response.content)
+
+                attachment = {
+                    "externalID": "portfolio",
+                    "fileContent": b64.decode(),
+                    "fileType": "SAMPLE",
+                    "name": hrflow_attachment["file_name"],
+                    "contentType": "text/plain",
+                    "description": "Resume file for candidate.",
+                    "type": "cover"
+                }
+                attachment_obj = BullhornAttachmentEnrichment.parse_obj(attachment)
+                attachments_json.append(attachment_obj)
+            return attachments_json
+
+        enrich_profile_education = get_education(data.get("educations"))
+        enrich_profile_experience = get_experience(data.get("experiences"))
+        enrich_profile_attachment = get_attachments(data.get("attachments"))
+        # When the action needs to send several requests to push a profile
+        # We group the formats of the different requests in a `profile_body_dict`.
+        profile_body_dict = dict(
+            create_profile_body=BullhornProfile.parse_obj(create_profile_body),
+            enrich_profile_education=enrich_profile_education,
+            enrich_profile_experience=enrich_profile_experience,
+            enrich_profile_attachment=enrich_profile_attachment,
+        )
+
+        return profile_body_dict
+
+    def push(self, data):
+
+        profile_body_dict = next(data)
+        create_profile_body = profile_body_dict["create_profile_body"]
+        enrich_profile_education = profile_body_dict["enrich_profile_education"]
+        enrich_profile_experience = profile_body_dict["enrich_profile_experience"]
+        enrich_profile_attachment = profile_body_dict["enrich_profile_attachment"]
+
+        # Preparing the request to push the profile
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        push_profile_request.method = "PUT"
+        push_profile_request.url = f"https://{self.subdomain}.bullhornstaffing.com/rest-services/7zwdd0/entity/Candidate"
+        push_profile_request.auth = self.auth
+        push_profile_request.json = create_profile_body.dict()
+        prepared_request = push_profile_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+        if not response.ok:
+            raise PushError(response)
+
+        # Get the id of the candidate whom have been just created.
+        candidate_id = response.json()
+        candidate_id = str(candidate_id["changedEntityId"])
+
+        # Preparing the request to enrich education
+        for education in enrich_profile_education:
+            education = education.dict()
+            # Set the Id of the candidate to enrich to the Id of the candidate whom have just been created
+            education["candidate"]["id"] = candidate_id
+            session = requests.Session()
+            push_profile_request = requests.Request()
+            push_profile_request.method = "PUT"
+            push_profile_request.url = f"https://{self.subdomain}.bullhornstaffing.com/rest-services/7zwdd0/entity/CandidateEducation"
+            push_profile_request.auth = self.auth
+            push_profile_request.json = education
+            prepared_request = push_profile_request.prepare()
+
+            # Send request for enrichment
+            response = session.send(prepared_request)
+            if not response.ok:
+                raise PushError(response)
+
+        # Preparing the request to enrich education
+        for experience in enrich_profile_experience:
+            experience = experience.dict()
+            # Set the Id of the candidate to enrich to the Id of the candidate whom have just been created
+            experience["candidate"]["id"] = candidate_id
+            session = requests.Session()
+            push_profile_request = requests.Request()
+            push_profile_request.method = "PUT"
+            push_profile_request.url = f"https://{self.subdomain}.bullhornstaffing.com/rest-services/7zwdd0/entity/CandidateWorkHistory"
+            push_profile_request.auth = self.auth
+            push_profile_request.json = experience
+            prepared_request = push_profile_request.prepare()
+
+            # Send request for enrichment
+            response = session.send(prepared_request)
+            if not response.ok:
+                raise PushError(response)
+
+        # Preparing the request to enrich attachment
+        for attachment in enrich_profile_attachment:
+            attachment = attachment.dict()
+            session = requests.Session()
+            push_profile_request = requests.Request()
+            push_profile_request.method = "PUT"
+            push_profile_request.url = f"https://{self.subdomain}.bullhornstaffing.com/rest-services/7zwdd0/file/Candidate/{candidate_id}"
+            push_profile_request.auth = self.auth
+            push_profile_request.json = attachment
+            prepared_request = push_profile_request.prepare()
+
+            # Send request for enrichment
+            response = session.send(prepared_request)
+            if not response.ok:
+                raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/bullhorn/connector.py b/src/hrflow_connectors/connectors/bullhorn/connector.py
new file mode 100644
index 000000000..62b55ed28
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/connector.py
@@ -0,0 +1,43 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import OAuth2Session
+from ...utils.schemas import HrflowProfile
+from .actions import PushProfileAction
+
+
+class Bullhorn(Connector):
+    @staticmethod
+    def push_profile(auth: OAuth2Session,
+                 hrflow_client: Hrflow,
+                 subdomain: str,
+                 profile: HrflowProfile,
+                 **kwargs
+            ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Bullhorn`.
+
+        `Hrflow.ai` -> `Bullhorn`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (OAuth2Session): Auth instance to identify and communicate with the platform
+            profile (HrflowProfile): Profile to push
+            subdomain (str): Subdomain Bullhorn just before `bullhorn.com`. For example subdomain=`my_subdomain.my` in `http//my_subdomain.my.salesforce.com/ABC`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
+            global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
+            local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
+            format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            subdomain=subdomain,
+            profile=profile,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/bullhorn/docs/push_profile.md b/src/hrflow_connectors/connectors/bullhorn/docs/push_profile.md
new file mode 100644
index 000000000..0bcbcdda8
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/docs/push_profile.md
@@ -0,0 +1,55 @@
+# Push profile
+`Hrflow.ai` :arrow_right: `Bullhorn`
+
+`PushProfileAction` pushes a Hrflow.ai profile to `Bullhorn` via their API.
+
+**Links to Flatchr documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Push profile](http://bullhorn.github.io/rest-api-docs) | Push a candidate with minimum information, the request method is `POST` |
+| [Enrich profile Skills](http://bullhorn.github.io/rest-api-docs) | Enrich profile Skills `POST` |
+| [Enrich profile Expriences](http://bullhorn.github.io/rest-api-docs) | Enrich profile Expriences `POST` |
+| [Enrich profile Educations](http://bullhorn.github.io/rest-api-docs) | Enrich profile Educations `POST` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
+| `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
+| `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
+| `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
+| `hrflow_client` :red_circle: | `hrflow.Hrflow` | Hrflow client instance used to communicate with the Hrflow.ai API        |
+| `profile` :red_circle: | `HrflowProfile` | Profile to push        |
+| `auth` :red_circle: | `OAuth2Session` | Auth instance to identify and communicate with the platform        |
+| `subdomain` :red_circle: | `str` | Subdomain Bullhorn just before bullhornstaffing.com. For example subdomain=my_subdomain.my in http://my_subdomain.my.bullhornstaffing.com/ABC        |
+
+:red_circle: : *required* 
+
+## Example
+Let's take as an example in a [***CATCH workflow***](https://developers.hrflow.ai/docs/workflows#catch-setup).
+```python
+from hrflow_connectors import Bullhorn
+from hrflow import Hrflow
+from hrflow_connectors import OAuth2Session
+from hrflow_connectors.utils.schemas import HrflowProfile
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
+auth = OAuth2Session(auth_code_url="https://auth.bullhornstaffing.com/oauth/authorize",
+                    access_token_url="https://auth.bullhornstaffing.com/oauth/token",
+                    session_token_url="https://rest.bullhornstaffing.com/rest-services/login",
+                    client_id="MY_CLIENT_ID",
+                    client_secret="MY_CLIENT_SECRET",
+                    username="MY_USERNAME",
+                    password="MY_PASSWORD",
+                    name="BhRestToken")
+
+Bullhorn.push_profile(
+    auth=auth,
+    subdomain="rest91",
+    hrflow_client=hrflow_client(),
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/bullhorn/schemas.py b/src/hrflow_connectors/connectors/bullhorn/schemas.py
new file mode 100644
index 000000000..4a4d30ce8
--- /dev/null
+++ b/src/hrflow_connectors/connectors/bullhorn/schemas.py
@@ -0,0 +1,65 @@
+from typing import Any, Optional
+from pydantic import BaseModel, Field
+
+
+class BullhornAddress(BaseModel):
+    address1: Optional[str] = Field(None, Description="Adress of the profile")
+    city: Optional[str] = Field(None, Description="City of the profile")
+    state: Optional[str] = Field(None, Description="Country code of the profile")
+    zip: Optional[str] = Field(None, Description="Postal code of the profile")
+
+
+class BullhornProfile(BaseModel):
+    id: Optional[str] = Field(None, Description="Unique identifier for this entity")
+    address: Optional[BullhornAddress] = Field(None, Description="Candidate address")
+    certifications: Any = Field(None, Description="Candidate’s certifications")
+    name: Optional[str] = Field(None, Description="Candidate’s full name. If setting firstname or lastname, you must also set "
+                                       "this field; it does not populate automatically")
+    firstName: Optional[str] = Field(None, Description="Candidate’s first name")
+    lastName: Optional[str] = Field(None, Description="Name of the file")
+    email: Optional[Optional[str]] = Field(None, Description="Candidate’s email address")
+    mobile: Optional[Optional[str]] = Field(None, Description="Candidate’s mobile (cell) telephone number")
+    dateOfBirth: Optional[int] = Field(None, Description="Candidate’s date of birth")
+    experience: Optional[int] = Field(None, Description="Number of years of experience that the Candidate has")
+    skillSet: Optional[str] = Field(None, Description="Text description of Candidate’s skills")
+
+
+class BullhornAttachmentEnrichment(BaseModel):
+    externalID: Optional[str] = Field(None, Description="External identifier for the file")
+    fileContent: Optional[str] = Field(None, Description="Base64-encoded Optional[str]ing of the file content")
+    fileExtension: Optional[Optional[str]] = Field(None, Description="Extension of the file. For example, .doc or .jpg")
+    fileType: Optional[str] = Field(None, Description="Always use the value “SAMPLE”")
+    name: Optional[str] = Field(None, Description="File name. If a file extension is included as part of the name and the "
+                                       "fileExtension field is not set, the file extension in the name is used.")
+    contentType: Optional[str] = Field(None, Description="Type/subtype of the file content.type")
+    description: Optional[str] = Field(None, Description="Unique identifier for this entity")
+    type: Optional[str]
+
+
+class BullhornCandidate(BaseModel):
+    id: Optional[Optional[int]] = Field(None, Description="Unique identifier for this entity")
+
+
+class BullhornExperienceEnrichment(BaseModel):
+    id: Optional[str] = Field(None, Description="Unique identifier for this entity")
+    candidate: BullhornCandidate = Field(None, Description="Candidate for whom this person is a reference")
+    companyName: Optional[Optional[str]] = Field(None, Description="Name of the company where reference works, if it does not "
+                                                        "have a ClientCorporation record in Bullhorn")
+    title: Optional[Optional[str]] = Field(None, Description="Candidate’s job title in this position")
+    comments: Optional[Optional[str]] = Field(None, Description="Free-text comments on CandidateWorkHistory")
+    startDate: Optional[Optional[int]] = Field(None, Description="Date on which Candidate began working at this position")
+    endDate: Optional[Optional[int]] = Field(None, Description="Date on which job ended, if applicable")
+
+
+class BullhornEducationEnrichment(BaseModel):
+    id: Optional[str] = Field(None, Description="Unique identifier for this entity")
+    candidate: BullhornCandidate = Field(None, Description="Candidate for whom this person is a reference")
+    school: Optional[Optional[str]] = Field(None, Description="Name of the educational institute where this education took place")
+    degree: Optional[Optional[str]] = Field(None, Description="Indicates what educational degree the Candidate received; for "
+                                                   "example, B.A., M.A., Ph.D., and so forth")
+    comments: Optional[Optional[str]] = Field(None, Description="Free-text comments on this record")
+    city: Optional[Optional[str]] = Field(None, Description="Name of the city where the education took place")
+    startDate: Optional[Optional[int]] = Field(None, Description="Date when Candidate began study")
+    endDate: Optional[Optional[int]] = Field(None, Description="Date when Candidate finished this education")
+
+
diff --git a/src/hrflow_connectors/connectors/ceridian/actions.py b/src/hrflow_connectors/connectors/ceridian/actions.py
index 3e4b86ec2..7be40646a 100644
--- a/src/hrflow_connectors/connectors/ceridian/actions.py
+++ b/src/hrflow_connectors/connectors/ceridian/actions.py
@@ -2,7 +2,10 @@
 from pydantic import Field
 import requests
 
+from ...core.error import PullError
 from ...core.action import PullJobsBaseAction
+from ...utils.schemas import HrflowJob
+from .schemas import CeridianDayforceJobModel
 
 
 class PullJobsAction(PullJobsBaseAction):
@@ -13,13 +16,12 @@ class PullJobsAction(PullJobsBaseAction):
         description="Uniquely identifies the client's Dayforce instance. Is needed to login",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[CeridianDayforceJobModel]:
         """
         pull all jobs from a ceridian dayforce job feed space
-        Raises:
-            ConnectionError: if the request failed, you may want to check your subdomain or client name space
+
         Returns:
-            Iterator[Dict[str, Any]]: a list of jobs dictionaries
+            Iterator[CeridianDayforceJobModel]: a list of jobs dictionaries
         """
         session = requests.Session()
         pull_jobs_request = requests.Request()
@@ -31,21 +33,20 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
+            raise PullError(response)
+        job_json_list = response.json()
+        job_obj_iter = map(CeridianDayforceJobModel.parse_obj, job_json_list)
+        return job_obj_iter
 
-            error_message = "Unable to pull the data ! Reason : `{}`, `{}`"
-            raise ConnectionError(
-                error_message.format(response.status_code, response.content)
-            )
-        return response.json()
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: CeridianDayforceJobModel) -> HrflowJob:
         """
         format a job into the hrflow job object format
         Args:
-            data (Dict[str, Any]): a job object pulled from a ceridian dayforce job fed space
+            data (CeridianDayforceJobModel): a job object pulled from a ceridian dayforce job fed space
         Returns:
-            Dict[str, Any]: a job into the hrflow job object format
+            HrflowJob: a job into the hrflow job object format
         """
+        data = data.dict()
         job = dict()
         # basic information
         job["name"] = data.get("Title")
@@ -86,5 +87,6 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
             dict(name="dayforce_company_name", value=company_name),
             dict(name="dayforce_remote", value=remote),
         ]
+        job_obj = HrflowJob.parse_obj(job)
 
-        return job
+        return job_obj
diff --git a/src/hrflow_connectors/connectors/ceridian/connector.py b/src/hrflow_connectors/connectors/ceridian/connector.py
index 9b4bb1438..6ef053e1d 100644
--- a/src/hrflow_connectors/connectors/ceridian/connector.py
+++ b/src/hrflow_connectors/connectors/ceridian/connector.py
@@ -24,7 +24,7 @@ def pull_jobs(
             board_key (str): Board key where the jobs to be added will be stored
             subdomain (str): subdomain just before `dayforcehcm.com`
             client_name_space (str): Uniquely identifies the client's Dayforce instance for example. Is needed to login
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
index 0b823b44b..5d2309290 100644
--- a/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/ceridian/schemas.py b/src/hrflow_connectors/connectors/ceridian/schemas.py
index e853b4c1b..074f158b6 100644
--- a/src/hrflow_connectors/connectors/ceridian/schemas.py
+++ b/src/hrflow_connectors/connectors/ceridian/schemas.py
@@ -1,22 +1,23 @@
 from pydantic import BaseModel
+from typing import Optional
 
 
 class CeridianDayforceJobModel(BaseModel):
     Title: str
-    Description: str
-    ClientSiteName: str
-    ClientSiteXRefCode: str
-    CompanyName: str
-    ParentCompanyName: str
+    Description: Optional[str]
+    ClientSiteName: Optional[str]
+    ClientSiteXRefCode: Optional[str]
+    CompanyName: Optional[str]
+    ParentCompanyName: Optional[str]
     JobDetailsUrl: str
-    ApplyUrl: str
-    AddressLine1: str
-    City: str
-    State: str
-    Country: str
-    PostalCode: str
-    DatePosted: str
-    LastUpdated: str
+    ApplyUrl: Optional[str]
+    AddressLine1: Optional[str]
+    City: Optional[str]
+    State: Optional[str]
+    Country: Optional[str]
+    PostalCode: Optional[str]
+    DatePosted: Optional[str]
+    LastUpdated: Optional[str]
     ReferenceNumber: int
-    ParentRequisitionCode: int
-    IsVirtualLocation: bool
+    ParentRequisitionCode: Optional[int]
+    IsVirtualLocation: Optional[bool]
diff --git a/src/hrflow_connectors/connectors/crosstalent/actions.py b/src/hrflow_connectors/connectors/crosstalent/actions.py
index e7273d181..63eb0dca0 100644
--- a/src/hrflow_connectors/connectors/crosstalent/actions.py
+++ b/src/hrflow_connectors/connectors/crosstalent/actions.py
@@ -3,9 +3,11 @@
 import requests
 
 from ...core.auth import OAuth2PasswordCredentialsBody
-
+from ...core.error import PullError, PushError
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...utils.hrflow import generate_workflow_response
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import CrosstalentJob, CrosstalentProfile
 
 
 class PullJobsAction(PullJobsBaseAction):
@@ -17,7 +19,7 @@ class PullJobsAction(PullJobsBaseAction):
         description="Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[CrosstalentJob]:
         # Prepare request
         session = requests.Session()
         pull_jobs_request = requests.Request()
@@ -30,13 +32,15 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
+            raise PullError(response)
+        job_json_list = response.json()
+        job_obj_iter = map(CrosstalentJob.parse_obj, job_json_list)
 
-        return response.json()
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: CrosstalentJob) -> HrflowJob:
         job = dict()
+        data = data.dict()
 
         # name
         job["name"] = data.get("Name", "Undefined")
@@ -279,7 +283,6 @@ def create_metadata(field_name: str):
         create_metadata("crta__Entite__c")
         create_metadata("crta__Entity__c")
         create_metadata("crta__Fiche_de_poste__c")
-        create_metadata("crta__Location__c")
         create_metadata("crta__Nb_positionnements__c")
         create_metadata("crta__Rattachement_hierarchique__c")
         create_metadata("crta__Recruteur__c")
@@ -292,7 +295,9 @@ def create_metadata(field_name: str):
         create_metadata("crtarecr__Nb_of_applications_still_to_be_processed__c")
         create_metadata("Besoin_client__c")
 
-        return job
+        job_obj = HrflowJob.parse_obj(job)
+
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -302,7 +307,8 @@ class PushProfileAction(PushProfileBaseAction):
         description="Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`",
     )
 
-    def format(self, data):
+    def format(self, data: HrflowProfile) -> CrosstalentProfile:
+        data = data.dict()
         firstname = data["info"].get("first_name")
         lastname = data["info"].get("last_name")
         email = data["info"].get("email")
@@ -321,6 +327,7 @@ def format(self, data):
             else:
                 email = f"{key}@vulcain.com"
             data["info"]["email"] = email
+        data = CrosstalentProfile.parse_obj(data)
 
         return data
 
@@ -333,13 +340,11 @@ def push(self, data):
         push_profile_request.method = "POST"
         push_profile_request.url = f"https://{self.subdomain}.salesforce.com/services/apexrest/crta/HrFlowCreateProfile"
         push_profile_request.auth = self.auth
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_request = push_profile_request.prepare()
 
         # Send request
         response = session.send(prepared_request)
 
         if not response.ok:
-            raise RuntimeError(
-                f"Push profile to Crosstalent failed : `{response.content}`"
-            )
+            raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/crosstalent/connector.py b/src/hrflow_connectors/connectors/crosstalent/connector.py
index 570286a5a..3cd078c0b 100644
--- a/src/hrflow_connectors/connectors/crosstalent/connector.py
+++ b/src/hrflow_connectors/connectors/crosstalent/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import OAuth2PasswordCredentialsBody
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -26,7 +26,7 @@ def pull_jobs(
             auth (OAuth2PasswordCredentialsBody): Auth instance to identify and communicate with the platform
             board_key (str): Board key where the jobs to be added will be stored
             subdomain (str): Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http//my_subdomain.my.salesforce.com/ABC`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -49,7 +49,7 @@ def pull_jobs(
     def push_profile(
         auth: OAuth2PasswordCredentialsBody,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         subdomain: str,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
@@ -63,7 +63,7 @@ def push_profile(
             auth (OAuth2PasswordCredentialsBody): Auth instance to identify and communicate with the platform
             profile (Profile): Profile to push
             subdomain (str): Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http//my_subdomain.my.salesforce.com/ABC`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md b/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
index 1d642cd36..fb2f2c579 100644
--- a/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
@@ -7,7 +7,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md b/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md
index 842dd9040..b0471cc72 100644
--- a/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md
@@ -7,7 +7,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -23,10 +23,10 @@
 from hrflow_connectors import Crosstalent
 from hrflow import Hrflow
 from hrflow_connectors import OAuth2PasswordCredentialsBody
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = OAuth2PasswordCredentialsBody(
     access_token_url="https://test.salesforce.com/services/oauth2/token",
     client_id="CLIENT_ID",
diff --git a/src/hrflow_connectors/connectors/crosstalent/schemas.py b/src/hrflow_connectors/connectors/crosstalent/schemas.py
new file mode 100644
index 000000000..0b0db4224
--- /dev/null
+++ b/src/hrflow_connectors/connectors/crosstalent/schemas.py
@@ -0,0 +1,332 @@
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any
+
+
+class CrosstalentJob(BaseModel):
+    Name: str = Field("Undefined")
+    Id: str = Field(..., description="ID d'enregistrement")
+    createdDate: Optional[str]
+    crtarecr__URL_of_the_form_on_job_offer__c: Optional[str] = Field(
+        None, description="URL of the job"
+    )
+    crta__Location__Latitude__s: Optional[float] = Field(
+        None, description="Geolocation latitude"
+    )
+    crta__Location__Longitude__s: Optional[float] = Field(
+        None, description="Geolocation longitude"
+    )
+    Lieu__c: Optional[str] = Field(None, description="Address")
+    crta__CT_City__c: Optional[str] = Field(None, description="City")
+    crta__CT_Country__c: Optional[str] = Field(None, description="Country")
+    crta__CT_Postal_code__c: Optional[str] = Field(None, description="Postal code")
+    crta__Postal_Code__c: Optional[str] = Field(None, description="Postal code")
+    crta__CT_Description__c: Optional[str] = Field(None, description="Job description")
+    crta__CT_Benefices_attendus__c: Optional[str] = Field(
+        None, description="Job benefits"
+    )
+    crta__CT_Description__c: Optional[str] = Field(None, description="Job description")
+    crta__CT_Detail_motif_recours_CDD__c: Optional[str]
+    crta__CT_Horaires_particuliers__c: Optional[str]
+    crta__CT_Needed_skills__c: Optional[str]
+    crta__CT_Other_contract_benefits__c: Optional[str]
+    crta__CT_Other_salary_benefits__c: Optional[str]
+    crta__CT_Remuneration_variable__c: Optional[str]
+    crta__RC_Service__c: Optional[str]
+    crtarecr__Personalised_header__c: Optional[str]
+    crtarecr__Required_Profile__c: Optional[str]
+    Profil_recherche__c: Optional[str]
+    crtarecr__Language_1__c: Optional[str]
+    crtarecr__Language_level_1__c: Optional[str]
+    crtarecr__Language_2__c: Optional[str]
+    crtarecr__Language_level_2__c: Optional[str]
+    crtarecr__Language_3__c: Optional[str]
+    crtarecr__Language_level_3__c: Optional[str]
+    OwnerId: Optional[str]
+    IsDeleted: Optional[bool]
+    CurrencyIsoCode: Optional[str]
+    RecordTypeId: Optional[str]
+    CreatedById: Optional[str]
+    LastModifiedDate: Optional[str]
+    LastModifiedById: Optional[str]
+    SystemModstamp: Optional[str]
+    LastActivityDate: Optional[str]
+    LastViewedDate: Optional[str]
+    LastReferencedDate: Optional[str]
+    crta__Etat__c: Optional[str]
+    crta__CT_Avec_RTT__c: Optional[bool]
+    crta__CT_Annee_budget__c: Optional[str]
+    crta__CT_Contact_Person__c: Optional[str]
+    crta__CT_Motif_recours_CDD__c: Optional[str]
+    crta__CT_Nom_demandeur_DAR__c: Optional[str]
+    crta__Contrainte__c: Optional[str]
+    crta__Categorie__c: Optional[str]
+    crta__CT_Organization_name__c: Optional[str]
+    crta__Diffusion_de_l_offre__c: Optional[bool]
+    crta__Origine_de_l_offre__c: Optional[str]
+    crta__Email_du_recruteur__c: Optional[str]
+    crta__Temps_de_travail_en_pourcentage__c: Optional[str]
+    crta__Filiere__c: Optional[str]
+    crta__CT_Date__c: Optional[str]
+    crta__Contrat__c: Optional[str]
+    crta__CT_Salary__c: Optional[str]
+    crta__Date_max_du_positionnement__c: Optional[str]
+    crta__CT_Nom_Responsable_hierarchique__c: Optional[str]
+    crta__CT_Duration__c: Optional[str]
+    crta__CT_Duree_Stage__c: Optional[str]
+    crta__CT_Email_Manager__c: Optional[str]
+    crta__CT_End_date__c: Optional[str]
+    crta__CT_Modalite_attribution_remun_variable__c: Optional[str]
+    crta__CT_Industry_of_activities__c: Optional[str]
+    crta__CT_Date_debut_Contrat__c: Optional[str]
+    crta__CT_Date_fin_Contrat__c: Optional[str]
+    crta__CT_date_validation_DAR__c: Optional[str]
+    crta__CT_Motif_CDI__c: Optional[str]
+    crta__CT_Number_of_opened_positions__c: Optional[int]
+    crta__CT_Prevu_au_budget__c: Optional[bool]
+    crta__CT_Publiee_Site_web_externe__c: Optional[bool]
+    crta__CT_Scope__c: Optional[bool]
+    crta__CT_Entrprise_Postal_code__c: Optional[str]
+    crta__Date_limite_de_reponse__c: Optional[str]
+    crta__Duree_ouverture_mois__c: Optional[int]
+    crta__Etat__c: Optional[str]
+    crta__Status__c: Optional[str]
+    crtarecr__Archived__c: Optional[bool]
+    crtarecr__Contract_type__c: Optional[str]
+    crtarecr__Published_on_Jobboards__c: Optional[bool]
+    crtarecr__Published_on_Website__c: Optional[bool]
+    crtarecr__Recruitment_Request_Date__c: Optional[str]
+    crtarecr__Recruitment_request_status__c: Optional[str]
+    M_tier__c: Optional[str]
+    Famille_de_m_tier__c: Optional[str]
+    Sourcing_Auto__c: Optional[bool]
+    Disponibilit_imm_diate__c: Optional[bool]
+    Date_de_d_but__c: Optional[str]
+    Fourchette_de_salaire__c: Optional[str]
+    Mobilit_R_gion__c: Optional[str]
+    Entit_juridique__c: Optional[str]
+    Compte_de_rattachement__c: Optional[str]
+    Site_de_diffusion_de_l_offre__c: Optional[str]
+    Mobilit_Pays__c: Optional[str]
+    Position__c: Optional[str]
+    Mobilit_R_gion__c: Optional[str]
+    Pays__c: Optional[str]
+    Mots_Clefs__c: Optional[str]
+    Emploi_Type__c: Optional[str]
+    Niveau_d_exp_rience_attendu__c: Optional[str]
+    Domaine_d_activite__c: Optional[str]
+    Secteur_d_activite__c: Optional[str]
+    Date_de_fin__c: Optional[str]
+    crtarecr__BU1__c: Optional[str]
+    crtarecr__BU2__c: Optional[str]
+    crtarecr__Contractual_status__c: Optional[str]
+    crtarecr__Employment_start_date__c: Optional[str]
+    crtarecr__End_date_of_Intranet_Site_publication__c: Optional[str]
+    crtarecr__End_date_of_Jobboard_publication__c: Optional[str]
+    crtarecr__End_date_of_Website_publication__c: Optional[str]
+    crtarecr__Envisaged_annual_gross_remuneration__c: Optional[str]
+    crtarecr__HR_Function__c: Optional[str]
+    crtarecr__Max_salary__c: Optional[int]
+    crtarecr__Max_term_months__c: Optional[int]
+    crtarecr__Min_salary__c: Optional[int]
+    crtarecr__Min_term_months__c: Optional[int]
+    crtarecr__Period__c: Optional[str]
+    crtarecr__Recruiter_Firstname__c: Optional[str]
+    crtarecr__Recruiter_Lastname__c: Optional[str]
+    crtarecr__Reference__c: Optional[str]
+    crtarecr__Required_Experience_Level__c: Optional[str]
+    crtarecr__Required_diploma_level__c: Optional[str]
+    crtarecr__Start_date_of_Intranet_Site_publication__c: Optional[str]
+    crtarecr__Start_date_of_Jobboard_publication__c: Optional[str]
+    crtarecr__Start_date_of_Website_publication__c: Optional[str]
+    crtarecr__Type_of_employment__c: Optional[str]
+    crtarecr__Weekly_Working_Time_in_hours__c: Optional[int]
+    crta__CT_Designation__c: Optional[str]
+    crta__Location__c: Optional[dict]
+    crta__Nb_positionnements__c: Optional[int]
+    crta__Texte_statut__c: Optional[str]
+    crta__Valeur_statut__c: Optional[int]
+    crtarecr__Link_of_the_form_on_job_offer__c: Optional[str]
+    crtarecr__Nb_of_applications_still_to_be_processed__c: Optional[int]
+    Besoin_client__c: Optional[str]
+    crta__Entite__c: Optional[str]
+
+
+class FieldLocation(BaseModel):
+    """ Location for profile info, experience and education information"""
+
+    text: Optional[str] = Field(None, description="Location text address.")
+    lat: Optional[float] = Field(
+        None, description="Geocentric latitude of the Location."
+    )
+    lng: Optional[float] = Field(
+        None, description="Geocentric longitude of the Location."
+    )
+    Fields: Optional[Dict[str, Any]] = Field(
+        None, description="other location attributes like country, country_codeNoneetc"
+    )
+
+
+class FieldSkill(BaseModel):
+    name: str = Field(..., description="Identification name of the skill")
+    type: str = Field(..., description="Type of the skill. hard or soft")
+    value: Optional[str] = Field(None, description="Value associated to the skill")
+
+
+class InfoUrls(BaseModel):
+    """ Porfile urls """
+
+    from_resume: Optional[List[str]]
+    linkedin: Optional[str]
+    twitter: Optional[str]
+    facebook: Optional[str]
+    github: Optional[str]
+
+
+class HrflowProfileField(BaseModel):
+    """
+    HrflowJobField: Languages, Certifications, Courses, Tasks, Interests, Metadatas, Tags
+    are arrays of objects following the JSON structure below
+    """
+
+    name: str = Field(..., description="Identification name of the Object")
+    value: Optional[str] = Field(
+        None, description="Value associated to the Object's name"
+    )
+
+
+class CrosstalentProfileInfo(BaseModel):
+    """ Profile basic informations"""
+
+    full_name: Optional[str]
+    first_name: Optional[str]
+    last_name: str
+    email: str
+    phone: Optional[str]
+    date_birth: Optional[str] = Field(None, description="Profile date of birth")
+    location: Optional[FieldLocation] = Field(
+        None, description="Profile location object"
+    )
+    urls: Optional[InfoUrls] = Field(
+        None, description="Profile social networks and URLs"
+    )
+    picture: Optional[str] = Field(None, description="Profile picture url")
+    gender: Optional[str] = Field(None, description="Profile gender")
+    summary: Optional[str] = Field(None, description="Profile summary text")
+
+
+class Experience(BaseModel):
+    key: Optional[str] = Field(
+        None, description="Identification key of the Experience."
+    )
+    company: Optional[str] = Field(None, description="Company name of the Experience.")
+    title: Optional[str] = Field(None, description="Title of the Experience.")
+    description: Optional[str] = Field(
+        None, description="Description of the Experience."
+    )
+    location: Optional[FieldLocation] = Field(
+        None, description="Location object of the Experience."
+    )
+    date_start: Optional[str] = Field(
+        None, description="Start date of the experience. type: ('datetime ISO 8601')"
+    )
+    date_end: Optional[str] = Field(
+        None, description="End date of the experience. type: ('datetime ISO 8601')"
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Experience."
+    )
+    certifications: Optional[List[HrflowProfileField]]
+    courses: Optional[List[HrflowProfileField]]
+    tasks: Optional[List[HrflowProfileField]]
+
+
+class Education(BaseModel):
+    key: Optional[str] = Field(None, description="Identification key of the Education.")
+    school: Optional[str] = Field(None, description="School name of the Education.")
+    title: Optional[str] = Field(None, description="Title of the Education.")
+    description: Optional[str] = Field(
+        None, description="Description of the Education."
+    )
+    location: Optional[FieldLocation] = Field(
+        None, description="Location object of the Education."
+    )
+    date_start: Optional[str] = Field(
+        None, description="Start date of the Education. type: ('datetime ISO 8601')"
+    )
+    date_end: Optional[str] = Field(
+        None, description="End date of the Education. type: ('datetime ISO 8601')"
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Education."
+    )
+    certifications: Optional[List[HrflowProfileField]]
+    courses: Optional[List[HrflowProfileField]]
+    tasks: Optional[List[HrflowProfileField]]
+
+
+class CrosstalentProfile(BaseModel):
+    """ Crosstalent Push Profile object model"""
+
+    key: Optional[str] = Field(None, description="Identification key of the Profile.")
+    reference: Optional[str] = Field(
+        None, description="Custom identifier of the Profile."
+    )
+    archieved_at: Optional[str] = Field(
+        None,
+        description="type: datetime ISO8601, Archive date of the Profile. The value is null for unarchived Profiles.",
+    )
+    updated_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Last update date of the Profile."
+    )
+    created_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Creation date of the Profile."
+    )
+    info: CrosstalentProfileInfo = Field(
+        ..., description="Object containing the Profile's info."
+    )
+    text_language: str = Field(
+        ..., description="Code language of the Profile. type: string code ISO 639-1"
+    )
+    text: str = Field(..., description="Full text of the Profile..")
+    experiences_duration: float = Field(
+        ..., description="Total number of years of experience."
+    )
+    educations_duration: float = Field(
+        ..., description="Total number of years of education."
+    )
+    experiences: Optional[List[Experience]] = Field(
+        None, description="List of experiences of the Profile."
+    )
+    educations: Optional[List[Education]] = Field(
+        None, description="List of educations of the Profile."
+    )
+    attachments: List = Field(
+        ..., description="List of documents attached to the Profile."
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Profile."
+    )
+    languages: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of spoken languages of the profile"
+    )
+    certifications: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of certifications of the Profile."
+    )
+    courses: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of courses of the Profile."
+    )
+    tasks: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of tasks of the Profile."
+    )
+    interests: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of interests of the Profile."
+    )
+    labels: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of labels of the Profile."
+    )
+    tags: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of tags of the Profile."
+    )
+    metadatas: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of metadatas of the Profile."
+    )
diff --git a/src/hrflow_connectors/connectors/flatchr/actions.py b/src/hrflow_connectors/connectors/flatchr/actions.py
index 5326eb9c7..bd21dbc4b 100644
--- a/src/hrflow_connectors/connectors/flatchr/actions.py
+++ b/src/hrflow_connectors/connectors/flatchr/actions.py
@@ -4,10 +4,12 @@
 import requests
 
 from ...core.auth import AuthorizationAuth
-
+from ...core.error import PushError, ConnectorError
 from ...core.action import PushProfileBaseAction
 from ...utils.hrflow import generate_workflow_response
 from ...utils.logger import get_logger
+from ...utils.schemas import HrflowProfile
+from .schemas import FlatchrEnrichmentProfile, FlatchrCreationProfile
 
 logger = get_logger()
 
@@ -23,11 +25,12 @@ class PushProfileAction(PushProfileBaseAction):
         description="The id of the company",
     )
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: HrflowProfile):
 
         ######################
         ### Create Profile ###
         ######################
+        data = data.dict()
 
         def get_candidate_attachments(hrflow_profile):
             attachments_list = hrflow_profile.get("attachments")
@@ -48,8 +51,9 @@ def get_candidate_attachments(hrflow_profile):
         info = data.get("info")
         email = info.get("email") if info else None
         if email == None:
-            raise Exception(
-                f"No email for hrflow_profile {data.get('reference')} but one is mandatory"
+            reference = data.get("reference")
+            raise ConnectorError(
+                f"No email for hrflow_profile {reference} but one is mandatory"
             )
 
         create_profile_body = {
@@ -159,7 +163,7 @@ def get_name():
         def get_phone(hrflow_profile):
             info = hrflow_profile.get("info")
             phone = [
-                {"dialNumber": info.get("phone") if info else None, "useCode": None}
+                {"dialNumber": info.get("phone") if info else "XXXXX", "useCode": None}
             ]
             return phone
 
@@ -219,15 +223,15 @@ def get_employment_positions(hrflow_profile):
         # When the action needs to send several requests to push a profile
         # We group the formats of the different requests in a `profile_body_dict`.
         profile_body_dict = dict(
-            create_profile_body=create_profile_body,
-            enrich_profile_body=enrich_profile_body,
+            create_profile_body=FlatchrCreationProfile.parse_obj(create_profile_body),
+            enrich_profile_body=FlatchrEnrichmentProfile.parse_obj(enrich_profile_body),
         )
         return profile_body_dict
 
     def push(self, data):
-        profile_body_dict = next(data)
-        create_profile_body = profile_body_dict["create_profile_body"]
-        enrich_profile_body = profile_body_dict["enrich_profile_body"]
+        profile_body_obj = next(data)
+        create_profile_body = profile_body_obj["create_profile_body"]
+        enrich_profile_body = profile_body_obj["enrich_profile_body"]
 
         session = requests.Session()
 
@@ -238,7 +242,7 @@ def push(self, data):
         create_profile_request.url = "http://careers.flatchr.io/vacancy/candidate/json"
 
         create_profile_request.auth = self.auth
-        create_profile_request.json = create_profile_body
+        create_profile_request.json = create_profile_body.dict()
         prepared_create_profile_request = create_profile_request.prepare()
 
         # Prepare enrich request
@@ -249,18 +253,16 @@ def push(self, data):
             f"http://api.flatchr.io/company/{self.company}/search/candidate"
         )
         enrich_profile_request.auth = self.auth
-        enrich_profile_request.json = enrich_profile_body
+        enrich_profile_request.json = enrich_profile_body.dict()
         prepared_enrich_profile_request = enrich_profile_request.prepare()
 
         # Send requests
         logger.info("Sending `create profile` request")
         response = session.send(prepared_create_profile_request)
         if not response.ok:
-            raise RuntimeError(
-                f"Create profile to Flatchr failed : `{response.content}`"
-            )
+            raise PushError(response, message="Create profile to Flatchr failed")
 
         logger.info("Sending `enrich profile` request")
         response = session.send(prepared_enrich_profile_request)
         if not response.ok:
-            raise RuntimeError(f"Enrich Flatchr profile failed : `{response.content}`")
+            raise PushError(response, message="Enrich Flatchr profile failed")
diff --git a/src/hrflow_connectors/connectors/flatchr/connector.py b/src/hrflow_connectors/connectors/flatchr/connector.py
index 822546faa..cbbb43c52 100644
--- a/src/hrflow_connectors/connectors/flatchr/connector.py
+++ b/src/hrflow_connectors/connectors/flatchr/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import AuthorizationAuth
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PushProfileAction
 
 
@@ -12,7 +12,7 @@ class Flatchr(Connector):
     def push_profile(
         auth: AuthorizationAuth,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         vacancy: str,
         company: str,
         **kwargs
@@ -28,7 +28,7 @@ def push_profile(
             profile (Profile): Profile to push
             vacancy (str): The pool in which candidates will be placed. Findable in the URL
             company (str): The id of the company
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter 
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md b/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md
index c81fb1afe..c16b93887 100644
--- a/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -31,10 +31,10 @@
 from hrflow_connectors import Flatchr
 from hrflow import Hrflow
 from hrflow_connectors import AuthorizationAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 client = Hrflow(api_secret="X-API-KEY", api_user="X-USER-EMAIL")
 auth = AuthorizationAuth(value="FLATCHR_KEY")
 Flatchr.push_profile(
diff --git a/src/hrflow_connectors/connectors/flatchr/schemas.py b/src/hrflow_connectors/connectors/flatchr/schemas.py
index 9a14f75ba..5cbec8cac 100644
--- a/src/hrflow_connectors/connectors/flatchr/schemas.py
+++ b/src/hrflow_connectors/connectors/flatchr/schemas.py
@@ -3,181 +3,205 @@
 
 
 class FlatchrResume(BaseModel):
-    fileName: str = Field(..., Description="Name of the file")
-    contentType: str = Field(..., Description="Type of the content of the file")
-    data: str = Field(..., Description="Content of the file")
-
-
-class FlatchrAnswers(BaseModel):
-    question: Union[str, int] = Field(..., Description="Question text or tag ID")
-    value: str = Field(..., Description="Value of the answer or tag")
+    fileName: Optional[str] = Field("resume.pdf", Description="Name of the file")
+    contentType: Optional[str] = Field(
+        "application/octet-stream", Description="Type of the content of the file"
+    )
+    data: Optional[str] = Field(None, Description="Content of the file")
 
 
 class FlatchrCreationProfile(BaseModel):
-    vacancy: str = Field(..., Description="Unique identifier of the ad")
-    firstname: str = Field(..., Description="First name /[a-zA-Z]/")
-    lastname: str = Field(..., Description="Last name /[a-zA-Z]/")
-    type: str = Field("applicants", Description="'applicants' by default")
-    token: str = Field(
-        ..., Description="API key. It is created in the Flatchr interface"
-    )
+    vacancy: Optional[str] = Field(None, Description="Unique identifier of the ad")
+    firstname: Optional[str] = Field(None, Description="First name /[a-zA-Z]/")
+    lastname: Optional[str] = Field(None, Description="Last name /[a-zA-Z]/")
+    type: Optional[str] = Field("applicants", Description="'applicants' by default")
     resume: FlatchrResume = Field(
-        ...,
+        None,
         Description="Type of format for the CV: document (if cv base64, default), "
         "json (if cv, hr-xml format)",
     )
-    email: str = Field(
-        ...,
+    email: Optional[str] = Field(
+        None,
         Description="Email will be used as a reference for the profile. The enrichment request "
         "aim the right profile to enrich with the email.",
     )
-    phone: Optional[str] = Field(..., Description="Phone")
-    comment: Optional[str] = Field(..., Description="Candidate's comment")
-    offerer_id: Optional[int] = Field(..., Description="Flatchr login to ask the team")
-    legalNewsletterPartners: Optional[bool] = Field(
-        ..., Description="Opt-in newsletter"
-    )
-    similarities: Optional[bool] = Field(..., Description="Return to similar offers")
-    response_text: Optional[str] = Field(..., Description="Return text")
-    answers: Optional[FlatchrAnswers] = Field(
-        ..., Description="Answers to questions/tags"
-    )
+    phone: Optional[str] = Field("XXXXX", Description="Phone")
 
 
 class FlatchrAdressProfile(BaseModel):
-    formattedAddress: str = Field(
-        ..., Description="Adress where the education were done"
+    formattedAddress: Optional[Optional[str]] = Field(
+        None, Description="Adress where the education were done"
     )
 
 
 class FlatchrCommunicationProfile(BaseModel):
     address: List[FlatchrAdressProfile] = Field(
-        ..., Description="List of adresses where the education were done"
+        None, Description="List of adresses where the education were done"
     )
 
 
 class FlatchrInstitutionProfile(BaseModel):
     communication: FlatchrCommunicationProfile = Field(
-        ..., Description="List of adresses"
+        None, Description="List of adresses"
     )
-    name: str = Field(
-        ..., Description="Name of the institution in which the education was provided"
+    name: Optional[str] = Field(
+        None, Description="Name of the institution in which the education was provided"
     )
 
 
 class FlatchrEducationLevelCode(BaseModel):
-    name: str = Field(..., Description="Degree of the education")
+    name: Optional[str] = Field(None, Description="Degree of the education")
 
 
 class FlatchrEducationDegree(BaseModel):
-    name: str = Field(..., Description="Degree of the education")
-    date: str = Field(..., Description="Date on which the degree was obtained")
-    specializations: List = Field(..., Description="Specializations of the degree")
+    name: Optional[str] = Field(None, Description="Degree of the education")
+    date: Optional[str] = Field(
+        None, Description="Date on which the degree was obtained"
+    )
+    specializations: List = Field([], Description="Specializations of the degree")
 
 
 class FlatchrEducationProfile(BaseModel):
     institution: FlatchrInstitutionProfile = Field(
-        ..., Description="Institution object"
+        None, Description="Institution object"
     )
     educationLevelCodes: List[FlatchrEducationLevelCode] = Field(
-        ..., Description="List of all the degrees"
+        None, Description="List of all the degrees"
     )
     educationDegrees: List[FlatchrEducationDegree] = Field(
-        ..., Description="List of all the degrees"
+        None, Description="List of all the degrees"
+    )
+    end: Optional[str] = Field("XXXX", Description="Date on which the degree has ended")
+    descriptions: List[Optional[str]] = Field(
+        [None], Description="Description of the education"
     )
-    end: str = Field(..., Description="Date on which the degree has ended")
-    descriptions: List[str] = Field(..., Description="Description of the education")
 
 
 class FlatchrGeoLocation(BaseModel):
-    latitude: float = Field(..., Description="Latitude of the employment location")
-    longitude: float = Field(..., Description="Longitude of the employment location")
+    latitude: Optional[float] = Field(
+        None, Description="Latitude of the employment location"
+    )
+    longitude: Optional[float] = Field(
+        None, Description="Longitude of the employment location"
+    )
 
 
 class FlatchrAddres1(BaseModel):
-    countryCode: str = Field(..., Description="Country code of the employment location")
-    city: str = Field(..., Description="City of the employment location")
-    postalCode: str = Field(..., Description="Postalcode of the employment location")
+    countryCode: Optional[str] = Field(
+        None, Description="Country code of the employment location"
+    )
+    city: Optional[str] = Field(None, Description="City of the employment location")
+    postalCode: Optional[str] = Field(
+        None, Description="Postalcode of the employment location"
+    )
     geoLocation: FlatchrGeoLocation = Field(
-        ..., Description="GeoLocation object of the employment location"
+        None, Description="GeoLocation object of the employment location"
+    )
+    formattedAddress: Optional[str] = Field(
+        None, Description="Address of the employment location"
     )
-    formattedAddress: str = Field(..., Description="Address of the employment location")
 
 
 class FlatchrCommunication1(BaseModel):
-    address: List[FlatchrAddres1] = Field(..., Description="Address1 object")
+    address: List[FlatchrAddres1] = Field(None, Description="Address1 object")
 
 
 class FlatchrOrganization(BaseModel):
     communication: FlatchrCommunication1 = Field(
-        ..., Description="Communication1 object"
+        None, Description="Communication1 object"
     )
-    name: str = Field(..., Description="Address of the employment")
+    name: Optional[str] = Field(None, Description="Address of the employment")
 
 
 class FlatchrJobCategory(BaseModel):
-    name: str = Field(..., Description="Category of the job")
+    name: Optional[str] = Field(None, Description="Category of the job")
 
 
 class FlatchrPositionHistory(BaseModel):
-    organization: FlatchrOrganization = Field(..., Description="Organization object")
+    organization: FlatchrOrganization = Field(None, Description="Organization object")
     jobCategories: List[FlatchrJobCategory] = Field(
-        ..., Description="Category of the job"
+        [], Description="Category of the job"
     )
     jobLevels: List = Field([], Description="[]")
-    start: str = Field(..., Description="Date of the start of the employment")
-    current: bool = Field(..., Description="Is this employment the current employment")
+    start: Optional[str] = Field(
+        None, Description="Date of the start of the employment"
+    )
+    current: Optional[bool] = Field(
+        None, Description="Is this employment the current employment"
+    )
 
 
 class FlatchrEmploymentProfile(BaseModel):
-    title: str = Field(..., Description="Title of the employment")
+    title: Optional[str] = Field(None, Description="Title of the employment")
     positionHistories: List[FlatchrPositionHistory] = Field(
-        ..., Description="List of the position histories object"
+        None, Description="List of the position histories object"
+    )
+    start: Optional[str] = Field(
+        None, Description="Date of the start of the employment"
+    )
+    current: bool = Field(None, Description="Is this employment the current employment")
+    descriptions: List[Optional[str]] = Field(
+        [None], Description="Description of the experience"
     )
-    start: str = Field(..., Description="Date of the start of the employment")
-    current: bool = Field(..., Description="Is this employment the current employment")
-    descriptions: List[str] = Field(..., Description="Description of the experience")
 
 
 class FlatchrNameProfile(BaseModel):
-    formattedName: str
-    given: str
-    family: str
+    formattedName: Optional[str] = Field("Undefined")
+    given: Optional[str] = Field("Undefined")
+    family: Optional[str] = Field("Undefined")
 
 
 class FlatchrPhoneProfile(BaseModel):
-    dialNumber: str = Field(..., Description="Number of the phone of the candidat")
-    useCode: str = Field(..., Description="What kind of phone it is")
+    dialNumber: Optional[str] = Field(
+        "XXXXX", Description="Number of the phone of the candidat"
+    )
+    useCode: Optional[str] = Field(None, Description="What kind of phone it is")
 
 
 class FlatchrEmailProfile(BaseModel):
-    address: str = Field(..., Description="Email adress of the candidat")
+    address: Optional[str] = Field(None, Description="Email adress of the candidat")
+
+
+class AddressValue(BaseModel):
+    administrative_area_level_1: Optional[str]
+    location_lat: Optional[str]
+    location_lng: Optional[str]
 
 
 class FlatchrValueProfile(BaseModel):
-    education: List[FlatchrEducationProfile] = Field(
-        ..., Description="List of educations"
+    education: Optional[List[FlatchrEducationProfile]] = Field(
+        None, Description="List of educations"
+    )
+    employment: Optional[List[FlatchrEmploymentProfile]] = Field(
+        None, Description="List of employments"
+    )
+    name: Optional[FlatchrNameProfile] = Field(None, Description="Name of the candidat")
+    phone: Optional[List[FlatchrPhoneProfile]] = Field(
+        "XXXXX", Description="Phone of the candidat"
     )
-    employment: List[FlatchrEmploymentProfile] = Field(
-        ..., Description="List of employments"
+    email: Optional[List[FlatchrEmailProfile]] = Field(
+        None, Description="Email adress of the candidat"
     )
-    name: FlatchrNameProfile = Field(..., Description="Name of the candidat")
-    phone: List[FlatchrPhoneProfile] = Field(..., Description="Phone of the candidat")
-    email: List[FlatchrEmailProfile] = Field(
-        ..., Description="Email adress of the candidat"
+    position: Optional[List[Optional[str]]] = Field(
+        None, Description="Position of the candidat"
     )
-    position: List[str] = Field(..., Description="Position of the candidat")
-    employment_positions: List[str] = Field(
-        ..., Description="Employment adress of the candidat"
+    employment_positions: Optional[List[Optional[str]]] = Field(
+        None, Description="Employment adress of the candidat"
+    )
+    experience: Optional[int] = Field(
+        None, Description="Years of experience of the candidat"
     )
-    experience: int = Field(..., Description="Years of experience of the candidat")
 
 
 class FlatchrEnrichmentProfile(BaseModel):
-    app_name: str = Field("HRMatch", Description="App name, 'HRMatch' by default")
-    reference: str = Field(
-        ..., Description="Reference of the profile which is the email adress"
+    app_name: Optional[str] = Field(
+        "HRMatch", Description="App name, 'HRMatch' by default"
+    )
+    reference: Optional[str] = Field(
+        None, Description="Reference of the profile which is the email adress"
+    )
+    name: Optional[str] = Field("parsing", Description="'Parsing' by default")
+    type: Optional[str] = Field("applicants", Description="'applicants by default'")
+    value: Optional[FlatchrValueProfile] = Field(
+        None, Description="Content of the profile"
     )
-    name: str = Field("parsing", Description="'Parsing' by default")
-    type: str = Field("applicants", Description="'applicants by default'")
-    value: FlatchrValueProfile = Field(..., Description="Content of the profile")
diff --git a/src/hrflow_connectors/connectors/greenhouse/actions.py b/src/hrflow_connectors/connectors/greenhouse/actions.py
index 11a5ac836..a51b999fe 100644
--- a/src/hrflow_connectors/connectors/greenhouse/actions.py
+++ b/src/hrflow_connectors/connectors/greenhouse/actions.py
@@ -3,11 +3,14 @@
 import html
 import requests
 
+from ...core.error import PullError, PushError
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
 from ...utils.hrflow import generate_workflow_response
 from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import GreenhouseJobModel, GreenhouseProfileModel
 
 logger = get_logger()
 
@@ -18,12 +21,12 @@ class PullJobsAction(PullJobsBaseAction):
         description="Job Board URL token, which is usually the company `name` -for example `lyft`- when it has job listings on greenhouse, mandatory to access job boards on `greenhouse.io`: `https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs`, getting jobs doesn't require an API Key",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[GreenhouseJobModel]:
         """
         Pull all jobs from a greenhouse job board
 
         Returns:
-            Iterator[Dict[str, Any]]: list of all jobs with their content if available
+            Iterator[GreenhouseJobModel]: list of all jobs with their content if available
         """
         # Prepare request
         session = requests.Session()
@@ -37,28 +40,30 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(
-                f"Failed to get jobs from board: `{self.board_token}`. Check that your board token is valid."
+            raise PullError(
+                response,
+                message="Failed to get jobs from Greenhouse board. Check that your board token is valid.",
+                board_token=self.board_token,
             )
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
 
         response_dict = response.json()
         total_info = response_dict["meta"]["total"]
         logger.info(f"Total jobs found : {total_info}")
 
-        job_list = response_dict["jobs"]
-        return job_list
+        job_json_list = response_dict["jobs"]
+        job_obj_iter = map(GreenhouseJobModel.parse_obj, job_json_list)
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: GreenhouseJobModel) -> HrflowJob:
         """
         format each job pulled from greenhouse job board into a HrFlow job object
 
         Returns:
-            Dict[str, Any]: job in the HrFlow job object format
+            HrflowJob: job in the HrFlow job object format
         """
 
         job = dict()
+        data = data.dict()
         # name
         job["name"] = data.get("title")
         # summary
@@ -116,8 +121,9 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         ]
         # updated_at
         job["updated_at"] = data.get("updated_at")
+        job_obj = HrflowJob.parse_obj(job)
 
-        return job
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -132,15 +138,16 @@ class PushProfileAction(PushProfileBaseAction):
         description="The ID of the user sending the profile, or the person he is sending the profile on behalf of",
     )
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: HrflowProfile) -> GreenhouseProfileModel:
         """
         Format a profile hrflow object to a greenhouse profile object
         Args:
-            profile (Dict[str, Any]): profile object in the hrflow profile format
+            profile (HrflowProfile): profile object in the hrflow profile format
         Returns:
-            Dict[str, Any]: profile in the greenhouse candidate  format
+            GreenhouseProfileModel: profile in the greenhouse candidate  format
         """
         profile = dict()
+        data = data.dict()
         profile["applications"] = []
         for id in self.job_id:
             profile["applications"].append(dict(job_id=id))
@@ -170,8 +177,9 @@ def get_social_media_urls():
             urls = data["info"]["urls"]
             website_list = []
             for url in urls:
-                if url["url"] not in ["", None, []]:
-                    website_list.append(dict(value=url["url"]))
+                if isinstance(url, dict):
+                    if url["url"] not in ["", None, []]:
+                        website_list.append(dict(value=url["url"]))
             return website_list
 
         if get_social_media_urls() not in [[], None]:
@@ -196,14 +204,15 @@ def get_social_media_urls():
                             end_date=experience["date_end"],
                         )
                     )
+        profile_obj = GreenhouseProfileModel.parse_obj(profile)
 
-        return profile
+        return profile_obj
 
-    def push(self, data: Dict[str, Any]):
+    def push(self, data: GreenhouseProfileModel):
         """
         Push profile
         Args:
-            data (Dict[str, Any]): Profile
+            data (GreenhouseProfileModel): Profile
         """
 
         profile = next(data)
@@ -215,15 +224,11 @@ def push(self, data: Dict[str, Any]):
         push_profile_request.url = "https://harvest.greenhouse.io/v1/candidates"
         push_profile_request.auth = self.auth
         push_profile_request.headers = {"on-behalf-of": self.on_behalf_of}
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_request = push_profile_request.prepare()
 
         # Send request
         response = session.send(prepared_request)
 
         if not response.ok:
-            raise RuntimeError(
-                "Push profile to Greenhouse failed : {}, `{}`".format(
-                    response.status_code, response.content
-                )
-            )
+            raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/greenhouse/connector.py b/src/hrflow_connectors/connectors/greenhouse/connector.py
index 73ccc03a5..72472a941 100644
--- a/src/hrflow_connectors/connectors/greenhouse/connector.py
+++ b/src/hrflow_connectors/connectors/greenhouse/connector.py
@@ -4,7 +4,7 @@
 from ...core.connector import Connector
 from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
 
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -22,7 +22,7 @@ def pull_jobs(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             board_key (str): Board key where the jobs to be added will be stored
             board_token (str): Job Board URL token, which is usually the company `name` -for example `lyft`- when it has job listings on greenhouse, mandatory to access job boards on `greenhouse.io` `https//boards-api.greenhouse.io/v1/boards/{board_token}/jobs`, getting jobs doesn't require an API Key
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -46,7 +46,7 @@ def push_profile(
         job_id: List[int],
         on_behalf_of: str,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
         """
@@ -60,7 +60,7 @@ def push_profile(
             profile (Profile): Profile to push
             job_id (List[int]): List of jobs internal ids to which the candidate should be added
             on_behalf_of (str): The ID of the user sending the profile, or the person he is sending the profile on behalf of
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md b/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
index 1b586cfe8..e2160bc99 100644
--- a/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md b/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md
index 0645fd999..e02feb5b1 100644
--- a/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -32,11 +32,11 @@
 from hrflow_connectors import Greenhouse
 from hrflow import Hrflow
 from hrflow_connectors.connectors import OAuth2PasswordCredentialsBody, AuthorizationAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = AuthorizationAuth(
     name = 'Authorization',
     value= 'MY_AUTHORIZATION',
diff --git a/src/hrflow_connectors/connectors/greenhouse/schemas.py b/src/hrflow_connectors/connectors/greenhouse/schemas.py
index 472cd60ea..220441523 100644
--- a/src/hrflow_connectors/connectors/greenhouse/schemas.py
+++ b/src/hrflow_connectors/connectors/greenhouse/schemas.py
@@ -85,49 +85,48 @@ class Recruiter(BaseModel):
 
 
 class coordinator(BaseModel):
-    id: Optional[int]
-    email: Optional[str]
+    id: int
+    email: str
 
 
 class GreenhouseProfileModel(BaseModel):
     first_name: str = Field(..., description="The candidate's first name")
     last_name: str = Field(..., description="The candidate's last name")
-    company: Optional[str] = Field(..., description="The candidate's company'")
-    title: Optional[str] = Field(..., description="The candidate's title'")
+    company: Optional[str] = Field(None, description="The candidate's company'")
+    title: Optional[str] = Field(None, description="The candidate's title'")
     phone_numbers: Optional[List[PhoneNumber]] = Field(
-        ..., description="Array of phone numbers. Passing an empty array will clear all"
+        None,
+        description="Array of phone numbers. Passing an empty array will clear all",
     )
     addresses: Optional[List[Address]] = Field(
-        ..., description="Array of addresses, passing an empty array will clear all"
+        None, description="Array of addresses, passing an empty array will clear all"
     )
     email_addresses: Optional[List[EmailAddress]] = Field(
-        ..., description="Array of email addresses, passing an empty array will"
+        None, description="Array of email addresses, passing an empty array will"
     )
     website_addresses: Optional[List[WebsiteAddress]] = Field(
-        ...,
+        None,
         description="Array of website addresses, passing an empty array will clear all",
     )
     social_media_addresses: Optional[List[SocialMediaAddress]] = Field(
-        ...,
+        None,
         description="Array of social media addresses. Passing an empty array will clear all",
     )
     educations: Optional[List[Education]] = Field(
-        ..., description="Array of education records"
+        None, description="Array of education records"
     )
     employments: Optional[List[Employment]] = Field(
-        ..., description="Array of employment records"
+        None, description="Array of employment records"
     )
     tags: Optional[List[str]] = Field(
-        ...,
+        None,
         description="Array of tags as strings. Passing an empty array will clear all",
     )
     applications: List[Application] = Field(
-        ...,
+        None,
         description="An array of application objects `dict(job_id=int)`. at least one is required",
     )
     recruiter: Optional[Recruiter] = Field(
-        ..., description="An object representing the candidate's recruiter"
-    )
-    coordinator: Optional[coordinator] = Field(
-        ..., description="An object representing the candidate's coordinator"
+        None, description="An object representing the candidate's recruiter"
     )
+    coordinator: Optional[coordinator]
diff --git a/src/hrflow_connectors/connectors/monster/actions.py b/src/hrflow_connectors/connectors/monster/actions.py
index fef0a9d9e..72b3d5029 100644
--- a/src/hrflow_connectors/connectors/monster/actions.py
+++ b/src/hrflow_connectors/connectors/monster/actions.py
@@ -3,8 +3,10 @@
 from typing import Union, Dict, Any
 import requests
 
+from ...core.error import PushError
 from ...core.action import PushJobBaseAction, CatchProfileBaseAction
 from ...utils.logger import get_logger
+from ...utils.schemas import HrflowJob
 
 
 logger = get_logger()
@@ -42,7 +44,7 @@ class PushJobAction(PushJobBaseAction):
         "`https://my_subdomain.my.monster.com:8443/bgwBroker`",
     )
 
-    def format(self, data: Dict[str, Any]) -> bytes:
+    def format(self, data: HrflowJob) -> bytes:
 
         xml_job_str = """<?xml version="1.0" encoding="UTF-8"?>
 <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
@@ -269,7 +271,7 @@ def format_credentials(formater, hrflow_job):
         formater = dict()
 
         for function in creation_pipeline:
-            function(formater, data)
+            function(formater, data.dict())
 
         job = xml_job_str.format(**formater)
         return job.encode("utf-8")
@@ -290,5 +292,4 @@ def push(self, data):
         # Send request
         response = session.send(prepared_request)
         if not response.ok:
-            error_message = "Unable to push the data ! Reason : `{}`"
-            raise RuntimeError(error_message.format(response.content))
+            raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/monster/connector.py b/src/hrflow_connectors/connectors/monster/connector.py
index caf30ee27..0fbec91ff 100644
--- a/src/hrflow_connectors/connectors/monster/connector.py
+++ b/src/hrflow_connectors/connectors/monster/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import MonsterBodyAuth
-from ...utils.hrflow import Job
+from ...utils.schemas import HrflowJob
 from .actions import CatchProfileAction, PushJobAction
 
 
@@ -21,7 +21,7 @@ def catch_profile(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             source_key (str): Source key where the profiles to be added will be stored
             request (Dict[str, Any]): Body to format in HrFlow Profile
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -39,7 +39,7 @@ def catch_profile(
 
     @staticmethod
     def push_job(
-        auth: MonsterBodyAuth, hrflow_client: Hrflow, subdomain: str, job: Job, **kwargs
+        auth: MonsterBodyAuth, hrflow_client: Hrflow, subdomain: str, job: HrflowJob, **kwargs
     ) -> Optional[Dict[str, Any]]:
         """
         `PushJobAction` pushes a Monster job to `Hrflow.ai`.
@@ -50,7 +50,7 @@ def push_job(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             job (Job): Job to push
             subdomain (str): Subdomain monster just before `monster.com`. For example subdomain=`my_subdomain.my` in `https//my_subdomain.my.monster.com8443/bgwBroker`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/monster/docs/catch_profile.md b/src/hrflow_connectors/connectors/monster/docs/catch_profile.md
index 06b1800a0..050090e1a 100644
--- a/src/hrflow_connectors/connectors/monster/docs/catch_profile.md
+++ b/src/hrflow_connectors/connectors/monster/docs/catch_profile.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/recruitee/actions.py b/src/hrflow_connectors/connectors/recruitee/actions.py
index 88f4d1423..e6da9bbc4 100644
--- a/src/hrflow_connectors/connectors/recruitee/actions.py
+++ b/src/hrflow_connectors/connectors/recruitee/actions.py
@@ -2,11 +2,14 @@
 from pydantic import Field
 import requests
 
+from ...core.error import PullError, PushError
 from ...core.auth import AuthorizationAuth
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...utils.hrflow import generate_workflow_response
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import RecruiteeCandidateModel, RecruiteJobModel
 
 logger = get_logger()
 
@@ -17,14 +20,12 @@ class PullJobsAction(PullJobsBaseAction):
         ..., description="the subdomain of your company's careers site."
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[RecruiteJobModel]:
         """
         pull all jobs from a recruitee subdomain endpoint
 
-        Raises:
-            ConnectionError: if the request failed, you may want to check your subdomain
         Returns:
-            Iterator[Dict[str, Any]]: a list of jobs dictionaries
+            Iterator[RecruiteeJobModel]: a list of jobs dictionaries
         """
         # Prepare request
         session = requests.Session()
@@ -38,27 +39,27 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(
-                f"Failed to get jobs for company: {self.subdomain}, Check that the subdomain is a valid one"
-            )
-            error_message = "Unable to pull the data ! Reason: `{}`, `{}`"
-            raise ConnectionError(
-                error_message.format(response.status_code, response.content)
+            raise PullError(
+                response,
+                message="Failed to get jobs for company. Check that the subdomain is a valid one.",
+                subdomain=self.subdomain,
             )
         response_dict = response.json()
         job_list = response_dict["offers"]
-        return job_list
+        job_obj_iter = map(RecruiteJobModel.parse_obj, job_list)
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: RecruiteJobModel) -> HrflowJob:
         """
         format a job into the hrflow job object format
         Args:
-            data (Dict[str, Any]): a job object pulled from a recruitee company subdomain
+            data (RecruiteeJobModel): a job object pulled from a recruitee company subdomain
         Returns:
-            Dict[str, Any]: a job into the hrflow job object format
+            HrflowJob: a job into the hrflow job object format
         """
 
         job = dict()
+        data = data.dict()
         # basic information
         job["name"] = data.get("title")
         job["summary"] = data.get("slug")
@@ -115,8 +116,9 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
             dict(name="recruitee_department", value=department),
             dict(name="recruitee_apply_url", value=apply_url),
         ]
+        job_obj = HrflowJob.parse_obj(job)
 
-        return job
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -130,12 +132,13 @@ class PushProfileAction(PushProfileBaseAction):
         description="Offers to which the candidate will be assigned with default stage. You can also pass one ID as offer_id",
     )
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: HrflowProfile) -> RecruiteeCandidateModel:
         """
         format a HrFlow Profile object into a Recruitee profile Object
-        returns Dict[str, Any]: a profile in the format of Recruitee profiles
+        returns RecruiteeCandidateModel: a profile in the format of Recruitee profiles
         """
         profile = dict()
+        data = data.dict()
         info = data.get("info")
         profile["name"] = info.get("full_name")
         if data.get("attachments") not in [None, []]:
@@ -147,8 +150,9 @@ def urls():
             urls = info.get("urls")
             website_list = []
             for url in urls:
-                if url["url"] not in ["", None, []]:
-                    website_list.append(url["url"])
+                if isinstance(url, dict):
+                    if url["url"] not in ["", None, []]:
+                        website_list.append(url["url"])
             return website_list
 
         if urls() not in ["", None, []]:
@@ -157,8 +161,8 @@ def urls():
         output_data = dict(candidate=profile)
         if self.offer_id is not None:
             output_data["offers"] = self.offer_id
-
-        return output_data
+        output_data_obj = RecruiteeCandidateModel.parse_obj(output_data)
+        return output_data_obj
 
     def push(self, data):
         profile = next(data)
@@ -171,13 +175,11 @@ def push(self, data):
             f"https://api.recruitee.com/c/{self.company_id}/candidates"
         )
         push_profile_request.auth = self.auth
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_request = push_profile_request.prepare()
 
         # Send request
         response = session.send(prepared_request)
 
         if not response.ok:
-            raise RuntimeError(
-                f"Push profile to Recruitee failed :`{response.status_code}` `{response.content}`"
-            )
+            raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/recruitee/connector.py b/src/hrflow_connectors/connectors/recruitee/connector.py
index 01d47a9b5..dc98311dd 100644
--- a/src/hrflow_connectors/connectors/recruitee/connector.py
+++ b/src/hrflow_connectors/connectors/recruitee/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import AuthorizationAuth
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -21,7 +21,7 @@ def pull_jobs(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             board_key (str): Board key where the jobs to be added will be stored
             subdomain (str): the subdomain of your company's careers site.
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -43,7 +43,7 @@ def pull_jobs(
     def push_profile(
         auth: AuthorizationAuth,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         company_id: str,
         offer_id: Optional[List[int]],
         **kwargs
@@ -58,7 +58,7 @@ def push_profile(
             auth (AuthorizationAuth): Auth instance to identify and communicate with the platform
             profile (Profile): Profile to push
             company_id (str): Company ID. A company subdomain can also be used.
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md b/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
index 32545293f..ca3b159ab 100644
--- a/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md b/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
index a490eb45f..c2550ca88 100644
--- a/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -33,10 +33,10 @@
 from hrflow_connectors import Recruitee
 from hrflow import Hrflow
 from hrflow_connectors import AuthorizationAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = AuthorizationAuth(
     name = 'Authorization',
     value= settings['BEARER_TOKEN'],
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py b/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py
index c335e5e8a..cb087a434 100644
--- a/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py
@@ -3,11 +3,14 @@
 import requests
 import datetime
 
+from ...core.error import PullError, PushError
 from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
 from ...utils.datetime_converter import from_str_to_datetime
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import SAPSuccessFactorsJob, SapCandidateModel
 
 logger = get_logger()
 
@@ -23,12 +26,12 @@ class PullJobsAction(PullJobsBaseAction):
         description="the API server for your company from the list of API servers for SAP SuccessFactors data centers",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[SAPSuccessFactorsJob]:
         """
         Pull all jobs from a local database that uses SAP successfactors API
 
         Returns:
-            Iterator[Dict[str, Any]]: list of all job requisitions with their content
+            Iterator[SAPSuccessFactorsJob]: list of all job requisitions with their content
         """
         # Prepare request
         session = requests.Session()
@@ -43,26 +46,26 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(f"The request failed !")
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
+            raise PullError(response)
 
         logger.info(f"All jobs have been pulled")
         response_dict = response.json()
         job_list = response_dict["d"]["results"]
-        return job_list
+        job_obj_iter = map(SAPSuccessFactorsJob.parse_obj, job_list)
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: SAPSuccessFactorsJob) -> HrflowJob:
         """
         Format retrieved jobs into a HrFlow job object
 
         Args:
-            data (Dict[str, Any]): job to format
+            data (SAPSuccessFactorsJob): job to format
 
         Returns:
-            Dict[str, Any]: job in the HrFlow job object format
+            HrflowJob: job in the HrFlow job object format
         """
         job = dict()
+        data = data.dict()
 
         jobRequisition = data.get("jobRequisition")
 
@@ -140,7 +143,8 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
                 value=jobRequisition.get("hiringManagerTeam"),
             ),
         ]
-        return job
+        job_obj = HrflowJob.parse_obj(job)
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -155,17 +159,18 @@ class PushProfileAction(PushProfileBaseAction):
         description="the API server for your company from the list of API servers for SAP SuccessFactors data centers",
     )
 
-    def format(self, profile: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, profile: HrflowProfile) -> SapCandidateModel:
         """
         Formats a hrflow profile into a sap successfactors candidate
 
         Args:
-            profile (Dict[str, Any]): profile to format
+            profile (HrflowProfile): profile to format
 
         Returns:
-            Dict[str, Any]: a SAP successfactors profile
+            SapCandidateModel: a SAP successfactors profile
         """
         sap_profile = dict()
+        profile = profile.dict()
 
         # Basic information
         info = profile.get("info")
@@ -296,8 +301,8 @@ def format_experience(experience: Dict[str, Any]) -> Dict[str, Any]:
             for experience in profile.get("experiences", []):
                 sap_experience = format_experience(experience)
                 sap_profile["outsideWorkExperience"]["results"].append(sap_experience)
-
-        return sap_profile
+        sap_profile_obj = SapCandidateModel.parse_obj(sap_profile)
+        return sap_profile_obj
 
     def push(self, data):
         profile = next(data)
@@ -308,7 +313,7 @@ def push(self, data):
         push_profile_request.method = "POST"
         push_profile_request.url = f"https://{self.api_server}/odata/v2/Candidate"
         push_profile_request.auth = self.auth
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_request = push_profile_request.prepare()
 
         # Send request
@@ -317,6 +322,4 @@ def push(self, data):
             logger.warning("Profile already exists")
         else:
             if not response.ok:
-                raise RuntimeError(
-                    f"Push profile to sapsuccesfactors api-server: {self.api_server} failed : `{response.content}`"
-                )
+                raise PushError(response, api_server=self.api_server)
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py b/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py
index 9ea9c4f9f..d42a02fdb 100644
--- a/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -27,7 +27,7 @@ def pull_jobs(
             auth (Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]): Auth instance to identify and communicate with the platform
             board_key (str): Board key where the jobs to be added will be stored
             api_server (str): the API server for your company from the list of API servers for SAP SuccessFactors data centers
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -52,7 +52,7 @@ def pull_jobs(
     def push_profile(
         auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth],
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         api_server: str,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
@@ -66,7 +66,7 @@ def push_profile(
             auth (Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]): Auth instance to identify and communicate with the platform
             profile (Profile): Profile to push
             api_server (str): the API server for your company from the list of API servers for SAP SuccessFactors data centers
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
index fba1d0130..a0f53a478 100644
--- a/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md
index 06fab6942..d16261fb9 100644
--- a/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -30,14 +30,14 @@
 from hrflow_connectors import SapSuccessfactors
 from hrflow import Hrflow
 from hrflow_connectors import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from hrflow_connectors.connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.connectors.utils.schemas import HrflowProfile
 
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = XAPIKeyAuth(
-    name = "APIKey",
-    value= settings['MY_API_KEY'],
+    name="APIKey",
+    value='MY_API_KEY',
 )
 
 SapSuccessfactors.push_profile(
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py b/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
index abd8701f8..baa87cc15 100644
--- a/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
@@ -3,7 +3,7 @@
 
 # Job model
 class SAPSuccessFactorsJobRequistion(BaseModel):
-    annual_SA: str
+    annual_SA: Optional[str]
     location: Optional[str]
     city: Optional[str] = None
     country: Optional[str] = None
@@ -25,9 +25,9 @@ class SAPSuccessFactorsJobRequistion(BaseModel):
 
 
 class SAPSuccessFactorsJob(BaseModel):
-    jobDescription: str
-    jobTitle: str
-    jobReqId: str
+    jobDescription: Optional[str]
+    jobTitle: Optional[str]
+    jobReqId: Optional[str]
     jobRequisition: SAPSuccessFactorsJobRequistion
 
 
@@ -36,10 +36,10 @@ class SAPSuccessFactorsJob(BaseModel):
 
 class Result(BaseModel):
 
-    endDate: str
+    endDate: Optional[str]
     school: str
     schoolAddress: str
-    startDate: str
+    startDate: Optional[str]
 
 
 class Education(BaseModel):
@@ -53,15 +53,11 @@ class ResultLanguage(BaseModel):
     writingProf: str
 
 
-class Languages(BaseModel):
-    results: List[ResultLanguage]
-
-
 class ResultOutsideWorkExperience(BaseModel):
-    employer: str
+    employer: Optional[str]
     employerAddress: str
-    endDate: str
-    startDate: str
+    endDate: Optional[str]
+    startDate: Optional[str]
 
 
 class OutsideWorkExperience(BaseModel):
@@ -69,26 +65,22 @@ class OutsideWorkExperience(BaseModel):
 
 
 class InsideWorkExperienceResult(BaseModel):
-    backgroundElementId: str
-    bgOrderPos: str
-    candidateId: str
-    department: str
-    endDate: str
-    lastModifiedDateTime: str
-    startDate: str
-    title: str
-    candidate: str
-
-
-class InsideWorkExperience(BaseModel):
-    results: List[InsideWorkExperienceResult]
+    backgroundElementId: Optional[str]
+    bgOrderPos: Optional[str]
+    candidateId: Optional[str]
+    department: Optional[str]
+    endDate: Optional[str]
+    lastModifiedDateTime: Optional[str]
+    startDate: Optional[str]
+    title: Optional[str]
+    candidate: Optional[str]
 
 
 class TalentPoolResults(BaseModel):
-    startDate: str
-    talentPoolComments: str
-    talentPoolStatus: str
-    talentPoolitem: str
+    startDate: Optional[str]
+    talentPoolComments: Optional[str]
+    talentPoolStatus: Optional[str]
+    talentPoolitem: Optional[str]
 
 
 class TalentPool(BaseModel):
@@ -96,24 +88,17 @@ class TalentPool(BaseModel):
 
 
 class SapCandidateModel(BaseModel):
-    address: str
+    address: Optional[str]
     cellPhone: Optional[str]
     city: Optional[str]
     contactEmail: Optional[str]
-    country: str
-    creationDateTime: str
+    country: Optional[str]
     currentTitle: Optional[str]
-    dateofAvailability: Optional[str]
-    firstName: str
+    firstName: Optional[str]
     homePhone: Optional[str]
-    lastName: str
+    lastName: Optional[str]
     middleName: Optional[str]
-    partnerMemberId: Optional[str]
-    partnerSource: Optional[str]
     primaryEmail: str
     zip: Optional[str]
-    education: Education
-    languages: Languages
+    education: Optional[Education]
     outsideWorkExperience: Optional[OutsideWorkExperience]
-    insideWorkExperience: Optional[InsideWorkExperience]
-    talentPool: Optional[TalentPool]
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/actions.py b/src/hrflow_connectors/connectors/smartrecruiters/actions.py
index 0f75544e6..0f48af58e 100644
--- a/src/hrflow_connectors/connectors/smartrecruiters/actions.py
+++ b/src/hrflow_connectors/connectors/smartrecruiters/actions.py
@@ -3,9 +3,12 @@
 import itertools
 import requests
 
+from ...core.error import PullError, PushError
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...core.auth import XSmartTokenAuth
 from ...utils.logger import get_logger
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import SmartrecruitersProfileModel, SmartRecruitersModel
 
 
 logger = get_logger()
@@ -33,12 +36,12 @@ class PullJobsAction(PullJobsBaseAction):
         description="Number of elements to return per page. max value is 100. Default value : 10",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[SmartRecruitersModel]:
         """
         Pull all jobs from SmartRecruiters
 
         Returns:
-            Iterator[Dict[str, Any]]: an iterator of jobs
+            Iterator[SmartRecruitersModel]: an iterator of jobs
         """
         # Prepare request
         session = requests.Session()
@@ -67,9 +70,11 @@ def get_page():
                 response = session.send(prepared_request)
 
                 if not response.ok:
-                    logger.error(f"Fail to get page of jobs : pageId=`{next_page_id}`")
-                    error_message = "Unable to pull the data ! Reason : `{}`"
-                    raise ConnectionError(error_message.format(response.content))
+                    raise PullError(
+                        response,
+                        message="Fail to get page of jobs",
+                        page_id=next_page_id,
+                    )
 
                 logger.info(f"Get page of jobs : pageId=`{next_page_id}`")
                 response_json = response.json()
@@ -110,32 +115,33 @@ def get_full_job(light_job_dict: Dict[str, Any]) -> Dict[str, Any]:
             get_job_request.auth = self.auth
 
             # send request
-            prepared_request = pull_jobs_request.prepare()
+            prepared_request = get_job_request.prepare()
             response = session.send(prepared_request)
 
             if not response.ok:
-                logger.error(f"Fail to get full job id=`{job_id}`")
-                error_message = f"Unable to pull the job id=`{job_id}` ! Reason : `{response.content}`"
-                raise ConnectionError(error_message)
+                raise PullError(response, message="Fail to get full job", job_id=job_id)
 
             logger.info(f"Get full job id=`{job_id}`")
+
             return response.json()
 
         chained_full_job_iter = map(get_full_job, chained_light_job_iter)
+        job_obj_iter = map(SmartRecruitersModel.parse_obj, chained_full_job_iter)
 
-        return chained_full_job_iter
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: SmartRecruitersModel) -> HrflowJob:
         """
         Format a job from SmartRecruiter job format to Hrflow job format
 
         Args:
-            data (Dict[str, Any]): Job offer
+            data (SmartRecruitersModel): Job offer
 
         Returns:
-            Dict[str, Any]: Job in the HrFlow job object format
+            HrflowJob: Job in the HrFlow job object format
         """
         job = dict()
+        data = data.dict()
 
         # job Title
         job["name"] = data.get("title", "Undefined")
@@ -196,73 +202,57 @@ def add_section(section_name: str):
         add_section("qualifications")
         add_section("additionalInformation")
 
+        job["tags"] = []
+
+        def create_tag(field_name: str, field_dict: Optional[str] = None) -> None:
+            name = "smartr_{}".format(field_name)
+            if field_dict is not None:
+                name = "smartr_{}-{}".format(field_dict, field_name)
+                if isinstance(data.get(field_dict), dict):
+                    field_value = data.get(field_dict).get(field_name)
+                    if field_value is not None:
+                        job["tags"].append(dict(name=name, value=field_value))
+            else:
+                field_value = data.get(field_name)
+                if field_value is not None:
+                    job["tags"].append(dict(name=name, value=field_value))
+
         # job tags
-        status = data.get("status")
-        posting_status = data.get("postingStatus")
-        job_id = data.get("id")
-        experience_level_id = data.get("experienceLevel", {}).get("id")
-        employmentType_id = data.get("typeOfEmployment", {}).get("id")
-        industry_id = data.get("industry", {}).get("id")
-        creator = data.get("creator", {})
-        function_id = data.get("function", {}).get("id")
-        department_id = data.get("department", {}).get("id")
-        manual = data.get("location", {}).get("manual")
-        remote = data.get("location", {}).get("remote")
-        eeo_category_id = data.get("eeoCategory", {}).get("id")
-        compensation = data.get("compensation", {})
-        job["tags"] = [
-            dict(name="smartrecruiters_status", value=status),
-            dict(name="smartrecruiters_postingStatus", value=posting_status),
-            dict(name="smartrecruiters_id", value=job_id),
-            dict(name="smartrecruiters_experienceLevel-id", value=experience_level_id),
-            dict(name="smartrecruiters_typeOfEmployment-id", value=employmentType_id),
-            dict(
-                name="smartrecruiters_compensation-min", value=compensation.get("min")
-            ),
-            dict(
-                name="smartrecruiters_compensation-max", value=compensation.get("max")
-            ),
-            dict(
-                name="smartrecruiters_compensation-currency",
-                value=compensation.get("currency"),
-            ),
-            dict(name="smartrecruiters_industry-id", value=industry_id),
-            dict(
-                name="smartrecruiters_creator-firstName", value=creator.get("firstName")
-            ),
-            dict(
-                name="smartrecruiters_creator-lastName", value=creator.get("lastName")
-            ),
-            dict(name="smartrecruiters_function-id", value=function_id),
-            dict(name="smartrecruiters_department-id", value=department_id),
-            dict(name="smartrecruiters_location-manual", value=manual),
-            dict(name="smartrecruiters_location-remote", value=remote),
-            dict(name="smartrecruiters_eeoCategory-id", value=eeo_category_id),
-            dict(
-                name="smartrecruiters_targetHiringDate",
-                value=data.get("targetHiringDate"),
-            ),
-        ]
-
-        return job
+        create_tag(field_name="status")
+        create_tag(field_name="postingStatus")
+        create_tag(field_name="compensation")
+        create_tag(field_name="id")
+        create_tag(field_name="id", field_dict="experienceLevel")
+        create_tag(field_name="id", field_dict="typeOfEmployment")
+        create_tag(field_name="id", field_dict="industry")
+        create_tag(field_name="id", field_dict="creator")
+        create_tag(field_name="id", field_dict="function")
+        create_tag(field_name="id", field_dict="department")
+        create_tag(field_name="id", field_dict="eeoCategory")
+        create_tag(field_name="manual", field_dict="location")
+        create_tag(field_name="remote", field_dict="location")
+
+        job_obj = HrflowJob.parse_obj(job)
+
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
     auth: XSmartTokenAuth
     job_id: str = Field(
         ...,
-        description="Id of a Job to which you want to assign a candidate when it’s created. A profile is sent to this URL `https://api.smartrecruiters.com/jobs/{job_id}/candidates` ",
+        description="Id of a Job to which you want to assign a candidate when it's created. A profile is sent to this URL `https://api.smartrecruiters.com/jobs/{job_id}/candidates` ",
     )
 
-    def format(self, profile: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, profile: HrflowProfile) -> SmartrecruitersProfileModel:
         """
         Format a profile hrflow object to a smartrecruiters profile object
 
         Args:
-            profile (Dict[str, Any]): [profile object in the hrflow profile format]
+            profile (HrflowProfile): [profile object in the hrflow profile format]
 
         Returns:
-            Dict[str, Any]: [profile in the SmartRecruiters candidate application format]
+            SmartRecruitersProfileModel: [profile in the SmartRecruiters candidate application format]
         """
 
         value_or_undefined = lambda s: s or "Undefined"
@@ -302,7 +292,7 @@ def format_educations(educations):
             for education_entity in educations:
                 formatted_education = format_project(education_entity)
                 if education_entity.get("school") is None:
-                    formatted_education["instituion"] = "Undefined"
+                    formatted_education["institution"] = "Undefined"
                 else:
                     formatted_education["institution"] = education_entity.get("school")
 
@@ -334,6 +324,7 @@ def format_experiences(experiences):
 
             return formatted_experience_list
 
+        profile = profile.dict()
         info = profile["info"]
         smart_candidate = dict()
         smart_candidate["firstName"] = info["first_name"]
@@ -341,7 +332,7 @@ def format_experiences(experiences):
         smart_candidate["email"] = info["email"]
         smart_candidate["phoneNumber"] = info["phone"]
 
-        if info["location"]["fields"] not in [
+        if info["location"].get("fields") not in [
             [],
             None,
         ]:  # check if fields is not an undefined list
@@ -373,15 +364,15 @@ def format_experiences(experiences):
         smart_candidate["experience"] = format_experiences(profile.get("experiences"))
         smart_candidate["attachments"] = profile.get("attachments")
         smart_candidate["consent"] = True
+        smart_candidate_obj = SmartrecruitersProfileModel.parse_obj(smart_candidate)
+        return smart_candidate_obj
 
-        return smart_candidate
-
-    def push(self, data: Dict[str, Any]):
+    def push(self, data: SmartrecruitersProfileModel):
         """
         Push profile
 
         Args:
-            data (Dict[str, Any]): Profile
+            data (SmartRecruitersProfileModel): Profile
         """
         profile = next(data)
 
@@ -393,13 +384,11 @@ def push(self, data: Dict[str, Any]):
             f"https://api.smartrecruiters.com/jobs/{self.job_id}/candidates"
         )
         push_profile_request.auth = self.auth
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_request = push_profile_request.prepare()
 
         # Send request
         response = session.send(prepared_request)
 
         if not response.ok:
-            raise RuntimeError(
-                f"Push profile to SmartRecruiters failed : `{response.content}`"
-            )
+            raise PushError(response)
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/connector.py b/src/hrflow_connectors/connectors/smartrecruiters/connector.py
index ad80bdc3c..3470dac3f 100644
--- a/src/hrflow_connectors/connectors/smartrecruiters/connector.py
+++ b/src/hrflow_connectors/connectors/smartrecruiters/connector.py
@@ -3,7 +3,7 @@
 
 from ...core.connector import Connector
 from ...core.auth import XSmartTokenAuth
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -21,7 +21,7 @@ def pull_jobs(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             auth (XSmartTokenAuth): Auth instance to identify and communicate with the platform
             board_key (str): Board key where the jobs to be added will be stored
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -45,7 +45,7 @@ def pull_jobs(
     def push_profile(
         auth: XSmartTokenAuth,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         job_id: str,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
@@ -59,7 +59,7 @@ def push_profile(
             auth (XSmartTokenAuth): Auth instance to identify and communicate with the platform
             profile (Profile): Profile to push
             job_id (str): Id of a Job to which you want to assign a candidate when it’s created. A profile is sent to this URL `https//api.smartrecruiters.com/jobs/{job_id}/candidates`
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md b/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
index 233374d93..c76ff8592 100644
--- a/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
@@ -14,7 +14,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md b/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md
index ed4f9dc05..27106ea3f 100644
--- a/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -30,11 +30,11 @@
 from hrflow_connectors import SmartRecruiters
 from hrflow import Hrflow
 from hrflow_connectors import XSmartTokenAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-auth = XSmartTokenAuth(value=settings["MY_SMART_TOKEN"])
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = XSmartTokenAuth(value="MY_SMART_TOKEN")
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 
 SmartRecruiters.push_profile(
     auth=auth,
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/schemas.py b/src/hrflow_connectors/connectors/smartrecruiters/schemas.py
index 3f9f28ca9..8d603ce14 100644
--- a/src/hrflow_connectors/connectors/smartrecruiters/schemas.py
+++ b/src/hrflow_connectors/connectors/smartrecruiters/schemas.py
@@ -1,74 +1,74 @@
 from typing import List, Optional
 
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
 
 # Job Model
 class Department(BaseModel):
-    id: str
+    id: Optional[str]
 
 
 class Location(BaseModel):
-    country: Optional[str]
-    countryCode: Optional[str]
-    regionCode: Optional[str]
-    region: Optional[str]
-    city: str
-    address: Optional[str]
-    longitude: Optional[str]
-    latitude: Optional[str]
+    country: Optional[Optional[str]]
+    countryCode: Optional[Optional[str]]
+    regionCode: Optional[Optional[str]]
+    region: Optional[Optional[str]]
+    city: Optional[str]
+    address: Optional[Optional[str]]
+    longitude: Optional[Optional[str]]
+    latitude: Optional[Optional[str]]
     remote: Optional[bool]
     manual: Optional[bool]
 
 
 class Industry(BaseModel):
-    id: str
+    id: Optional[str]
 
 
 class Function(BaseModel):
-    id: str
+    id: Optional[str]
 
 
 class TypeOfEmployment(BaseModel):
-    id: str
+    id: Optional[str]
 
 
 class ExperienceLevel(BaseModel):
-    id: Optional[str]
+    id: Optional[Optional[str]]
 
 
 class EeoCategory(BaseModel):
-    id: str
+    id: Optional[str]
 
 
 class Creator(BaseModel):
-    firstName: str
-    lastName: str
+    firstName: Optional[str]
+    lastName: Optional[str]
 
 
 class Compensation(BaseModel):
     min: int
     max: int
-    currency: str
+    currency: Optional[str]
 
 
 class CompanyDescription(BaseModel):
-    title: str
-    text: str
+    title: Optional[str]
+    text: Optional[str]
 
 
 class JobDescription(BaseModel):
-    title: str
-    text: str
+    title: Optional[str]
+    text: Optional[str]
 
 
 class Qualifications(BaseModel):
-    title: str
-    text: str
+    title: Optional[str]
+    text: Optional[str]
 
 
 class AdditionalInformation(BaseModel):
-    title: str
-    text: str
+    title: Optional[str]
+    text: Optional[str]
 
 
 class Sections(BaseModel):
@@ -84,14 +84,14 @@ class JobAd(BaseModel):
 
 class SmartRecruitersModel(BaseModel):
     title: str
-    refNumber: str
-    createdOn: str
-    updatedOn: str
+    refNumber: Optional[str]
+    createdOn: Optional[str]
+    updatedOn: Optional[str]
     department: Optional[Department]
     location: Location
-    status: Optional[str]
-    postingStatus: Optional[str]
-    targetHiringDate: Optional[str]
+    status: Optional[Optional[str]]
+    postingStatus: Optional[Optional[str]]
+    targetHiringDate: Optional[Optional[str]]
     industry: Optional[Industry]
     function: Optional[Function]
     typeOfEmployment: Optional[TypeOfEmployment]
@@ -106,51 +106,53 @@ class SmartRecruitersModel(BaseModel):
 
 
 class Location(BaseModel):
-    country: str
-    countryCode: str
-    regionCode: str
-    region: str
-    city: str
-    lat: int
-    lng: int
+    country: Optional[str] = Field("Undefined")
+    countryCode: Optional[str] = Field("NO")
+    regionCode: Optional[str] = Field("Undefined")
+    region: Optional[str] = Field("Undefined")
+    city: Optional[str] = Field("Undefined")
+    lat: Optional[int] = Field(0)
+    lng: Optional[int] = Field(0)
 
 
 class Web(BaseModel):
-    skype: str
-    linkedin: str
-    facebook: str
-    twitter: str
-    website: str
+    skype: Optional[str] = Field("Undefined")
+    linkedin: Optional[str] = Field("Undefined")
+    facebook: Optional[str] = Field("Undefined")
+    twitter: Optional[str] = Field("Undefined")
+    website: Optional[str] = Field("Undefined")
 
 
 class EducationItem(BaseModel):
-    institution: str
-    degree: str
-    major: str
-    current: bool
-    location: str
-    startDate: str
-    endDate: str
-    description: str
+    institution: Optional[str]
+    degree: Optional[str]
+    major: Optional[str]
+    current: Optional[bool]
+    location: Optional[str]
+    startDate: Optional[str]
+    endDate: Optional[str]
+    description: Optional[str]
 
 
 class ExperienceItem(BaseModel):
-    title: str
-    company: str
-    current: bool
-    startDate: str
-    endDate: str
-    location: str
-    description: str
+    title: Optional[str]
+    company: Optional[str]
+    current: Optional[bool]
+    startDate: Optional[str]
+    endDate: Optional[str]
+    location: Optional[str]
+    description: Optional[str]
 
 
 class SmartrecruitersProfileModel(BaseModel):
     firstName: str
     lastName: str
-    email: str
-    phoneNumber: str
+    email: Optional[str]
+    phoneNumber: Optional[str]
     location: Location
-    web: Web
+    web: Optional[Web]
     tags: List[str]
     education: List[EducationItem]
     experience: List[ExperienceItem]
+    consent: Optional[bool]
+    attachments: Optional[list]
diff --git a/src/hrflow_connectors/connectors/taleez/actions.py b/src/hrflow_connectors/connectors/taleez/actions.py
index 7ec582356..e0424e90a 100644
--- a/src/hrflow_connectors/connectors/taleez/actions.py
+++ b/src/hrflow_connectors/connectors/taleez/actions.py
@@ -2,12 +2,15 @@
 from pydantic import Field
 import requests
 
+from ...core.error import PullError, PushError
 from ...core.action import PullJobsBaseAction, PushProfileBaseAction
 from ...core.auth import XTaleezAuth
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
 from ...utils.hrflow import generate_workflow_response
 from datetime import datetime
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import TaleezJobModel, TaleezCandidateModel
 
 logger = get_logger()
 
@@ -19,7 +22,7 @@ class PullJobsAction(PullJobsBaseAction):
         100, description="Page size. Max size of the list returned. Max value : 100"
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[TaleezJobModel]:
         """
         Pull jobs from a Taleez jobs owner endpoint
 
@@ -38,30 +41,28 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(f"Failed to get jobs from this endpoint")
-            error_message = "Unable to pull the data ! Reason : `{}` `{}`"
-            raise RuntimeError(
-                error_message.format(response.status_code, response.content)
-            )
+            raise PullError(response, message="Failed to get jobs")
 
         response_dict = response.json()
         logger.info(f"Total found: {response_dict['listSize']}")
         job_list = response_dict["list"]
+        job_obj_iter = map(TaleezJobModel.parse_obj, job_list)
 
-        return job_list
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: TaleezJobModel) -> HrflowJob:
         """
         Format a job into the hrflow job object format
 
         Args:
-            data (Dict[str, Any]): a taleez job object form
+            data (TaleezJobModel): a taleez job object form
 
         Returns:
-            Dict[str, Any]: a hrflow job object form
+            HrflowJob: a hrflow job object form
         """
 
         job = dict()
+        data = data.dict()
 
         # Job basic information
         job["name"] = data.get("label")
@@ -131,8 +132,8 @@ def seconds_to_isoformat(seconds: int) -> str:
         # datetime fields
         job["created_at"] = seconds_to_isoformat(data.get("dateCreation"))
         job["updated_at"] = seconds_to_isoformat(data.get("dateLastPublish"))
-
-        return job
+        job_obj = HrflowJob.parse_obj(job)
+        return job_obj
 
 
 class PushProfileAction(PushProfileBaseAction):
@@ -143,13 +144,14 @@ class PushProfileAction(PushProfileBaseAction):
         None, description="ID of the job to add a candidate to"
     )
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: HrflowProfile) -> TaleezCandidateModel:
         """
         Format a Hrflow profile object into a Taleez profile object
 
-        Returns: Dict[str, Any] a Taleez profile object form
+        Returns: TaleezCandidateModel a Taleez profile object form
         """
         profile = dict()
+        data = data.dict()
         info = data.get("info")
         profile["firstName"] = info.get("first_name")
         profile["lastName"] = info.get("last_name")
@@ -180,19 +182,15 @@ def format_urls() -> None:
                         profile["socalLinks"][file_name] = public_url
 
         format_urls()
-
-        return profile
+        profile_obj = TaleezCandidateModel.parse_obj(profile)
+        return profile_obj
 
     def push(self, data):
         """
         Push a profile into a Taleez CVTheque or a Taleez job offer as a candidate
 
         Args:
-            data ([type]): a Taleez profile form
-
-        Raises:
-            RuntimeError: if profile pushes fail
-            Exception: if you want to add a candidate to a job without specifiying it
+            data (TaleezCandidateModel): a Taleez profile form
         """
         profile = next(data)
 
@@ -202,16 +200,15 @@ def push(self, data):
         push_profile_request.method = "POST"
         push_profile_request.url = f"https://api.taleez.com/0/candidates"
         push_profile_request.auth = self.auth
-        push_profile_request.json = profile
+        push_profile_request.json = profile.dict()
         prepared_push_profile_request = push_profile_request.prepare()
 
         # Send request
         push_profile_response = session.send(prepared_push_profile_request)
 
         if not push_profile_response.ok:
-            raise RuntimeError(
-                f"Push profile to Taleez failed :`{push_profile_response.status_code}` `{push_profile_response.content}`"
-            )
+            raise PushError(push_profile_response)
+
         if self.job_id is not None:
             push_profile_response_dict = push_profile_response.json()
             candidate_id = push_profile_response_dict["id"]
@@ -229,6 +226,4 @@ def push(self, data):
             add_profile_response = session.send(prepared_add_profile_request)
 
             if not add_profile_response.ok:
-                raise RuntimeError(
-                    f"Add profile to Taleez job: `{self.job_id}` failed :`{add_profile_response.status_code}` `{add_profile_response.content}`"
-                )
+                raise PushError(add_profile_response, job_id=self.job_id)
diff --git a/src/hrflow_connectors/connectors/taleez/connector.py b/src/hrflow_connectors/connectors/taleez/connector.py
index ec95b23ca..bdcc15e0f 100644
--- a/src/hrflow_connectors/connectors/taleez/connector.py
+++ b/src/hrflow_connectors/connectors/taleez/connector.py
@@ -4,7 +4,7 @@
 from ...core.connector import Connector
 from ...core.auth import XTaleezAuth
 
-from ...utils.hrflow import Profile
+from ...utils.schemas import HrflowProfile
 from .actions import PullJobsAction, PushProfileAction
 
 
@@ -22,7 +22,7 @@ def pull_jobs(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             auth (XTaleezAuth): Auth instance to identify and communicate with the platform
             board_key (str): Board key where the jobs to be added will be stored
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
@@ -43,7 +43,7 @@ def pull_jobs(
     def push_profile(
         auth: XTaleezAuth,
         hrflow_client: Hrflow,
-        profile: Profile,
+        profile: HrflowProfile,
         recruiter_id: int,
         **kwargs
     ) -> Optional[Dict[str, Any]]:
@@ -56,7 +56,7 @@ def push_profile(
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
             profile (Profile): Profile to push
             recruiter_id (int): ID of the person recruiting the candidate, mandatory
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md b/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md
index 86226e774..033672991 100644
--- a/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/connectors/taleez/docs/push_profile.md b/src/hrflow_connectors/connectors/taleez/docs/push_profile.md
index 6462916e5..503adf5b3 100644
--- a/src/hrflow_connectors/connectors/taleez/docs/push_profile.md
+++ b/src/hrflow_connectors/connectors/taleez/docs/push_profile.md
@@ -13,7 +13,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -32,10 +32,10 @@
 from hrflow_connectors import Taleez
 from hrflow import Hrflow
 from hrflow_connectors import XTaleezAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
 auth = XTaleezAuth(value='MY_X_TALEEZ_API_KEY')
 
 Taleez.push_profile(
diff --git a/src/hrflow_connectors/connectors/taleez/schemas.py b/src/hrflow_connectors/connectors/taleez/schemas.py
index 0d08c6b3d..6edb611f3 100644
--- a/src/hrflow_connectors/connectors/taleez/schemas.py
+++ b/src/hrflow_connectors/connectors/taleez/schemas.py
@@ -13,13 +13,13 @@ class TaleezJobModel(BaseModel):
         ..., description="Job Profile Type, for example administration, HR..."
     )
     currentStatus: str = Field(
-        ..., description="status of the JOb whether it is Published, Draft or suspended"
+        ..., description="status of the Job whether it is Published, Draft or suspended"
     )
     contract: str = Field(..., description="Job Employment Type")
-    contractLength: int
+    contractLength: Optional[int]
     fullTime: bool
-    workHours: int
-    qualification: str
+    workHours: Optional[int]
+    qualification: Optional[str]
     remote: bool
     country: str
     city: str
@@ -38,9 +38,9 @@ class TaleezJobModel(BaseModel):
 class TaleezCandidateModel(BaseModel):
     firstName: str
     lastName: str
-    mail: str
-    phone: str
+    mail: Optional[str]
+    phone: Optional[str]
     initialReferrer: Optional[str]
-    lang: str
+    lang: Optional[str]
     recruiterId: int
     socialLinks: Optional[Dict[str, str]]
diff --git a/src/hrflow_connectors/connectors/teamtailor/README.md b/src/hrflow_connectors/connectors/teamtailor/README.md
new file mode 100644
index 000000000..6bb6e5ff7
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/README.md
@@ -0,0 +1,9 @@
+# Recruitee Connector
+> The recruitment & employer branding ATS. The new way to attract, nurture and hire top talent. Grow faster by focusing on what matters the most — your candidates
+
+🔗 https://www.teamtailor.com/en/
+
+| Actions |
+| ------- |
+| [**Pull jobs**](docs/pull_jobs.md) |
+| [**Push profile**](docs/push_profile.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/teamtailor/__init__.py b/src/hrflow_connectors/connectors/teamtailor/__init__.py
new file mode 100644
index 000000000..c5f92bc05
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/teamtailor/actions.py b/src/hrflow_connectors/connectors/teamtailor/actions.py
new file mode 100644
index 000000000..166811f30
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/actions.py
@@ -0,0 +1,275 @@
+from typing import Iterator, Dict, Any, Optional
+from pydantic import Field
+import requests
+
+from ...core.error import PullError, PushError
+from ...core.action import PullJobsBaseAction, PushProfileBaseAction
+from ...utils.logger import get_logger
+from ...utils.clean_text import remove_html_tags
+from ...utils.hrflow import generate_workflow_response
+from ...core.auth import AuthorizationAuth
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import TeamtailorJob, TeamtailorCandidateAttribute
+
+logger = get_logger()
+
+
+class PullJobsAction(PullJobsBaseAction):
+    auth: AuthorizationAuth
+
+    def pull(self) -> Iterator[TeamtailorJob]:
+        """
+        Pull all jobs from a Teamtailor job board
+        Returns:
+            Iterator[TeamtailorJob]: list of all jobs with their content if available
+        """
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = "https://api.teamtailor.com/v1/jobs"
+        pull_jobs_request.auth = self.auth
+        pull_jobs_request.headers = {"X-Api-Version": "20210218"}
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+
+        if not response.ok:
+            raise PullError(
+                response,
+                message="Failed to get jobs from Teamtailor.",
+            )
+
+        response_dict = response.json()
+
+        job_list = response_dict["data"]
+        job_obj_list = map(TeamtailorJob.parse_obj, job_list)
+        return job_obj_list
+
+    def format(self, data: TeamtailorJob) -> HrflowJob:
+        """
+        Format a Teamtailor job object into a Hrflow job object
+
+        Args:
+            data (TeamtailorJob): Teamtailor job object from the list of jobs pulled
+
+        Returns:
+            HrflowJob: a job in the HrFlow job object form
+        """
+        job = dict()
+        data = data.dict()
+        attribute = data.get("attributes")
+        job["name"] = attribute.get("title")
+        job["reference"] = data.get("id")
+        job["summary"] = attribute.get("pitch")
+        job["created_at"] = attribute.get("created-at")
+        job["updated_at"] = attribute.get("updated-at")
+        job["url"] = data.get("links").get("careersite-job-url")
+
+        # get location request
+        job["location"] = dict(text=None, lat=None, lng=None)
+
+        def get_location() -> None:
+            """
+            Get_location sends a request to get the job location from its API endpoint
+            """
+            session = requests.Session()
+            pull_job_location_request = requests.Request()
+            pull_job_location_request.method = "GET"
+            id = job["reference"]
+            pull_job_location_request.url = (
+                f"https://api.teamtailor.com/v1/jobs/{id}/location"
+            )
+
+            pull_job_location_request.headers = {"X-Api-Version": "20210218"}
+            pull_job_location_request.auth = self.auth
+            prepared_request = pull_job_location_request.prepare()
+            response = session.send(prepared_request)
+            if not response.ok:
+                raise PullError(
+                    response,
+                    message="Failed to get job location from Teamtailor.",
+                )
+            location = response.json().get("data")
+            if location is not None:
+                location_attribute = location.get("attributes")
+                text = location_attribute["address"]
+                city = location_attribute["city"]
+                country = location_attribute["country"]
+                headquarters = location_attribute["headquarters"]
+                lat = location_attribute["lat"]
+                lng = location_attribute["long"]
+                zip = location_attribute["zip"]
+                name = location_attribute["name"]
+                geojson = dict(
+                    city=city,
+                    country=country,
+                    headquarters=headquarters,
+                    zip=zip,
+                    name=name,
+                )
+                job["location"] = dict(text=text, geojson=geojson, lat=lat, lng=lng)
+
+        get_location()
+
+        # sections
+        description = remove_html_tags(attribute.get("body"))
+        job["sections"] = [
+            dict(
+                name="teamtailor_description",
+                title="teamtailor_description",
+                description=description,
+            )
+        ]
+        # tags
+        job["tags"] = []
+
+        def create_tag(field_name: str) -> None:
+            tag_name = "teamtailor_{}".format(field_name)
+            tag_value = attribute.get(field_name)
+
+            if tag_value is not None:
+                tag = dict(name=tag_name, value=tag_value)
+                job["tags"].append(tag)
+
+        create_tag("start-date")
+        create_tag("end-date")
+        create_tag("status")
+        create_tag("employment-type")
+        create_tag("employment-level")
+        create_tag("remote-status")
+        create_tag("salary-time-unit")
+        create_tag("min-salary")
+        create_tag("max-salary")
+        create_tag("currency")
+        create_tag("internal")
+
+        job_obj = HrflowJob.parse_obj(job)
+        return job_obj
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    auth: AuthorizationAuth
+    sourced: Optional[bool] = Field(
+        False, description="True if added by a recruiter without applying"
+    )
+
+    def format(self, data: HrflowProfile) -> TeamtailorCandidateAttribute:
+        """
+        Format a Hrflow profile object into a Teamtailor profile object
+
+        Args:
+            data (HrflowProfile): Hrflow Profile to format
+
+        Returns:
+            Dict[str, Any]: a Teamtailor formatted profile object
+        """
+        profile = dict()
+        data = data.dict()
+        info = data.get("info")
+        profile["first-name"] = info.get("first_name")
+        profile["last-name"] = info.get("last_name")
+        profile["email"] = info.get("email")
+        profile["phone"] = info.get("phone")
+        profile["pitch"] = info.get("summary")
+        resume = data.get("attachments")[1]
+        profile["resume"] = resume.get("public_url")
+        profile["sourced"] = self.sourced
+        profile["tags"] = data.get("tags")
+        profile_obj = TeamtailorCandidateAttribute.parse_obj(profile)
+
+        return profile_obj
+
+    def push(self, data: TeamtailorCandidateAttribute):
+        """
+        Push a Hrflow profile object to a Teamtailor candidate pool for a position
+
+        Args:
+            data (TeamtailorCandidateAttribute): profile to push
+        """
+        profile = next(data)
+        profile = profile.dict()
+        profile["first-name"] = profile.pop("first_name")
+        profile["last-name"] = profile.pop("last_name")
+        profile_json = dict(data=dict(type="candidates", attributes=profile))
+        auth = self.auth
+        request_url = "https://api.teamtailor.com/v1/candidates"
+
+        session = requests.Session()
+
+        def send_request(
+            method: str,
+            url: str,
+            error_message: str,
+            params: Optional[Dict[str, Any]] = None,
+            json: Optional[Dict[str, Any]] = None,
+        ) -> requests.Response:
+            """
+            Send a HTTPS request to the specified url using the specified paramters
+            Args:
+                method (str): request method: "GET", "PUT", "POST"...
+                url (str): url endpoint to receive the request
+                error_message (str): message to be displayed when a `PushError` error is raised
+                json (optional): data to be sent to the endpoint. Defaults to None.
+                params (optional): additional parameters to the request
+                return_response (optional): In case we want to the function to return the response. Defaults to None.
+            Returns:
+                Response (requests.Response)
+            """
+            request = requests.Request()
+            request.method = method
+            request.url = url
+            request.auth = auth
+            request.headers = {
+                "X-Api-Version": "20210218",
+                "content-type": "application/vnd.api+json",
+            }
+            if json is not None:
+                request.json = json
+            if params is not None:
+                request.params = params
+            prepared_request = request.prepare()
+            response = session.send(prepared_request)
+            if not response.ok:
+                raise PushError(response, message=error_message)
+
+            return response
+
+        # a request to verify if the candidate profile already exist
+        get_candidate_param = {"filter[email]": f'{profile.get("email")}'}
+        get_candidate_response = send_request(
+            method="GET",
+            url=request_url,
+            params=get_candidate_param,
+            error_message="Failed to get candidate",
+        )
+        candidate_list = get_candidate_response.json().get("data")
+        candidate = None
+        if candidate_list != []:
+            candidate = candidate_list[0]
+
+        # In case the candidate exists we retrieve his id to update his profile with a "PUT" request
+        if candidate is not None:
+            candidate_id = candidate["id"]
+            logger.info(f"Candidate Already exists with the id {candidate_id}")
+            update_profile_url = request_url + f"/{candidate_id}"
+            profile_json["data"]["id"] = candidate_id
+            logger.info("Updating Candidate profile")
+            send_request(
+                method="PATCH",
+                url=update_profile_url,
+                json=profile_json,
+                error_message="Failed to update candidate profile",
+            )
+        # If the candidate doesn't already exist we "POST" his profile
+        else:
+            # Post profile request
+            logger.info("Preparing request to push candidate profile")
+            send_request(
+                method="POST",
+                url=request_url,
+                json=profile_json,
+                error_message="Failed to post candidate profile",
+            )
diff --git a/src/hrflow_connectors/connectors/teamtailor/connector.py b/src/hrflow_connectors/connectors/teamtailor/connector.py
new file mode 100644
index 000000000..c3fb02a67
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/connector.py
@@ -0,0 +1,58 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import AuthorizationAuth
+from ...utils.schemas import HrflowProfile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Teamtailor(Connector):
+    @staticmethod
+    def pull_jobs(
+        auth: AuthorizationAuth, hrflow_client: Hrflow, board_key: str, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs listed on Teamtailor company endpoints. It adds all these jobs to a Hrflow.ai Board.
+        `Teamtailor` -> `Hrflow.ai`
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (AuthorizationAuth): Auth instance to identify and communicate with the platform
+            board_key (str): Board key where the jobs to be added will be stored
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
+            global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
+            local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
+            format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
+            hydrate_with_parsing (bool, optional): Enrich the job with parsing. Default value `False`
+            archive_deleted_jobs_from_stream (bool, optional): Archive Board jobs when they are no longer in the incoming job stream. Default value `True`
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PullJobsAction(
+            auth=auth, hrflow_client=hrflow_client, board_key=board_key, **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: AuthorizationAuth, hrflow_client: Hrflow, profile: HrflowProfile, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Teamtailor` via their Teamtailor API.
+        `Hrflow.ai` -> `Teamtailor`
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (AuthorizationAuth): Auth instance to identify and communicate with the platform
+            profile (Profile): Profile to push
+            sourced (Optional[bool]): True if added by a recruiter without applying. Default value `False`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
+            global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
+            local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
+            format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth, hrflow_client=hrflow_client, profile=profile, **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/teamtailor/docs/pull_jobs.md b/src/hrflow_connectors/connectors/teamtailor/docs/pull_jobs.md
new file mode 100644
index 000000000..54c685bb7
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/docs/pull_jobs.md
@@ -0,0 +1,47 @@
+# Pull jobs
+
+`Teamtailor` :arrow_right: `Hrflow.ai`
+
+`PullJobsAction` gets all available jobs listed on ***Teamtailor company endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+
+**Links to Teamtailor documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [List Jobs](https://docs.teamtailor.com/#a0351972-8394-4646-95f0-56a4c4e3886d) | Endpoint to retrieve a list of your company jobs, the request method is `GET`|
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
+| `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
+| `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
+| `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
+| `hrflow_client` :red_circle: | `hrflow.Hrflow` | Hrflow client instance used to communicate with the Hrflow.ai API        |
+| `auth` :red_circle: | `AuthorizationAuth`| Auth instance to identify and communicate with the platform        |
+| `board_key` :red_circle: | `str` | Board key where the jobs to be added will be stored        |
+| `hydrate_with_parsing`  | `bool` | Enrich the job with parsing. Default value : `False`        |
+| `archive_deleted_jobs_from_stream`  | `bool` | Archive Board jobs when they are no longer in the incoming job stream. Default value : `True`        |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Teamtailor
+from hrflow_connectors import AuthorizationAuth
+from hrflow import Hrflow
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+auth = AuthorizationAuth(
+    name = 'Authorization',
+    value= 'MY_AUTHORIZATION',
+)
+
+action = Teamtailor.PullJobs(
+    hrflow_client=client,
+    board_key="MY_BOARD_KEY",
+    hydrate_with_parsing=True,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/teamtailor/docs/push_profile.md b/src/hrflow_connectors/connectors/teamtailor/docs/push_profile.md
new file mode 100644
index 000000000..ce447826a
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/docs/push_profile.md
@@ -0,0 +1,52 @@
+# Push profile
+
+`Hrflow.ai` :arrow_right: `Teamtailor`
+
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***Teamtailor*** candidate pool.
+
+**Links to Teamtailor documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Push profile](https://docs.teamtailor.com/#8c5ab123-1a53-4e3b-959f-e3d6d6224d71) | Endpoint to create candidate using Teamtailor API, the request method is `POST` |
+| [Get Profile](https://docs.teamtailor.com/#1e591a5a-99cc-4d17-aaeb-7744dde0dbb2) | Endpoint to get a candidate ID if his profile already exists, the request method is `GET` |
+| [Update Profile](https://docs.teamtailor.com/#28b9f6cb-72f0-45fa-8f3c-a2004b54e609) | Endpoint to update a candidate profile if it already exists, the request method is `PATCH` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
+| `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
+| `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
+| `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
+| `hrflow_client` :red_circle: | `hrflow.Hrflow` | Hrflow client instance used to communicate with the Hrflow.ai API        |
+| `profile` :red_circle: | `Profile` | Profile to push        |
+| `auth` :red_circle: | `AuthorizationAuth` | Auth instance to identify and communicate with the platform        |
+| `sourced` | `Optional[bool]` | True if added by a recruiter without applying. Default value `False` |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Teamtailor
+from hrflow import Hrflow
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors.utils.schemas import HrflowProfile
+
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
+auth = AuthorizationAuth(
+    name = 'Authorization',
+    value= 'MY_AUTHORIZATION',
+)
+
+response = Teamtailor.push_profile(
+    auth=auth,
+    hrflow_client=client,
+    profile=profile,
+)
+return response
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/teamtailor/schemas.py b/src/hrflow_connectors/connectors/teamtailor/schemas.py
new file mode 100644
index 000000000..de41c89e7
--- /dev/null
+++ b/src/hrflow_connectors/connectors/teamtailor/schemas.py
@@ -0,0 +1,39 @@
+from typing import Optional, List, Any
+from pydantic import BaseModel, Field
+
+
+class TeamtailorJobAttribute(BaseModel):
+    title: str
+    pitch: Optional[str]
+    body: str = Field(..., description="job description")
+    created_at: str = Field(..., alias="created-at")
+    updated_at: Optional[str] = Field(..., alias="updated-at")
+    status: Optional[str]
+    tags: List[str]
+    remote_status: Optional[str] = Field(..., alias="remote-status")
+    currency: Optional[str]
+    salary_time_unit: Optional[str] = Field(None, alias="salary-time-unit")
+    min_salary: Optional[int] = Field(..., alias="min-salary")
+    max_salary: Optional[int] = Field(..., alias="max-salary")
+    employment_type: Optional[str] = Field(None, alias="employment-type")
+    employment_level: Optional[str] = Field(None, alias="employment-level")
+    internal: Optional[bool]
+    start_date: Optional[str] = Field(None, alias="start-date")
+    end_date: Optional[str] = Field(None, alias="end-date")
+
+
+class TeamtailorJob(BaseModel):
+    attributes: TeamtailorJobAttribute
+    links: Optional[dict]
+    id: Any
+
+
+class TeamtailorCandidateAttribute(BaseModel):
+    first_name: str = Field(..., alias="first-name")
+    last_name: str = Field(..., alias="last-name")
+    email: str
+    phone: Optional[str]
+    pitch: Optional[str] = Field(None, description="summary")
+    resume: Optional[str] = Field(None, description="candidate profile resume")
+    sourced: Optional[bool]
+    tags: Optional[List]
diff --git a/src/hrflow_connectors/connectors/workable/README.md b/src/hrflow_connectors/connectors/workable/README.md
index d5b34f4a2..0c8e52abd 100644
--- a/src/hrflow_connectors/connectors/workable/README.md
+++ b/src/hrflow_connectors/connectors/workable/README.md
@@ -5,4 +5,5 @@
 
 | Actions |
 | ------- |
-| [**Pull jobs**](docs/pull_jobs.md) |
\ No newline at end of file
+| [**Pull jobs**](docs/pull_jobs.md) |
+| [**Push Profile**](docs/pull_jobs.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/workable/actions.py b/src/hrflow_connectors/connectors/workable/actions.py
index e5160922a..375808a65 100644
--- a/src/hrflow_connectors/connectors/workable/actions.py
+++ b/src/hrflow_connectors/connectors/workable/actions.py
@@ -1,105 +1,198 @@
-from typing import Iterator, Dict, Any
+from typing import Iterator, Union, Optional
 from pydantic import Field
 import requests
 
-from ...core.action import PullJobsBaseAction
+from ...core.error import PullError
+from ...core.action import PullJobsBaseAction, PushProfileBaseAction
+from ...core.auth import AuthorizationAuth, OAuth2PasswordCredentialsBody
 from ...utils.logger import get_logger
 from ...utils.clean_text import remove_html_tags
+from ...utils.schemas import HrflowJob, HrflowProfile
+from .schemas import WorkableJobModel, WorkableCandidate
 
 logger = get_logger()
 
 
 class PullJobsAction(PullJobsBaseAction):
 
+    auth: Union[AuthorizationAuth, OAuth2PasswordCredentialsBody]
     subdomain: str = Field(
         ...,
-        description="subdomain of a company endpoint in `https://www.workable.com/api/accounts/{subdomain}` for example subdomain=`eurostar` for eurostar company",
+        description="subdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs` for example subdomain=`eurostar` for eurostar company",
     )
 
-    def pull(self) -> Iterator[Dict[str, Any]]:
+    def pull(self) -> Iterator[WorkableJobModel]:
         """
         pull all jobs from a workable public endpoint jobs stream
         Returns:
-            Iterator[Dict[str, Any]]: a list of jobs dictionaries
+            Iterator[WorkableJobModel]: a list of workable job models
         """
 
         # Prepare request
         session = requests.Session()
         pull_jobs_request = requests.Request()
         pull_jobs_request.method = "GET"
-        pull_jobs_request.url = (
-            f"https://www.workable.com/api/accounts/{self.subdomain}?details=True"
-        )
+        pull_jobs_request.auth = self.auth
+        pull_jobs_request.url = f"https://{self.subdomain}.workable.com/spi/v3/jobs"
+        pull_jobs_request.params = {
+            "include_fields": ["description, requirements, benefits, employment_type"]
+        }
         prepared_request = pull_jobs_request.prepare()
 
         # Send Request
         response = session.send(prepared_request)
 
         if not response.ok:
-            logger.error(
-                f"Failed to get jobs from subdomain: {self.subdomain}. Check that the subdomain is a valid one"
+            raise PullError(
+                response,
+                message="Failed to get jobs. Check that the subdomain is a valid one",
+                subdomain=self.subdomain,
             )
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
 
         response_dict = response.json()
 
         job_list = response_dict["jobs"]
-        return job_list
+        job_obj_iter = map(WorkableJobModel.parse_obj, job_list)
+        return job_obj_iter
 
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+    def format(self, data: WorkableJobModel) -> HrflowJob:
         """
-        format a job into the hrflow job object format
+        Format a job into the hrflow job object format
         Args:
-            data (Dict[str, Any]): a job object pulled from workable subdomain
+            data (WorkableJobModel): a job object pulled from workable subdomain
         Returns:
-            Dict[str, Any]: a job into the hrflow job object format
+            HrflowJob: a job into the hrflow job object format
         """
         job = dict()
+        data = data.dict()
         # name and reference
         job["name"] = data.get("title")
         job["reference"] = data.get("shortcode")
-        # location
-        country = data.get("country")
-        state = data.get("state")
-        city = data.get("city")
-        geojson = dict(country=country, state=state)
-        job["location"] = dict(
-            text=city,
-            lat=None,
-            lng=None,
-            geojson=geojson,
-        )
         # url
         job["url"] = data.get("url")
+        # location
+        location = data.get("location")
+        location_str = location.get("location_str")
+        text = None
+        if isinstance(location_str, str):
+            text = location_str
+        geojson = dict()
+
+        def get_geojson(field_name: str):
+            if location.get(field_name) is not None:
+                geojson["field_name"] = location.get(field_name)
+
+        get_geojson("country")
+        get_geojson("country_code")
+        get_geojson("region_code")
+        get_geojson("region")
+        get_geojson("city")
+        get_geojson("zip_code")
+        get_geojson("telecommuting")
+        job["location"] = dict(text=text, geojson=geojson)
         # sections
-        description = remove_html_tags(data.get("description"))
-        job["sections"] = [
-            dict(
-                name="workable_description",
-                title="workable_description",
-                description=description,
-            )
-        ]
+        job["sections"] = []
+
+        def create_section(field_name: str):
+            title_name = "workable_{}".format(field_name)
+            field_value = data.get(field_name)
+            if isinstance(field_value, str):
+                description = remove_html_tags(field_value)
+                section = dict(
+                    name=title_name, title=title_name, description=description
+                )
+                job["sections"].append(section)
+
+        create_section("description")
+        create_section("requirements")
+        create_section("benefits")
         # creation_date
         job["created_at"] = data.get("created_at")
         # tags
-        employment_type = data.get("employment_type")
-        industry = data.get("industry")
-        function = data.get("function")
-        experience = data.get("experience")
-        education = data.get("education")
-        application_url = data.get("application_url")
-        department = data.get("department")
-        job["tags"] = [
-            dict(name="workable_employment_type", value=employment_type),
-            dict(name="workable_indsutry", value=industry),
-            dict(name="workable_function", value=function),
-            dict(name="workable_experience", value=experience),
-            dict(name="workable_education", value=education),
-            dict(name="workable_application_url", value=application_url),
-            dict(name="workable_department", value=department),
-        ]
-        job["metadatas"] = []
-
-        return job
+        job["tags"] = []
+
+        def create_tag(field_name):
+            name = "workable_{}".format(field_name)
+            field_value = data.get("field_name")
+            if field_value is not None:
+                tag = dict(name=name, value=field_value)
+                job["tags"].append(tag)
+
+        create_tag("employment_type")
+        create_tag("full_title")
+        create_tag("id")
+        create_tag("code")
+        create_tag("state")
+        create_tag("department")
+        create_tag("application_url")
+        create_tag("shortlink")
+        create_tag("employment_type")
+
+        job_obj = HrflowJob.parse_obj(job)
+
+        return job_obj
+
+
+class PushProfileAction(PushProfileBaseAction):
+    auth: Union[AuthorizationAuth, OAuth2PasswordCredentialsBody]
+    subdomain: str = Field(
+        ...,
+        description="subdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs` for example subdomain=`eurostar` for eurostar company",
+    )
+    shortcode: str = Field(..., description="The job's shortcode")
+
+    def format(self, data: HrflowProfile) -> WorkableCandidate:
+        """
+        Format a HrflowProfile object into a WorkableCandidate object
+
+        Args:
+            data (HrflowProfile): HrflowProfile object
+
+        Returns:
+            WorkableCandidate: WorkableCandidate object
+        """
+        data = data.dict()
+        info = data.get("info")
+        profile = dict()
+        profile["name"] = info.get("full_name")
+        profile["summary"] = info.get("summary")
+        profile["email"] = info.get("email")
+        profile["phone"] = info.get("phone")
+        location = info.get("location")
+        if isinstance(location.get("text"), str):
+            profile["address"] = location.get("text")
+        attachments = data.get("attachments")
+        if isinstance(attachments, list):
+            for attachment in attachments:
+                if isinstance(attachment, dict):
+                    if attachment["type"] == "resume":
+                        profile["resume_url"] = attachment["public_url"]
+        candidate_profile = dict(sourced=True, candidate=profile)
+
+        candidate_profile_obj = WorkableCandidate.parse_obj(candidate_profile)
+        return candidate_profile_obj
+
+    def push(self, data: WorkableCandidate):
+        """
+        Push a HrflowProfile formatted to a WorkableCandidate object to a company workable endpoint
+        """
+        profile = next(data)
+        profile = profile.dict()
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        push_profile_request.method = "POST"
+        push_profile_request.auth = self.auth
+        push_profile_request.url = f"https://{self.subdomain}.workable.com/spi/v3/jobs/{self.shortcode}/candidates"
+        push_profile_request.json = profile
+        prepared_request = push_profile_request.prepare()
+
+        # Send Request
+        response = session.send(prepared_request)
+
+        if not response.ok:
+            raise PullError(
+                response,
+                message="Failed to Push profile.",
+            )
diff --git a/src/hrflow_connectors/connectors/workable/connector.py b/src/hrflow_connectors/connectors/workable/connector.py
index 37f887169..d3c95f6c0 100644
--- a/src/hrflow_connectors/connectors/workable/connector.py
+++ b/src/hrflow_connectors/connectors/workable/connector.py
@@ -1,14 +1,19 @@
 from hrflow import Hrflow
-from typing import Optional, Dict, Any
-
+from typing import Optional, Dict, Any, Union
+from ...core.auth import AuthorizationAuth, OAuth2PasswordCredentialsBody
 from ...core.connector import Connector
-from .actions import PullJobsAction
+from .actions import PullJobsAction, PushProfileAction
+from ...utils.schemas import HrflowProfile
 
 
 class Workable(Connector):
     @staticmethod
     def pull_jobs(
-        hrflow_client: Hrflow, board_key: str, subdomain: str, **kwargs
+        hrflow_client: Hrflow,
+        board_key: str,
+        auth: Union[AuthorizationAuth, OAuth2PasswordCredentialsBody],
+        subdomain: str,
+        **kwargs,
     ) -> Optional[Dict[str, Any]]:
         """
         `PullJobsAction` gets all available jobs listed on Workable public endpoints. It adds all these jobs to a Hrflow.ai Board.
@@ -17,16 +22,15 @@ def pull_jobs(
 
         Args:
             hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
-            auth (XTaleezAuth): Auth instance to identify and communicate with the platform
+            auth (Union[AuthorizationAuth, OAuth2PasswordCredentialsBody]): Auth instance to identify and communicate with the platform
             board_key (str): Board key where the jobs to be added will be stored
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
             hydrate_with_parsing (bool, optional): Enrich the job with parsing. Default value `False`
             archive_deleted_jobs_from_stream (bool, optional): Archive Board jobs when they are no longer in the incoming job stream. Default value `True`
-            page (int, optional): Page number. Start at '0'. Default value `0`
-            page_size (int, optional): Page size. Max size of the list returned. Max value  100. Default value `100`
+            subdomain (str): subdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs`.
 
         Returns:
             Optional[Dict[str, Any]]: Workflow response or `None`
@@ -35,7 +39,44 @@ def pull_jobs(
         action = PullJobsAction(
             hrflow_client=hrflow_client,
             board_key=board_key,
+            auth=auth,
+            subdomain=subdomain,
+            **kwargs,
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: Union[AuthorizationAuth, OAuth2PasswordCredentialsBody],
+        hrflow_client: Hrflow,
+        profile: HrflowProfile,
+        subdomain,
+        shortcode,
+        **kwargs,
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Workable` via their Workable API.
+        `Hrflow.ai` -> `Workable`
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (AuthorizationAuth): Auth instance to identify and communicate with the platform
+            profile (Profile): Profile to push
+            sourced (Optional[bool]): True if added by a recruiter without applying. Default value `False`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
+            global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
+            local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
+            format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
+            subdomain (str): subdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs`.
+            shortcode: (str): shortcode of the job you want to source the candidate for, job shortcodes can be found in hrflow job references
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            profile=profile,
             subdomain=subdomain,
-            **kwargs
+            shortcode=shortcode,
+            **kwargs,
         )
         return action.execute()
diff --git a/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md b/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
index 73ffdb272..925d1f478 100644
--- a/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
@@ -2,19 +2,19 @@
 
 `Workable` :arrow_right: `Hrflow.ai`
 
-`PullJobsAction` gets all available jobs listed on ***Workable public endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs listed on a ***Workable account private endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
 **Links to Workable documentation on the endpoints used :**
 
 | Endpoints | Description |
 | --------- | ----------- |
-| [subdomain](https://workable.readme.io/docs/jobs-1) | Endpoint to get a collection of the public jobs for an account subdomain, the request method is `GET` |
+| [Pull Jobs](https://workable.readme.io/docs/jobs) | Endpoint to get a collection of the private jobs for an account subdomain, the request method is `GET` |
 
 ## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
@@ -22,7 +22,8 @@
 | `board_key` :red_circle: | `str` | Board key where the jobs to be added will be stored        |
 | `hydrate_with_parsing`  | `bool` | Enrich the job with parsing. Default value : `False`        |
 | `archive_deleted_jobs_from_stream`  | `bool` | Archive Board jobs when they are no longer in the incoming job stream. Default value : `True`        |
-| `subdomain` :red_circle: | `str` | Companies have jobs listed on workable public endpoints, subdomain of a company endpoint in `https://www.workable.com/api/accounts/{subdomain}` for example subdomain=`eurostar` for ***Eurostar*** company      |
+| `auth` :red_circle: | `Union[AuthorizationAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
+| `subdomain` :red_circle: | `str` | subdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs` for example subdomain=`eurostar` for eurostar company      |
 
 :red_circle: : *required* 
 
@@ -30,13 +31,16 @@
 
 ```python
 from hrflow_connectors import Workable
+from hrflow_connectors import AuthorizationAuth
 from hrflow import Hrflow
 
+auth = AuthorizationAuth(value="MY_BEARER_TOKEN")
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
 
 Workable.pull_jobs(
     subdomain="MY_SUBDOMAIN",
     hrflow_client=client,
+    auth=auth,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
diff --git a/src/hrflow_connectors/connectors/workable/docs/push_profile.md b/src/hrflow_connectors/connectors/workable/docs/push_profile.md
new file mode 100644
index 000000000..ee2a94e84
--- /dev/null
+++ b/src/hrflow_connectors/connectors/workable/docs/push_profile.md
@@ -0,0 +1,52 @@
+# Push profile
+
+`Hrflow.ai` :arrow_right: `Workable`
+
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***Workable*** company endpoint and a optionally a Jobs pool.
+
+**Links to Workable documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Create Candidate](https://workable.readme.io/docs/job-candidates-create) | Create a candidate with provided details,a job shortcode is required the request method is `POST`|
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
+| `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
+| `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
+| `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
+| `hrflow_client` :red_circle: | `hrflow.Hrflow` | Hrflow client instance used to communicate with the Hrflow.ai API        |
+| `board_key` :red_circle: | `str` | Board key where the jobs to be added will be stored        |
+| `hydrate_with_parsing`  | `bool` | Enrich the job with parsing. Default value : `False`        |
+| `archive_deleted_jobs_from_stream`  | `bool` | Archive Board jobs when they are no longer in the incoming job stream. Default value : `True`        |
+| `auth` :red_circle: | `Union[AuthorizationAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
+| `subdomain` :red_circle: | `str` | cubdomain of a company endpoint in `https://{self.subdomain}.workable.com/spi/v3/jobs` for example subdomain=`eurostar` for eurostar company     |
+| `shortcode` | `str` | Offer to which the candidate will be assigned.|
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Workable
+from hrflow import Hrflow
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors.utils.schemas import HrflowProfile
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = HrflowProfile(key="PROFILE_KEY", source=dict(key="SOURCE_KEY"))
+auth = AuthorizationAuth(
+    value= "MY_BEARER_TOKEN",
+)
+
+Workable.push_profile(
+    subdomain="MY_SUBDOMAIN",
+    shortcode="MY_JOB_SHORTCODE",
+    auth=auth,
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/workable/schemas.py b/src/hrflow_connectors/connectors/workable/schemas.py
index d464cd4e7..6c328e1f1 100644
--- a/src/hrflow_connectors/connectors/workable/schemas.py
+++ b/src/hrflow_connectors/connectors/workable/schemas.py
@@ -1,20 +1,49 @@
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
+from typing import Optional
+
+
+class Location(BaseModel):
+    location_str: Optional[str]
+    country: Optional[str]
+    country_code: Optional[str]
+    region: Optional[str]
+    region_code: Optional[str]
+    city: Optional[str]
+    zip_code: Optional[str]
+    telecommuting: Optional[bool]
 
 
 class WorkableJobModel(BaseModel):
+    id: Optional[str]
     title: str
+    full_title: str
     shortcode: str
-    country: str
-    state: str
-    city: str
-    department: str
-    published_on: str
-    url: str
-    application_url: str
+    code: Optional[str]
+    state: Optional[str]
+    department: Optional[str]
+    url: Optional[str]
+    application_url: Optional[str]
+    shortlink: Optional[str]
+    location: Location
     created_at: str
-    description: str
-    employment_type: str
-    industry: str
-    function: str
-    experience: str
-    education: str
+    description: Optional[str]
+    requirements: Optional[str]
+    benefit: Optional[str]
+    employment_type: Optional[str]
+
+
+class Profile(BaseModel):
+    name: str
+    summary: Optional[str]
+    address: Optional[str]
+    headline: Optional[str] = Field(
+        None, description="One line description as provided by the candidate or you"
+    )
+    phone: Optional[str]
+    email: str
+    resume_url: Optional[str]
+
+
+class WorkableCandidate(BaseModel):
+    sourced: Optional[bool] = True
+    candidate: Profile
diff --git a/src/hrflow_connectors/connectors/xml/actions.py b/src/hrflow_connectors/connectors/xml/actions.py
index 7ff4c36dc..9ac345b22 100644
--- a/src/hrflow_connectors/connectors/xml/actions.py
+++ b/src/hrflow_connectors/connectors/xml/actions.py
@@ -3,6 +3,7 @@
 from typing import Iterator
 import requests
 
+from ...core.error import PullError
 from ...core.action import PullJobsBaseAction
 
 
@@ -25,11 +26,11 @@ def pull(self) -> Iterator[xml.etree.ElementTree.Element]:
         response = session.send(prepared_request)
 
         if not response.ok:
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
+            raise PullError(response)
 
         xml_stream = response.content
         root_element = xml.etree.ElementTree.fromstring(xml_stream)
         job_list_element = root_element.find(self.job_list_xpath)
         job_list = list(job_list_element)
+
         return job_list
diff --git a/src/hrflow_connectors/connectors/xml/connector.py b/src/hrflow_connectors/connectors/xml/connector.py
index 205c06944..c9b591727 100644
--- a/src/hrflow_connectors/connectors/xml/connector.py
+++ b/src/hrflow_connectors/connectors/xml/connector.py
@@ -24,7 +24,7 @@ def pull_jobs(
             board_key (str): Board key where the jobs to be added will be stored
             xml_stream_url (str): URL to XML Stream
             job_list_xpath (str): XPath pointing to the job list in the XML stream
-            logics (List[str], optional): Function names to apply as filter before pushing the data. Default value `[]`
+            logics (List[str], optional): Function names to apply as filter . Default value `[]`
             global_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's global variables. Default value `None`
             local_scope (Optional[Dict[str, Any]], optional): A dictionary containing the current scope's local variables. Default value `None`
             format_function_name (Optional[str], optional): Function name to format job before pushing. Default value `None`
diff --git a/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md b/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
index 60a886f3f..ca6a77dc5 100644
--- a/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
+++ b/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
@@ -7,7 +7,7 @@
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. Default value : `[]`        |
+| `logics`  | `List[str]` | Function names to apply as filter . Default value : `[]`        |
 | `local_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's local variables. Default value : `None`        |
 | `global_scope`  | `Optional[Dict[str, Any]]` | A dictionary containing the current scope's global variables. Default value : `None`       |
 | `format_function_name`  | `Optional[str]` | Function name to format job before pushing. Default value : `None`        |
diff --git a/src/hrflow_connectors/core/action.py b/src/hrflow_connectors/core/action.py
index fa738ff0f..4edfbcab2 100644
--- a/src/hrflow_connectors/core/action.py
+++ b/src/hrflow_connectors/core/action.py
@@ -1,17 +1,26 @@
 from pydantic import BaseModel, Field
-from typing import List, Dict, Any, Iterator, TypeVar, Optional, Union, Tuple
+from typing import List, Dict, Any, Iterator, TypeVar, Optional, Union
 import itertools
 import xml.etree.ElementTree
 import html
 
+from ..core.error import HrflowError
 from ..utils.clean_text import remove_html_tags
-from ..utils.hrflow import find_element_in_list, Profile, Job
+from ..utils.hrflow import find_element_in_list
 from ..utils.hrflow import generate_workflow_response
 from ..utils.logger import get_logger
 from ..core.auth import Auth, NoAuth
+from ..utils.schemas import HrflowJob, HrflowProfile
 
 Hrflow = TypeVar("Hrflow")
-TalentDataType = Union[str, xml.etree.ElementTree.Element, Dict[str, Any]]
+TalentDataType = Union[
+    str,
+    xml.etree.ElementTree.Element,
+    Dict[str, Any],
+    HrflowJob,
+    HrflowProfile,
+    BaseModel,
+]
 
 logger = get_logger()
 
@@ -32,7 +41,8 @@ class BaseAction(BaseModel):
     )
 
     logics: List[str] = Field(
-        [], description="Function names to apply as filter before pushing the data"
+        [],
+        description="Function names to apply as filter that return a modified item if a condition is fullfilled, if not they remove the item from the list and return None ",
     )
     global_scope: Optional[Dict[str, Any]] = Field(
         None, description="A dictionary containing the current scope's global variables"
@@ -89,10 +99,11 @@ def apply_logics(self, data: Iterator[TalentDataType]) -> Iterator[TalentDataTyp
             logger.info(
                 f"The logic function named `{logic_function_name}` has been evaluated"
             )
-            filtered_list = filter(logic_function, filtered_list)
+            mapped_list = map(logic_function, filtered_list)
+            filtered_list = filter(lambda item: item is not None, mapped_list)
         return filtered_list
 
-    def format_switcher(self, data: TalentDataType) -> Dict[str, Any]:
+    def format_switcher(self, data: TalentDataType) -> TalentDataType:
         """
         Choose the right function to format the data and format the input data into a push-ready data schema.
 
@@ -131,7 +142,7 @@ def format_switcher(self, data: TalentDataType) -> Dict[str, Any]:
             )
             return format_function(data)
 
-    def format(self, data: TalentDataType) -> Dict[str, Any]:
+    def format(self, data: TalentDataType) -> TalentDataType:
         """
         Format the input data into a push-ready data schema
 
@@ -148,7 +159,7 @@ def format(self, data: TalentDataType) -> Dict[str, Any]:
         """
         return data
 
-    def push(self, data: Iterator[Union[str, Dict[str, Any]]]):
+    def push(self, data: Iterator[TalentDataType]):
         """
         Push data
 
@@ -265,9 +276,7 @@ def get_jobs_page(page: int) -> Dict[str, Any]:
                 board_keys=[self.board_key], limit=30, page=page
             )
             if response["code"] >= 400:
-                raise RuntimeError(
-                    "Hrflow searching failed : `{}`".format(response["message"])
-                )
+                raise HrflowError(response, "Search Jobs failed")
             return response
 
         job_page = get_jobs_page(page=1)
@@ -333,21 +342,19 @@ def none_filter(data: Optional[str]) -> bool:
         logger.info("Iterator containing all references is ready !")
         return clean_iter
 
-    def push(self, data: Iterator[Union[str, Dict[str, Any]]]):
+    def push(self, data: Iterator[TalentDataType]):
         for job in data:
-            reference = job.get("reference")
+            reference = job.reference
             logger.debug(
                 f"Pushing a job ref=`{reference}` to Hrflow Board `{self.board_key}`"
             )
             response = self.hrflow_client.job.indexing.add_json(
-                board_key=self.board_key, job_json=job
+                board_key=self.board_key, job_json=job.dict()
             )
             if response["code"] >= 400:
-                message = response["message"]
-                logger.error("Failed to push a job !")
-                raise RuntimeError("Failed to push ! Reason : `{}`".format(message))
+                raise HrflowError(response, "Index Job failed")
 
-    def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
+    def hydrate_job_with_parsing(self, job_obj: TalentDataType) -> TalentDataType:
         """
         Hydrate job with parsing
 
@@ -359,6 +366,7 @@ def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
         Returns:
             Dict[str, Any]: hydrated job
         """
+        job = job_obj.dict()
         reference = job.get("reference")
         logger.debug(f"Hydrating the current job with parsing... (ref=`{reference}`)")
 
@@ -386,14 +394,13 @@ def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
         # If text is empty, the parsing can return an error
         if cleaned_str == "":
             logger.debug("Cleaned text is empty")
-            return job
+            return HrflowJob.parse_obj(job)
 
         # Parse the `cleaned`
         logger.debug("Parsing the cleaned text...")
         response = self.hrflow_client.document.parsing.post(text=cleaned_str)
         if response["code"] >= 400:
-            logger.error("Fail to parse the cleaned text !")
-            raise RuntimeError("Parsing failed : `{}`".format(response["message"]))
+            raise HrflowError(response, "Parsing failed")
         logger.debug("Text has been parsed")
 
         entity_list = response["data"]["ents"]
@@ -466,9 +473,10 @@ def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
                     )
 
         logger.debug("The job has been enriched !")
-        return job
+        job_obj = HrflowJob.parse_obj(job)
+        return job_obj
 
-    def check_reference_in_board(self, job: Dict[str, Any]) -> bool:
+    def check_reference_in_board(self, job: TalentDataType) -> bool:
         """
         Check if a job reference is in the Board.
         If the job reference is not in the Board, return `True` to add the job.
@@ -480,7 +488,7 @@ def check_reference_in_board(self, job: Dict[str, Any]) -> bool:
         Returns:
             bool: Job is in the Board
         """
-        reference = job.get("reference")
+        reference = job.reference
         logger.debug(
             f"Checking if the reference `{reference}` is already in Hrflow Board `{self.board_key}`"
         )
@@ -499,8 +507,7 @@ def check_reference_in_board(self, job: Dict[str, Any]) -> bool:
             logger.debug("Hrflow does not find this job reference")
             return True
         elif response_code >= 400:
-            logger.error("GET Indexing failed !")
-            raise RuntimeError("GET Indexing failed : {}".format(response["message"]))
+            raise HrflowError(response, "Get Job failed")
         elif response_code == 200:
             logger.debug("Hrflow got this job in the Board")
             job_in_board = response["data"]
@@ -533,7 +540,7 @@ def check_reference_in_board(self, job: Dict[str, Any]) -> bool:
                     # Edit job
                     logger.debug(f"Editing the job key=`{job_key}`")
                     edit_response = self.hrflow_client.job.indexing.edit(
-                        board_key=self.board_key, key=job_key, job_json=job
+                        board_key=self.board_key, key=job_key, job_json=job.dict()
                     )
                     if edit_response["code"] >= 400:
                         error_message = edit_response["message"]
@@ -562,7 +569,7 @@ def get_all_references_from_stream(self) -> Iterator[str]:
         filtered_data = self.apply_logics(formatted_data)
 
         logger.info(f"Keeping only reference from the stream")
-        references_iter = map(lambda job: job.get("reference"), filtered_data)
+        references_iter = map(lambda job: job.reference, filtered_data)
 
         references_without_none_iter = filter(
             lambda ref: ref is not None, references_iter
@@ -640,7 +647,7 @@ def execute(self) -> Optional[Dict[str, Any]]:
 
 class PushJobBaseAction(PushBaseAction):
 
-    job: Job = Field(..., description="Job to push")
+    job: HrflowJob = Field(..., description="Job to push")
 
     def pull(self) -> Iterator[Dict[str, Any]]:
         """
@@ -650,11 +657,10 @@ def pull(self) -> Iterator[Dict[str, Any]]:
             board_key=self.job.board.key, key=self.job.key
         )
         if response["code"] >= 400:
-            raise RuntimeError(
-                "Indexing job get failed : `{}`".format(response["message"])
-            )
+            raise HrflowError(response, "Get Job failed")
 
         job = response["data"]
+        job = HrflowJob.parse_obj(job)
         return [job]
 
     def execute(self):
@@ -665,7 +671,7 @@ def execute(self):
 
 
 class PushProfileBaseAction(PushBaseAction):
-    profile: Profile = Field(..., description="Profile to push")
+    profile: HrflowProfile = Field(..., description="Profile to push")
 
     def pull(self) -> Iterator[TalentDataType]:
         """
@@ -678,13 +684,11 @@ def pull(self) -> Iterator[TalentDataType]:
             source_key=self.profile.source.key, key=self.profile.key
         )
         if response["code"] >= 400:
-            logger.error("Fail to pull a profile")
-            raise RuntimeError(
-                "Indexing profile get failed : `{}`".format(response["message"])
-            )
+            raise HrflowError(response, "Get Profile failed")
 
         profile = response["data"]
-        return [profile]
+        profile_obj = HrflowProfile.parse_obj(profile)
+        return [profile_obj]
 
     def execute(self):
         super().execute()
@@ -724,6 +728,4 @@ def push(self, data: Dict[str, Any]):
         logger.debug(f"Parsing a profile to Hrflow Source `{self.source_key}`")
         response = self.hrflow_client.profile.parsing.add_file(**data)
         if response["code"] >= 400:
-            message = response["message"]
-            logger.error("Failed to push a profile !")
-            raise RuntimeError("Failed to push ! Reason : `{}`".format(message))
+            raise HrflowError(response, "Parsing a profile failed")
diff --git a/src/hrflow_connectors/core/auth.py b/src/hrflow_connectors/core/auth.py
index a75551d1b..40f13cbb9 100644
--- a/src/hrflow_connectors/core/auth.py
+++ b/src/hrflow_connectors/core/auth.py
@@ -1,6 +1,9 @@
 import requests
-from typing import Union, Dict, Optional, Any
+import urllib.parse
+from typing import Union, Dict, Any
 from pydantic import BaseModel, Field, SecretStr
+
+from ..core.error import AuthError
 from ..utils.logger import get_logger
 
 
@@ -73,7 +76,7 @@ def get_access_token(self) -> str:
 
         if not response.ok:
             logger.error("OAuth2 failed for getting access token !")
-            raise RuntimeError("OAuth2 failed ! Reason : `{}`".format(response.content))
+            raise AuthError("OAuth2 failed ! Reason : `{}`".format(response.content))
 
         logger.debug("The access token has been got")
         return response.json()["access_token"]
@@ -145,7 +148,7 @@ def get_access_token(self):
 
         if not response.ok:
             logger.error("Sign in Failure !")
-            raise RuntimeError("Signin failed ! Reason : `{}`".format(response.content))
+            raise AuthError("Signin failed ! Reason : `{}`".format(response.content))
 
         logger.debug("The access token has been got")
         return response.json()["access_token"]
@@ -188,3 +191,57 @@ def __call__(
         encoded_body = formatted_body.encode("utf-8")
         updatable_object.body = encoded_body
         return updatable_object
+
+
+class OAuth2Session(Auth):
+    """
+    OAuth2 by using a password and adding credentials in the body of the request used to get the "access token".
+    """
+
+    auth_code_url: str
+    access_token_url: str
+    session_token_url: str
+    client_id: str
+    client_secret: str
+    username: str
+    password: str
+    name: str
+
+    def get_auth_code(self):
+        params = {
+            "client_id": self.client_id,
+            "response_type": "code",
+            "action": "Login",
+            "username": self.username,
+            "password": self.password,
+        }
+        resp_auth = requests.post(self.auth_code_url, params=params)
+        url = resp_auth.url
+        query = urllib.parse.urlparse(url).query
+        params_dict = urllib.parse.parse_qs(query)
+        if "code" not in params_dict:
+            raise AuthError("Can't find the auth_code.")
+        return params_dict["code"][0]
+
+    def get_access_token(self, auth_code:str):
+        params = {
+            "grant_type": "authorization_code",
+            "code": auth_code,
+            "client_id": self.client_id,
+            "client_secret": self.client_secret,
+        }
+        resp_token = requests.post(self.access_token_url, params=params)
+        return resp_token.json()["access_token"]
+
+    def get_session_token(self, access_token:str):
+        params = {"version": "*", "access_token": access_token}
+        session_token = requests.post(self.session_token_url, params=params)
+        return session_token.json()["BhRestToken"]
+
+    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
+        auth_code = self.get_auth_code()
+        access_token = self.get_access_token(auth_code)
+        session_token = self.get_session_token(access_token)
+        auth_header = {"BhRestToken": session_token}
+        request.headers.update(auth_header)
+        return request
diff --git a/src/hrflow_connectors/core/connector.py b/src/hrflow_connectors/core/connector.py
index a3ba0c95c..029bc1505 100644
--- a/src/hrflow_connectors/core/connector.py
+++ b/src/hrflow_connectors/core/connector.py
@@ -1,5 +1,5 @@
 from typing import Optional, Dict, Any
-
+from hrflow import Hrflow
 
 class Connector:
     """
@@ -7,35 +7,35 @@ class Connector:
     """
 
     @staticmethod
-    def pull_jobs(*args, **kwargs) -> Optional[Dict[str, Any]]:
+    def pull_jobs(hrflow_client: Hrflow, *args, **kwargs) -> Optional[Dict[str, Any]]:
         """
         Pull jobs
         """
         raise NotImplementedError("`pull_jobs` method is not implemented !")
 
     @staticmethod
-    def pull_profiles(*args, **kwargs) -> Optional[Dict[str, Any]]:
+    def pull_profiles(hrflow_client: Hrflow, *args, **kwargs) -> Optional[Dict[str, Any]]:
         """
         Pull profiles
         """
         raise NotImplementedError("`pull_profiles` method is not implemented !")
 
     @staticmethod
-    def push_job(*args, **kwargs) -> Optional[Dict[str, Any]]:
+    def push_job(hrflow_client: Hrflow, *args, **kwargs) -> Optional[Dict[str, Any]]:
         """
         Push job
         """
         raise NotImplementedError("`push_job` method is not implemented !")
 
     @staticmethod
-    def push_profile(*args, **kwargs) -> Optional[Dict[str, Any]]:
+    def push_profile(hrflow_client: Hrflow, *args, **kwargs) -> Optional[Dict[str, Any]]:
         """
         Push profile
         """
         raise NotImplementedError("`push_profile` method is not implemented !")
 
     @staticmethod
-    def catch_profile(*args, **kwargs) -> Optional[Dict[str, Any]]:
+    def catch_profile(hrflow_client: Hrflow, *args, **kwargs) -> Optional[Dict[str, Any]]:
         """
         Catch profile
         """
diff --git a/src/hrflow_connectors/core/error.py b/src/hrflow_connectors/core/error.py
new file mode 100644
index 000000000..1ceda236a
--- /dev/null
+++ b/src/hrflow_connectors/core/error.py
@@ -0,0 +1,127 @@
+import requests
+
+from ..utils.logger import get_logger
+from typing import Dict, Any
+
+logger = get_logger()
+
+
+class ConnectorError(Exception):
+    """
+    Connector Error
+    """
+
+    pass
+
+
+class AuthError(ConnectorError):
+    """
+    Auth Error
+    """
+
+    pass
+
+
+class ActionError(ConnectorError):
+    """
+    Action Error
+    """
+
+    def __init__(self, response: requests.Response, title: str, **kwargs):
+        """
+        Action Error
+
+        Args:
+            response (requests.Response): server response
+            title (str): title shown as the first error line
+
+        >>> raise ActionError(response, "Failed to do", job_id=15, job_ref="abc")
+        ... ActionError: Failed to do
+        ... job_id: `15`
+        ... job_ref: `abc`
+        ... Status code: `400`
+        ... Server response: `{message="error"}`
+        """
+        message = f"{title}\n"
+        for field_name, field_value in kwargs.items():
+            message += f"{field_name}: `{field_value}`\n"
+        message += f"Status code: `{response.status_code}`\n"
+        message += f"Server response: `{response.content}`"
+        logger.error(title)
+        super().__init__(message)
+
+
+class PullError(ActionError):
+    def __init__(self, response: requests.Response, **kwargs):
+        """
+        Pull Error
+
+        Args:
+            response (requests.Response): server response
+
+        >>> response = requests.get("http://test.test/")
+        >>> raise PullError(response, job_id=15, job_ref="abc")
+        ... PullError: Failed to pull
+        ... job_id: `15`
+        ... job_ref: `abc`
+        ... Status code: `400`
+        ... Server response: `{message="error"}`
+        """
+        title = "Failed to pull"
+        super().__init__(response, title, **kwargs)
+
+
+class PushError(ActionError):
+    def __init__(self, response: requests.Response, **kwargs):
+        """
+        Push Error
+
+        Args:
+            response (requests.Response): server response
+
+        >>> response = requests.post("http://test.test/")
+        >>> raise PushError(response, job_id=15, job_ref="abc")
+        ... PushError: Failed to push
+        ... job_id: `15`
+        ... job_ref: `abc`
+        ... Status code: `400`
+        ... Server response: `{message="error"}`
+        """
+        title = "Failed to push"
+        super().__init__(response, title, **kwargs)
+
+
+class HrflowError(ConnectorError):
+    """
+    Hrflow Error
+    """
+
+    def __init__(self, response: Dict[str, Any], title: str, **kwargs):
+        """
+        Hrflow Error
+
+        Args:
+            response (Dict[str, Any]): Hrflow request response
+            title (str): title shown as the first error line
+
+        >>> client = hrflow.Hrflow(api_secret="x_api_key", api_user="x_user_email")
+        >>> response = client.profile.indexing.get(...)
+        >>> raise PushError(response, "Get Profile failed !", profile_key="abc", source_key="efg")
+        ... HrflowError: Get Profile failed !
+        ... profile_key: `abc`
+        ... source_key: `efg`
+        ... Status code: `500`
+        ... Hrflow message: `Internal error`
+        """
+        message = f"{title}\n"
+        for field_name, field_value in kwargs.items():
+            message += f"{field_name}: `{field_value}`\n"
+
+        status_code = response["code"]
+        message += f"Status code: `{status_code}`\n"
+
+        hrflow_message = response["message"]
+        message += f"Hrflow message: `{hrflow_message}`"
+
+        logger.error(title)
+        super().__init__(message)
diff --git a/src/hrflow_connectors/utils/adress_to_lat_long.py b/src/hrflow_connectors/utils/adress_to_lat_long.py
index 8618524b6..7d88f7c3c 100644
--- a/src/hrflow_connectors/utils/adress_to_lat_long.py
+++ b/src/hrflow_connectors/utils/adress_to_lat_long.py
@@ -3,7 +3,7 @@
 import re
 
 
-def accent_replacer(s):
+def accent_replacer(s:str) -> str:
     """
     Replace the accentuated characters by same character without accent.
 
@@ -29,7 +29,7 @@ def accent_replacer(s):
     return s
 
 
-def get_cities_code_lat_long_mapping():
+def get_cities_code_lat_long_mapping() -> dict:
     """
     Get the dict of all postcode of french cities.
 
@@ -54,7 +54,7 @@ def get_cities_code_lat_long_mapping():
     return ville_code
 
 
-def get_cities_names_lat_long_mapping():
+def get_cities_names_lat_long_mapping() -> dict:
     """
     Get the dict of all names of french cities.
 
@@ -77,7 +77,7 @@ def get_cities_names_lat_long_mapping():
     return ville_name
 
 
-def get_departments_codes_lat_long_mapping():
+def get_departments_codes_lat_long_mapping() -> dict:
     """
     Get the dict of all postcode of french departments.
 
@@ -100,7 +100,7 @@ def get_departments_codes_lat_long_mapping():
 
 
 def get_lat_lng(
-    location, cities_codes_dict, cities_names_dict, departments_codes_dict, api_key=None
+    location: str, cities_codes_dict:dict, cities_names_dict:dict, departments_codes_dict:dict, api_key=None
 ):
     """
     Get the tuple latitude, longitude of a location with a 4 level of fall back.
diff --git a/src/hrflow_connectors/utils/config.py b/src/hrflow_connectors/utils/config.py
new file mode 100644
index 000000000..4feb36bbf
--- /dev/null
+++ b/src/hrflow_connectors/utils/config.py
@@ -0,0 +1,62 @@
+import os
+from dotenv import load_dotenv
+
+from .logger import get_logger
+
+logger = get_logger()
+
+
+class ConfigError(Exception):
+    """
+    Config Error
+    """
+
+    pass
+
+
+class Config:
+    """
+    Config class
+
+    Add the configuration line in `.env` to the root of the project.
+    You must start the environment variable name with `HRFLOW_CONNECTORS`.
+    For example: `HRFLOW_CONNECTORS_COMPANY_TOKEN = "abc"`.
+
+    Instantiate `Config` and call the newly created attribute (without `HRFLOW_CONNECTORS_` prefix).
+    If the environment variable is not found, then it will raise a `ConfigError`.
+
+    >>> config = Config()
+    >>> config.COMPANY_TOKEN
+    'abc'
+    >>> config.OTHER_TOKEN
+    ConfigError: Unable to get the value of the `OTHER_TOKEN` field from the configuration. Check that the field `HRFLOW_CONNECTORS_OTHER_TOKEN` is initialized in `.env` at the root of the project.
+    """
+
+    def __init__(self):
+        load_dotenv()
+        for key, value in os.environ.items():
+            if key.startswith("HRFLOW_CONNECTORS_"):
+                prefix_last_char_position = len("HRFLOW_CONNECTORS_")
+                # truncate var name to remove `HRFLOW_CONNECTORS_`
+                attribute_name = key[prefix_last_char_position:]
+                self.__setattr__(attribute_name, value)
+
+    def __getattr__(self, name: str):
+        """
+        Default behavior if the attribute `name` is not found.
+
+        Python call `__getattribute__` and if the attribute is not found
+        Then Python call `__getattr__`.
+
+        Args:
+            name (str): Attribute name
+
+        Raises:
+            ConfigError: If the attribute with the `name` is not found
+        """
+        error_message = (
+            f"Unable to get the value of the `{name}` field from the configuration. "
+        )
+        error_message += f"Check that the `HRFLOW_CONNECTORS_{name}` field is initialized in `.env` at the root of the project."
+        logger.error(error_message)
+        raise ConfigError(error_message)
\ No newline at end of file
diff --git a/src/hrflow_connectors/utils/hrflow.py b/src/hrflow_connectors/utils/hrflow.py
index db1a27385..d5902db09 100644
--- a/src/hrflow_connectors/utils/hrflow.py
+++ b/src/hrflow_connectors/utils/hrflow.py
@@ -1,5 +1,6 @@
 from typing import Dict, Any, List, Optional
 from pydantic import BaseModel
+from .schemas import HrflowJob, HrflowProfile
 
 
 def find_element_in_list(
@@ -45,44 +46,10 @@ def generate_workflow_response(status_code=201, **kwargs) -> Dict[str, Any]:
     return response
 
 
-class Source(BaseModel):
-    """
-    Hrflow Source
-    """
-
-    key: str
-
-
-class Profile(BaseModel):
-    """
-    Hrflow Profile
-    """
-
-    key: str
-    source: Source
-
-
-class Board(BaseModel):
-    """
-    Hrflow board
-    """
-
-    key: str
-
-
-class Job(BaseModel):
-    """
-    Hrflow job
-    """
-
-    key: str
-    board: Board
-
-
 class EventParser(BaseModel):
     request: Dict[str, Any]
 
-    def get_job(self, board_to_listen: List[str] = None) -> Optional[Job]:
+    def get_job(self, board_to_listen: List[str] = None) -> Optional[HrflowJob]:
         """
         Get job
 
@@ -96,12 +63,12 @@ def get_job(self, board_to_listen: List[str] = None) -> Optional[Job]:
         """
         job_dict = self.request.get("job")
         if job_dict is not None:
-            job_obj = Job.parse_obj(job_dict)
+            job_obj = HrflowJob.parse_obj(job_dict)
             if board_to_listen is None or job_obj.board.key in board_to_listen:
                 return job_obj
         return None
 
-    def get_profile(self, source_to_listen: List[str] = None) -> Optional[Profile]:
+    def get_profile(self, source_to_listen: List[str] = None) -> Optional[HrflowProfile]:
         """
         Get profile
 
@@ -115,7 +82,7 @@ def get_profile(self, source_to_listen: List[str] = None) -> Optional[Profile]:
         """
         profile_dict = self.request.get("profile")
         if profile_dict is not None:
-            profile_obj = Profile.parse_obj(profile_dict)
+            profile_obj = HrflowProfile.parse_obj(profile_dict)
             if source_to_listen is None or profile_obj.source.key in source_to_listen:
                 return profile_obj
         return None
diff --git a/src/hrflow_connectors/utils/schemas.py b/src/hrflow_connectors/utils/schemas.py
new file mode 100644
index 000000000..6191786ad
--- /dev/null
+++ b/src/hrflow_connectors/utils/schemas.py
@@ -0,0 +1,326 @@
+from typing import Union, Dict, Any, Optional, List
+from pydantic import BaseModel, Field
+
+# Hrflow source
+class HrflowSource(BaseModel):
+    key: str = Field(..., description="Identification key of the source")
+
+
+# Hrflow board
+class HrflowBoard(BaseModel):
+    key: str = Field(..., description="Identification key of the board")
+
+
+# Hrflow Job model
+
+
+class HrflowJobSection(BaseModel):
+    name: Optional[str] = Field(
+        None,
+        description="Identification name of a Section of the Job. Example: culture",
+    )
+    title: Optional[str] = Field(
+        None, description="Display Title of a Section. Example: Corporate Culture"
+    )
+    description: Optional[str] = Field(
+        None, description="Text description of a Section: Example: Our values areNone"
+    )
+
+
+class HrflowJobLocation(BaseModel):
+    text: Optional[str] = Field(None, description="Location text address.")
+    lat: Optional[float] = Field(
+        None, description="Geocentric latitude of the Location."
+    )
+    lng: Optional[float] = Field(
+        None, description="Geocentric longitude of the Location."
+    )
+    Fields: Optional[Dict[str, Any]] = Field(
+        None, description="other location attributes like country, country_codeNoneetc"
+    )
+
+
+class HrflowJobSKill(BaseModel):
+    name: str = Field(None, description="Identification name of the skill")
+    type: str = Field(None, description="Type of the skill. hard or soft")
+    value: Optional[str] = Field(None, description="Value associated to the skill")
+
+
+class HrflowJobField(BaseModel):
+    """
+    HrflowJobField: Languages, Certifications, Courses, Tasks, Interests, Metadatas, Tags
+    are arrays of objects following the JSON structure below
+    """
+
+    name: str = Field(None, description="Identification name of the Object")
+    value: Union[Optional[str], Optional[int], Optional[bool]] = Field(
+        None, description="Value associated to the Object's name"
+    )
+
+
+class HrflowJobRangesFloat(BaseModel):
+    """
+    HrflowJobRangesFloat: Ranges of floats is an array of objects accessible via the field ranges_float
+    and follows the structure below
+    """
+
+    name: Optional[str] = Field(
+        None,
+        description="Identification name of a Range of floats attached to the Job. Example: salary",
+    )
+    value_min: Optional[float] = Field(None, description="Min value. Example: 500.")
+    value_max: Optional[float] = Field(None, description="Max value. Example: 100.")
+    unit: Optional[str] = Field(None, description="Unit of the value. Example: euros.")
+
+
+class HrflowJobRangesDate(BaseModel):
+    """
+    HrflowJobRangesDate: Ranges of dates is an array of objects accessible via the field ranges_date
+    and follows the structure below
+    """
+
+    name: Optional[str] = Field(
+        None,
+        description="Identification name of a Range of dates attached to the Job. Example: availability.",
+    )
+    value_min: Optional[str] = Field(
+        None, description="Min value in datetime ISO 8601, Example: 500."
+    )
+    value_max: Optional[str] = Field(
+        None, description="Max value in datetime ISO 8601, Example: 1000"
+    )
+
+
+class HrflowJob(BaseModel):
+    """ Hrflow Job Object model """
+
+    key: Optional[str] = Field(None, description="Identification key of the Job.")
+    reference: Optional[str] = Field(None, description="Custom identifier of the Job.")
+    name: Optional[str] = Field(None, description="Job title.")
+    location: HrflowJobLocation = Field(None, description="Job location object.")
+    sections: List[HrflowJobSection] = Field(None, description="Job custom sections.")
+    url: Optional[str] = Field(None, description="Job post original URL.")
+    summary: Optional[str] = Field(None, description="Brief summary of the Job.")
+    archieved_at: Optional[str] = Field(
+        None,
+        description="type: datetime ISO8601, Archive date of the Job. The value is null for unarchived Jobs.",
+    )
+    updated_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Last update date of the Job."
+    )
+    created_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Creation date of the Job."
+    )
+    skills: Optional[List[HrflowJobSKill]] = Field(
+        None, description="List of skills of the Job."
+    )
+    languages: Optional[List[HrflowJobField]] = Field(
+        None, description="List of spoken languages of the Job"
+    )
+    certifications: Optional[List[HrflowJobField]] = Field(
+        None, description="List of certifications of the Job."
+    )
+    courses: Optional[List[HrflowJobField]] = Field(
+        None, description="List of courses of the Job"
+    )
+    tasks: Optional[List[HrflowJobField]] = Field(
+        None, description="List of tasks of the Job"
+    )
+    tags: Optional[List[HrflowJobField]] = Field(
+        None, description="List of tags of the Job"
+    )
+    metadatas: Optional[List[HrflowJobField]] = Field(
+        None, description="List of metadatas of the Job"
+    )
+    ranges_float: Optional[List[HrflowJobRangesFloat]] = Field(
+        None, description="List of ranges of floats"
+    )
+    ranges_date: Optional[List[HrflowJobRangesDate]] = Field(
+        None, description="List of ranges of dates"
+    )
+    board: Optional[HrflowBoard]
+
+
+# Hflow Profile model
+
+
+class FieldLocation(BaseModel):
+    """ Location for profile info, experience and education information"""
+
+    text: Optional[str] = Field(None, description="Location text address.")
+    lat: Optional[float] = Field(
+        None, description="Geocentric latitude of the Location."
+    )
+    lng: Optional[float] = Field(
+        None, description="Geocentric longitude of the Location."
+    )
+    Fields: Optional[Dict[str, Any]] = Field(
+        None, description="other location attributes like country, country_codeNoneetc"
+    )
+
+
+class FieldSkill(BaseModel):
+    name: str = Field(None, description="Identification name of the skill")
+    type: str = Field(None, description="Type of the skill. hard or soft")
+    value: Optional[str] = Field(None, description="Value associated to the skill")
+
+
+class InfoUrls(BaseModel):
+    """ Porfile urls """
+
+    from_resume: Optional[List[str]]
+    linkedin: Optional[str]
+    twitter: Optional[str]
+    facebook: Optional[str]
+    github: Optional[str]
+
+
+class HrflowProfileField(BaseModel):
+    """
+    HrflowJobField: Languages, Certifications, Courses, Tasks, Interests, Metadatas, Tags
+    are arrays of objects following the JSON structure below
+    """
+
+    name: str = Field(None, description="Identification name of the Object")
+    value: Optional[str] = Field(
+        None, description="Value associated to the Object's name"
+    )
+
+
+class HrflowProfileInfo(BaseModel):
+    """ Profile basic informations"""
+
+    full_name: Optional[str]
+    first_name: Optional[str]
+    last_name: Optional[str]
+    email: Optional[str]
+    phone: Optional[str]
+    date_birth: Optional[str] = Field(None, description="Profile date of birth")
+    location: Optional[FieldLocation] = Field(
+        None, description="Profile location object"
+    )
+    urls: Optional[InfoUrls] = Field(
+        None, description="Profile social networks and URLs"
+    )
+    picture: Optional[str] = Field(None, description="Profile picture url")
+    gender: Optional[str] = Field(None, description="Profile gender")
+    summary: Optional[str] = Field(None, description="Profile summary text")
+
+
+class Experience(BaseModel):
+    key: Optional[str] = Field(
+        None, description="Identification key of the Experience."
+    )
+    company: Optional[str] = Field(None, description="Company name of the Experience.")
+    title: Optional[str] = Field(None, description="Title of the Experience.")
+    description: Optional[str] = Field(
+        None, description="Description of the Experience."
+    )
+    location: Optional[FieldLocation] = Field(
+        None, description="Location object of the Experience."
+    )
+    date_start: Optional[str] = Field(
+        None, description="Start date of the experience. type: ('datetime ISO 8601')"
+    )
+    date_end: Optional[str] = Field(
+        None, description="End date of the experience. type: ('datetime ISO 8601')"
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Experience."
+    )
+    certifications: Optional[List[HrflowProfileField]]
+    courses: Optional[List[HrflowProfileField]]
+    tasks: Optional[List[HrflowProfileField]]
+
+
+class Education(BaseModel):
+    key: Optional[str] = Field(None, description="Identification key of the Education.")
+    school: Optional[str] = Field(None, description="School name of the Education.")
+    title: Optional[str] = Field(None, description="Title of the Education.")
+    description: Optional[str] = Field(
+        None, description="Description of the Education."
+    )
+    location: Optional[FieldLocation] = Field(
+        None, description="Location object of the Education."
+    )
+    date_start: Optional[str] = Field(
+        None, description="Start date of the Education. type: ('datetime ISO 8601')"
+    )
+    date_end: Optional[str] = Field(
+        None, description="End date of the Education. type: ('datetime ISO 8601')"
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Education."
+    )
+    certifications: Optional[List[HrflowProfileField]]
+    courses: Optional[List[HrflowProfileField]]
+    tasks: Optional[List[HrflowProfileField]]
+
+
+class HrflowProfile(BaseModel):
+    """ Hrflow Profile object model"""
+
+    key: Optional[str] = Field(None, description="Identification key of the Profile.")
+    reference: Optional[str] = Field(
+        None, description="Custom identifier of the Profile."
+    )
+    archieved_at: Optional[str] = Field(
+        None,
+        description="type: datetime ISO8601, Archive date of the Profile. The value is null for unarchived Profiles.",
+    )
+    updated_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Last update date of the Profile."
+    )
+    created_at: Optional[str] = Field(
+        None, description="type: datetime ISO8601, Creation date of the Profile."
+    )
+    info: Optional[HrflowProfileInfo] = Field(
+        None, description="Object containing the Profile's info."
+    )
+    text_language: Optional[str] = Field(
+        None, description="Code language of the Profile. type: string code ISO 639-1"
+    )
+    text: Optional[str] = Field(None, description="Full text of the Profile..")
+    experiences_duration: float = Field(
+        None, description="Total number of years of experience."
+    )
+    educations_duration: float = Field(
+        None, description="Total number of years of education."
+    )
+    experiences: Optional[List[Experience]] = Field(
+        None, description="List of experiences of the Profile."
+    )
+    educations: Optional[List[Education]] = Field(
+        None, description="List of educations of the Profile."
+    )
+    attachments: Optional[List] = Field(
+        None, description="List of documents attached to the Profile."
+    )
+    skills: Optional[List[FieldSkill]] = Field(
+        None, description="List of skills of the Profile."
+    )
+    languages: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of spoken languages of the profile"
+    )
+    certifications: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of certifications of the Profile."
+    )
+    courses: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of courses of the Profile."
+    )
+    tasks: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of tasks of the Profile."
+    )
+    interests: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of interests of the Profile."
+    )
+    labels: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of labels of the Profile."
+    )
+    tags: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of tags of the Profile."
+    )
+    metadatas: Optional[List[HrflowProfileField]] = Field(
+        None, description="List of metadatas of the Profile."
+    )
+    source: Optional[HrflowSource]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 33de0fa59..d34ee06f7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,23 +12,22 @@
 from hrflow import Hrflow
 
 from hrflow_connectors.utils.logger import get_logger_with_basic_config
+from hrflow_connectors.utils.config import Config
 
 
 @pytest.fixture(scope="session")
-def credentials(pytestconfig) -> Dict[str, Any]:
+def config() -> Config:
     """
-    Get credentials from a file in the root of the project `credentials.json` (to be defined)
+    Get config from the `.env` file at the root of the project (to be defined)
 
     Returns:
-        Dict[str, Any]: Credentials
+        Config: Config instance
     """
-    with open(os.path.join(pytestconfig.rootpath, "credentials.json"), "r") as f:
-        credentials = json.loads(f.read())
-    return credentials
+    return Config()
 
 
 @pytest.fixture(scope="session")
-def hrflow_client(credentials) -> Callable:
+def hrflow_client(config) -> Callable:
     """
     Get a function to generate an instance of Hrflow Client
 
@@ -41,10 +40,15 @@ def hrflow_client(credentials) -> Callable:
     """
 
     def hrflow_client_func(portal_name="dev-demo"):
-        x_api_key = credentials["hrflow"][portal_name]["x-api-key"]
-        x_user_email = credentials["hrflow"][portal_name]["x-user-email"]
-        client = Hrflow(api_secret=x_api_key, api_user=x_user_email)
-        return client
+        if portal_name == "dev-demo":
+            x_api_key = config.HRFLOW_DEVDEMO_XAPIKEY
+            x_user_email = config.HRFLOW_DEVDEMO_XUSEREMAIL
+        elif portal_name == "vulcain":
+            x_api_key = config.HRFLOW_VULCAIN_XAPIKEY
+            x_user_email = config.HRFLOW_VULCAIN_XUSEREMAIL
+        else:
+            raise RuntimeError(f"Hrflow Portal `{portal_name}` not found !")
+        return Hrflow(api_secret=x_api_key, api_user=x_user_email)
 
     return hrflow_client_func
 
diff --git a/tests/connectors/breezyhr/test_pull_jobs.py b/tests/connectors/breezyhr/test_pull_jobs.py
index 748d93f60..1dc9188e9 100644
--- a/tests/connectors/breezyhr/test_pull_jobs.py
+++ b/tests/connectors/breezyhr/test_pull_jobs.py
@@ -5,11 +5,11 @@
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     auth = OAuth2EmailPasswordBody(
-        access_token_url = "https://api.breezy.hr/v3/signin",
-        email = credentials["breezyhr"]["email"],
-        password= credentials["breezyhr"]["password"]
+        access_token_url="https://api.breezy.hr/v3/signin",
+        email=config.BREEZYHR_EMAIL,
+        password=config.BREEZYHR_PASSWORD,
     )
     return auth
 
@@ -18,7 +18,7 @@ def test_PullJobsAction(logger, auth, hrflow_client):
     Breezyhr.pull_jobs(
         auth=auth,
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="fa06643e19d811e8da472858c07f8bbbd954dfd0",
+        board_key="194d9440437c157275f33ec9eda80a0250872e54",
         hydrate_with_parsing=True,
-        company_name="Hrflow.ai"
+        company_name="Hrflow.ai",
     )
diff --git a/tests/connectors/breezyhr/test_push_profile.py b/tests/connectors/breezyhr/test_push_profile.py
index b4e7c58a2..6e67b3170 100644
--- a/tests/connectors/breezyhr/test_push_profile.py
+++ b/tests/connectors/breezyhr/test_push_profile.py
@@ -2,23 +2,23 @@
 
 from hrflow_connectors import OAuth2EmailPasswordBody
 from hrflow_connectors import Breezyhr
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     auth = OAuth2EmailPasswordBody(
         access_token_url="https://api.breezy.hr/v3/signin",
-        email=credentials["breezyhr"]["email"],
-        password=credentials["breezyhr"]["password"],
+        email=config.BREEZYHR_EMAIL,
+        password=config.BREEZYHR_PASSWORD,
     )
     return auth
 
 
 def test_PushProfileBaseAction(logger, auth, hrflow_client):
-    profile = Profile(
+    profile = HrflowProfile(
         key="a7e7fa4af68e7c450f2b708d14a3bda9b6ade5d9",
-        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
     response = Breezyhr.push_profile(
         auth=auth,
diff --git a/tests/connectors/bullhorn/__init__.py b/tests/connectors/bullhorn/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/connectors/bullhorn/test_push_profile.py b/tests/connectors/bullhorn/test_push_profile.py
new file mode 100644
index 000000000..e488f04f4
--- /dev/null
+++ b/tests/connectors/bullhorn/test_push_profile.py
@@ -0,0 +1,32 @@
+import pytest
+
+from hrflow_connectors import OAuth2Session
+from hrflow_connectors import Bullhorn
+from hrflow_connectors.utils.schemas import HrflowProfile
+
+
+@pytest.fixture
+def auth(config):
+    auth = OAuth2Session(auth_code_url="https://auth.bullhornstaffing.com/oauth/authorize",
+                         access_token_url="https://auth.bullhornstaffing.com/oauth/token",
+                         session_token_url="https://rest.bullhornstaffing.com/rest-services/login",
+                         client_id=config.BULLHORN_CLIENT_ID,
+                         client_secret=config.BULLHORN_CLIENT_SECRET,
+                         username=config.BULLHORN_USERNAME,
+                         password=config.BULLHORN_PASSWORD,
+                         name="BhRestToken")
+    return auth
+
+
+def test_PushProfile(logger, auth, hrflow_client):
+    profile = HrflowProfile(
+        key="5574b4ebdfe0e52446eade74e87cfe04739d1c96",
+        source=dict(key="af00e468b1cf0d5eda0bc6062f2e163d50b1872e"),
+    )
+    response = Bullhorn.push_profile(
+        auth=auth,
+        subdomain="rest91",
+        hrflow_client=hrflow_client(),
+        profile=profile,
+    )
+    assert response.get("status_code") == 201
diff --git a/tests/connectors/ceridian/test_pull_jobs.py b/tests/connectors/ceridian/test_pull_jobs.py
index a8f5ce9a9..59ac1b56c 100644
--- a/tests/connectors/ceridian/test_pull_jobs.py
+++ b/tests/connectors/ceridian/test_pull_jobs.py
@@ -6,6 +6,6 @@ def test_PullJobs(logger, hrflow_client):
         subdomain='ustest61-services',
         client_name_space = 'ddn',
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="f54e75c62da8f3273789e10a3c89d9877b72c66d",
+        board_key="3bc89d440036641fedb04349e8e9767d77fa6830",
         hydrate_with_parsing=True,
     )
diff --git a/tests/connectors/crosstalent/test_auth.py b/tests/connectors/crosstalent/test_auth.py
index 6e0c0fda5..fcdb864bd 100644
--- a/tests/connectors/crosstalent/test_auth.py
+++ b/tests/connectors/crosstalent/test_auth.py
@@ -4,15 +4,14 @@
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     access_token_url = "https://test.salesforce.com/services/oauth2/token"
-
     auth = OAuth2PasswordCredentialsBody(
         access_token_url=access_token_url,
-        client_id=credentials["crosstalent"]["oauth2"]["client_id"],
-        client_secret=credentials["crosstalent"]["oauth2"]["client_secret"],
-        username=credentials["crosstalent"]["oauth2"]["username"],
-        password=credentials["crosstalent"]["oauth2"]["password"],
+        client_id=config.CROSSTALENT_CLIENT_ID,
+        client_secret=config.CROSSTALENT_CLIENT_SECRET,
+        username=config.CROSSTALENT_USERNAME,
+        password=config.CROSSTALENT_PASSWORD,
     )
     return auth
 
diff --git a/tests/connectors/crosstalent/test_pull_jobs.py b/tests/connectors/crosstalent/test_pull_jobs.py
index cd3b88e56..94a8fa71c 100644
--- a/tests/connectors/crosstalent/test_pull_jobs.py
+++ b/tests/connectors/crosstalent/test_pull_jobs.py
@@ -5,15 +5,14 @@
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     access_token_url = "https://test.salesforce.com/services/oauth2/token"
-
     auth = OAuth2PasswordCredentialsBody(
         access_token_url=access_token_url,
-        client_id=credentials["crosstalent"]["oauth2"]["client_id"],
-        client_secret=credentials["crosstalent"]["oauth2"]["client_secret"],
-        username=credentials["crosstalent"]["oauth2"]["username"],
-        password=credentials["crosstalent"]["oauth2"]["password"],
+        client_id=config.CROSSTALENT_CLIENT_ID,
+        client_secret=config.CROSSTALENT_CLIENT_SECRET,
+        username=config.CROSSTALENT_USERNAME,
+        password=config.CROSSTALENT_PASSWORD,
     )
     return auth
 
@@ -23,6 +22,6 @@ def test_PullJobsAction(logger, auth, hrflow_client):
         auth=auth,
         subdomain="vulcain-eng--recette.my",
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="8eba188e1af123a9818d00974ff37b943b7d54f4",
+        board_key="34ba5e086fd953f2bb835c58f2a3bb2425dc09e6",
         hydrate_with_parsing=True,
     )
\ No newline at end of file
diff --git a/tests/connectors/crosstalent/test_push_profile.py b/tests/connectors/crosstalent/test_push_profile.py
index 306b8d63d..9a8895f6b 100644
--- a/tests/connectors/crosstalent/test_push_profile.py
+++ b/tests/connectors/crosstalent/test_push_profile.py
@@ -2,31 +2,31 @@
 
 from hrflow_connectors import OAuth2PasswordCredentialsBody
 from hrflow_connectors import Crosstalent
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     access_token_url = "https://test.salesforce.com/services/oauth2/token"
     auth = OAuth2PasswordCredentialsBody(
         access_token_url=access_token_url,
-        client_id=credentials["crosstalent"]["oauth2"]["client_id"],
-        client_secret=credentials["crosstalent"]["oauth2"]["client_secret"],
-        username=credentials["crosstalent"]["oauth2"]["username"],
-        password=credentials["crosstalent"]["oauth2"]["password"],
+        client_id=config.CROSSTALENT_CLIENT_ID,
+        client_secret=config.CROSSTALENT_CLIENT_SECRET,
+        username=config.CROSSTALENT_USERNAME,
+        password=config.CROSSTALENT_PASSWORD,
     )
     return auth
 
 
 def test_PushProfileBaseAction(logger, auth, hrflow_client):
-    profile = Profile(
-        key="ea5704b959c5e53aaef65c04ef5018ae1fee1a77",
-        source=Source(key="15517d70b0870e4cf431eefd78f8b39cff5607e8"),
+    profile = HrflowProfile(
+        key="a7e7fa4af68e7c450f2b708d14a3bda9b6ade5d9",
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
     response = Crosstalent.push_profile(
         auth=auth,
         subdomain="vulcain-eng--recette.my",
-        hrflow_client=hrflow_client("vulcain"),
+        hrflow_client=hrflow_client("dev-demo"),
         profile=profile,
     )
     assert response.get("status_code") == 201
diff --git a/tests/connectors/flatchr/test_push_profile.py b/tests/connectors/flatchr/test_push_profile.py
index 92c9b1177..a29f9a1d8 100644
--- a/tests/connectors/flatchr/test_push_profile.py
+++ b/tests/connectors/flatchr/test_push_profile.py
@@ -2,19 +2,18 @@
 
 from hrflow_connectors import AuthorizationAuth
 from hrflow_connectors import Flatchr
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 @pytest.fixture
-def auth(credentials):
-    auth = AuthorizationAuth(value=credentials["flatchr"]["x-api-key"])
-    return auth
+def auth(config):
+    return AuthorizationAuth(value=config.FLATCHR_TOKEN)
 
 
 def test_PushProfileBaseAction(logger, auth, hrflow_client):
-    profile = Profile(
+    profile = HrflowProfile(
         key="5746beca5e941a5a55706efd9adfce31f59e6e2b",
-        source=Source(key="d42eed17626b7ae3dc05efca363788caef91d44b"),
+        source=dict(key="d42eed17626b7ae3dc05efca363788caef91d44b"),
     )
     response = Flatchr.push_profile(
         auth=auth,
diff --git a/tests/connectors/greenhouse/test_pull_jobs.py b/tests/connectors/greenhouse/test_pull_jobs.py
index b77905fd1..fb8f182bf 100644
--- a/tests/connectors/greenhouse/test_pull_jobs.py
+++ b/tests/connectors/greenhouse/test_pull_jobs.py
@@ -5,6 +5,6 @@ def test_PullJobsAcrion(logger, hrflow_client):
     Greenhouse.pull_jobs(
         board_token="vaulttec",
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="61abdf5e11a8b75f2f5d1789339ee3638d4c6197",
+        board_key="f02c4d041ce04a4657e7aaa360fdeb3712aec130",
         hydrate_with_parsing=True,
     )
\ No newline at end of file
diff --git a/tests/connectors/greenhouse/test_push_profile.py b/tests/connectors/greenhouse/test_push_profile.py
index 8fa621377..f618a51b2 100644
--- a/tests/connectors/greenhouse/test_push_profile.py
+++ b/tests/connectors/greenhouse/test_push_profile.py
@@ -2,30 +2,23 @@
 
 from hrflow_connectors import Greenhouse
 from hrflow_connectors.core.auth import AuthorizationAuth, OAuth2PasswordCredentialsBody
-from hrflow_connectors.utils.hrflow import Profile, Source
-
-
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 @pytest.fixture
-def auth(credentials):
-    auth = AuthorizationAuth(
-        name = 'Authorization',
-        value= credentials["greenhouse"]["oauth2"]["Authorization"]
-    )
-    
-    return auth
+def auth(config):
+    return AuthorizationAuth(value=config.GREENHOUSE_TOKEN)
 
 
 def test_PushProfile(logger, auth, hrflow_client):
 
-    profile = Profile(
+    profile = HrflowProfile(
         key="65903f6989594e72ccf64a10875127b1293fdce3",
-        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
     response = Greenhouse.push_profile(
-        job_id = ["4179714004"] ,
-        on_behalf_of = "4249018004",
+        job_id=["4179714004"],
+        on_behalf_of="4249018004",
         auth=auth,
         hrflow_client=hrflow_client(),
         profile=profile,
diff --git a/tests/connectors/monster/test_catch_profile.py b/tests/connectors/monster/test_catch_profile.py
index b3ac3dd7d..49ff7b097 100644
--- a/tests/connectors/monster/test_catch_profile.py
+++ b/tests/connectors/monster/test_catch_profile.py
@@ -8,10 +8,10 @@
 @pytest.fixture()
 def monster_request() -> Dict[str, Any]:
     """
-    Get credentials from a file in the root of the project `credentials.json` (to be defined)
+    Get monster request
 
     Returns:
-        Dict[str, Any]: Credentials
+        Dict[str, Any]: Monster request
     """
 
     url = (
diff --git a/tests/connectors/monster/test_push_job.py b/tests/connectors/monster/test_push_job.py
index 86b0c71b4..ae6139571 100644
--- a/tests/connectors/monster/test_push_job.py
+++ b/tests/connectors/monster/test_push_job.py
@@ -2,22 +2,22 @@
 
 from hrflow_connectors import Monster
 from hrflow_connectors import MonsterBodyAuth
-from hrflow_connectors.utils.hrflow import Job, Board
+from hrflow_connectors.utils.schemas import HrflowJob
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     auth = MonsterBodyAuth(
-        username=credentials["monster"]["username"],
-        password=credentials["monster"]["password"],
+        username=config.MONSTER_USERNAME,
+        password=config.MONSTER_PASSWORD,
     )
     return auth
 
 
 def test_PushJobBaseAction(logger, auth, hrflow_client):
-    job = Job(
+    job = HrflowJob(
         key="230f841bc57774bde1ad67563d9f092a788364e0",
-        board=Board(key="d31518949ed1f88ac61308670324f93bc0f9374d"),
+        board=dict(key="d31518949ed1f88ac61308670324f93bc0f9374d"),
     )
     response = Monster.push_job(
         auth=auth,
diff --git a/tests/connectors/recruitee/test_pull_jobs.py b/tests/connectors/recruitee/test_pull_jobs.py
index ba91b4886..600df9e38 100644
--- a/tests/connectors/recruitee/test_pull_jobs.py
+++ b/tests/connectors/recruitee/test_pull_jobs.py
@@ -5,6 +5,6 @@ def test_PullJobs(logger, hrflow_client):
     Recruitee.pull_jobs(
         subdomain='testhr',
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="c430a1fe111a4076605a8dee85448300bd40f890",
+        board_key="243ed7c3fef55e1bacee3ba71b6d589e2b21079a",
         hydrate_with_parsing=True,
     )
\ No newline at end of file
diff --git a/tests/connectors/recruitee/test_push_profile.py b/tests/connectors/recruitee/test_push_profile.py
index a06e26d5f..bad6a3be1 100644
--- a/tests/connectors/recruitee/test_push_profile.py
+++ b/tests/connectors/recruitee/test_push_profile.py
@@ -2,27 +2,23 @@
 
 from hrflow_connectors import AuthorizationAuth
 from hrflow_connectors import Recruitee
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
+
 
 @pytest.fixture
-def auth(credentials):
-    auth = AuthorizationAuth(
-        name = 'Authorization',
-        value= credentials["recruitee"]["oauth2"]["Bearer"]
-    )
-    
-    return auth
+def auth(config):
+    return AuthorizationAuth(value=config.RECRUITEE_TOKEN)
 
 
 def test_PushProfile(logger, auth, hrflow_client):
 
-    profile = Profile(
+    profile = HrflowProfile(
         key="65903f6989594e72ccf64a10875127b1293fdce3",
-        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
     response = Recruitee.push_profile(
-        company_id = "hrflowai",
-        offer_id = None,
+        company_id="hrflowai",
+        offer_id=None,
         auth=auth,
         hrflow_client=hrflow_client(),
         profile=profile,
diff --git a/tests/connectors/sapsuccessfactors/test_pull_jobs.py b/tests/connectors/sapsuccessfactors/test_pull_jobs.py
index ca60ce597..02df0344e 100644
--- a/tests/connectors/sapsuccessfactors/test_pull_jobs.py
+++ b/tests/connectors/sapsuccessfactors/test_pull_jobs.py
@@ -4,10 +4,10 @@
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     auth = XAPIKeyAuth(
         name='APIKey',
-        value=credentials["sapsuccessfactors"]["oauth2"]["APIKey"],
+        value=config.SAPSUCCESSFACTORS_TOKEN,
     )
     return auth
 
@@ -17,6 +17,6 @@ def test_PullJobs(logger,auth, hrflow_client):
         api_server="sandbox.api.sap.com:443/successfactors",
         top=20,
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="193bc9ff5d3076937d93950381783e108c957fb3",
+        board_key="91c42ec77a7abfdd51e2a48a4a4d7ab147e0f17d",
         hydrate_with_parsing=True,
     )
diff --git a/tests/connectors/sapsuccessfactors/test_push_profile.py b/tests/connectors/sapsuccessfactors/test_push_profile.py
index 912a27b70..7a2ff179b 100644
--- a/tests/connectors/sapsuccessfactors/test_push_profile.py
+++ b/tests/connectors/sapsuccessfactors/test_push_profile.py
@@ -1,23 +1,24 @@
 import pytest
 from hrflow_connectors import SapSuccessfactors
 from hrflow_connectors import XAPIKeyAuth
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 
 @pytest.fixture
-def auth(credentials):
+def auth(config):
     auth = XAPIKeyAuth(
         name='APIKey',
-        value=credentials["sapsuccessfactors"]["oauth2"]["APIKey"],
+        value=config.SAPSUCCESSFACTORS_TOKEN,
     )
     return auth
 
+
 def test_PushProfile(logger,auth, hrflow_client):
 
-    profile = Profile(
+    profile = HrflowProfile(
         key = "65903f6989594e72ccf64a10875127b1293fdce3",
-        source = Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb")
+        source = dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb")
     )
     response = SapSuccessfactors.push_profile(
         profile_already_exists='COE0019',
diff --git a/tests/connectors/smartrecruiters/test_pull_jobs.py b/tests/connectors/smartrecruiters/test_pull_jobs.py
index fe60507d8..574ddcde9 100644
--- a/tests/connectors/smartrecruiters/test_pull_jobs.py
+++ b/tests/connectors/smartrecruiters/test_pull_jobs.py
@@ -5,11 +5,8 @@
 
 
 @pytest.fixture
-def auth(credentials):
-    auth = XSmartTokenAuth(
-        value=credentials["smartrecruiters"]["oauth2"]["X-SmartToken"]
-    )
-    return auth
+def auth(config):
+    return XSmartTokenAuth(value=config.SMARTRECRUITERS_TOKEN)
 
 
 def test_PullJobsAction(logger, auth, hrflow_client):
@@ -17,7 +14,7 @@ def test_PullJobsAction(logger, auth, hrflow_client):
         auth=auth,
         hrflow_client=hrflow_client("dev-demo"),
         limit=2,
-        board_key="8ebdea98768dfc04d15f76afab70415ed280ea90",
+        board_key="1bed06c0123081959f830a920b3113d2540a02f7",
         hydrate_with_parsing=False,
         archive_deleted_jobs_from_stream=False,
         posting_status="PUBLIC",
diff --git a/tests/connectors/smartrecruiters/test_push_profile.py b/tests/connectors/smartrecruiters/test_push_profile.py
index 8ab6d0842..89feb5bba 100644
--- a/tests/connectors/smartrecruiters/test_push_profile.py
+++ b/tests/connectors/smartrecruiters/test_push_profile.py
@@ -2,21 +2,18 @@
 
 from hrflow_connectors import XSmartTokenAuth
 from hrflow_connectors import SmartRecruiters
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
 
 @pytest.fixture
-def auth(credentials):
-    auth = XSmartTokenAuth(
-        value=credentials["smartrecruiters"]["oauth2"]["X-SmartToken"]
-    )
-    return auth
+def auth(config):
+    return XSmartTokenAuth(value=config.SMARTRECRUITERS_TOKEN)
 
 
 def test_SmartRecruitersPushProfileAction(logger, auth, hrflow_client):
-    profile = Profile(
+    profile = HrflowProfile(
         key="89ddf5f18768747011a06b8921607cb54a4274a5",
-        source=Source(key="6d68a20b2dd7c2bfdcb232b9234c38eada0fdcb4"),
+        source=dict(key="6d68a20b2dd7c2bfdcb232b9234c38eada0fdcb4"),
     )
     response = SmartRecruiters.push_profile(
         auth=auth,
diff --git a/tests/connectors/taleez/test_pull_jobs.py b/tests/connectors/taleez/test_pull_jobs.py
index e552babff..ba89cba59 100644
--- a/tests/connectors/taleez/test_pull_jobs.py
+++ b/tests/connectors/taleez/test_pull_jobs.py
@@ -5,18 +5,14 @@
 
 
 @pytest.fixture
-def auth(credentials):
-    auth = XTaleezAuth(
-        name = 'X-taleez-api-secret',
-        value=credentials["taleez"]["X-taleez-api-secret"]
-    )
-    return auth
+def auth(config):
+    return XTaleezAuth(value=config.TALEEZ_TOKEN)
 
 
 def test_PullJobsAction(logger, auth, hrflow_client):
     Taleez.pull_jobs(
         auth=auth,
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="234e96cf22440fa88cae5bca854d674f77fa8f93",
+        board_key="a820ecb28fc23b8217276cf2352387feaa5d7249",
         hydrate_with_parsing=True,
     )
diff --git a/tests/connectors/taleez/test_push_profile.py b/tests/connectors/taleez/test_push_profile.py
index 2d90b4f4c..c345afd36 100644
--- a/tests/connectors/taleez/test_push_profile.py
+++ b/tests/connectors/taleez/test_push_profile.py
@@ -2,28 +2,24 @@
 
 from hrflow_connectors import XTaleezAuth
 from hrflow_connectors import Taleez
-from hrflow_connectors.utils.hrflow import Profile, Source
+from hrflow_connectors.utils.schemas import HrflowProfile
 
-@pytest.fixture
-def auth(credentials):
-    auth = XTaleezAuth(
-        name = 'X-taleez-api-secret',
-        value=credentials["taleez"]["X-taleez-api-secret"]
-    )
-    return auth
 
+@pytest.fixture
+def auth(config):
+    return XTaleezAuth(value=config.TALEEZ_TOKEN)
 
 
 def test_PushProfile(logger, auth, hrflow_client):
 
-    profile = Profile(
+    profile = HrflowProfile(
         key="65903f6989594e72ccf64a10875127b1293fdce3",
-        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
     response = Taleez.push_profile(
-        job_id = None,
+        job_id=None,
         auth=auth,
-        recruiter_id = 15886,
+        recruiter_id=15886,
         hrflow_client=hrflow_client(),
         profile=profile,
     )
diff --git a/tests/connectors/teamtailor/__init__.py b/tests/connectors/teamtailor/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/connectors/teamtailor/test_pull_jobs.py b/tests/connectors/teamtailor/test_pull_jobs.py
new file mode 100644
index 000000000..1991923ec
--- /dev/null
+++ b/tests/connectors/teamtailor/test_pull_jobs.py
@@ -0,0 +1,18 @@
+import pytest
+
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors import Teamtailor
+
+
+@pytest.fixture
+def auth(config):
+    return AuthorizationAuth(value=config.TEAMTAILOR_TOKEN)
+
+
+def test_PullJobsAction(logger, auth, hrflow_client):
+    Teamtailor.pull_jobs(
+        auth=auth,
+        hrflow_client=hrflow_client("dev-demo"),
+        board_key="3fbfb874dc01a22e29abe639193c2cc319074712",
+        hydrate_with_parsing=True,
+    )
\ No newline at end of file
diff --git a/tests/connectors/teamtailor/test_push_profile.py b/tests/connectors/teamtailor/test_push_profile.py
new file mode 100644
index 000000000..74364bdc4
--- /dev/null
+++ b/tests/connectors/teamtailor/test_push_profile.py
@@ -0,0 +1,25 @@
+import pytest
+
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors import Teamtailor
+from hrflow_connectors.utils.schemas import HrflowProfile
+
+
+@pytest.fixture
+def auth(config):
+    return AuthorizationAuth(value=config.TEAMTAILOR_TOKEN)
+
+
+def test_PushProfile(logger, auth, hrflow_client):
+
+    profile = HrflowProfile(
+        key="65903f6989594e72ccf64a10875127b1293fdce3",
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+    )
+    response = Teamtailor.push_profile(
+        company_id="hrflowai",
+        auth=auth,
+        hrflow_client=hrflow_client("dev-demo"),
+        profile=profile,
+    )
+    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/workable/test_pull_jobs.py b/tests/connectors/workable/test_pull_jobs.py
index 4d7d3b531..886910b20 100644
--- a/tests/connectors/workable/test_pull_jobs.py
+++ b/tests/connectors/workable/test_pull_jobs.py
@@ -1,10 +1,16 @@
 from hrflow_connectors import Workable
+from hrflow_connectors import AuthorizationAuth
+import pytest
 
+@pytest.fixture
+def auth(config):
+    return AuthorizationAuth(value=config.WORKABLE_TOKEN)
 
-def test_PullJobsAcrion(logger, hrflow_client):
+def test_PullJobsAcrion(logger,auth, hrflow_client):
     Workable.pull_jobs(
-        subdomain="eurostar",
+        subdomain="arabeg",
+        auth=auth,
         hrflow_client=hrflow_client("dev-demo"),
-        board_key="12dc2abb0d952bfcbbc6259b76c0631754459787",
+        board_key="0d9a50e90e529e43394cf84b2f0666432551980b",
         hydrate_with_parsing=True,
     )
\ No newline at end of file
diff --git a/tests/connectors/workable/test_push_profile.py b/tests/connectors/workable/test_push_profile.py
new file mode 100644
index 000000000..d8b15c66d
--- /dev/null
+++ b/tests/connectors/workable/test_push_profile.py
@@ -0,0 +1,23 @@
+from hrflow_connectors import Workable
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors.utils.schemas import HrflowProfile
+import pytest
+
+@pytest.fixture
+def auth(config):
+    return AuthorizationAuth(value=config.WORKABLE_TOKEN)
+
+def test_PushProfile(logger, auth, hrflow_client):
+
+    profile = HrflowProfile(
+        key="65903f6989594e72ccf64a10875127b1293fdce3",
+        source=dict(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+    )
+    response = Workable.push_profile(
+        auth=auth,
+        subdomain="arabeg",
+        shortcode="EE2E1C5B15",
+        hrflow_client=hrflow_client("dev-demo"),
+        profile=profile,
+    )
+    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/xml/test_pull_jobs.py b/tests/connectors/xml/test_pull_jobs.py
index 400324207..6ab133b97 100644
--- a/tests/connectors/xml/test_pull_jobs.py
+++ b/tests/connectors/xml/test_pull_jobs.py
@@ -3,6 +3,7 @@
 from hrflow_connectors import XML
 from hrflow_connectors.connectors.xml.actions import PullJobsAction
 from hrflow_connectors.utils.datetime_converter import from_str_to_datetime
+from hrflow_connectors.utils.schemas import HrflowJob
 
 
 def test_PullJobsAction_from_samsic_xml_stream(logger, hrflow_client):
@@ -120,8 +121,8 @@ def create_tag(field_path: str):
 
         # metadatas
         job["metadatas"] = []
-
-        return job
+        job_obj = HrflowJob.parse_obj(job)
+        return job_obj
 
     xml_stream_url = "https://cv.samsic-emploi.fr/media/flux/jobs.xml"
     job_list_xpath = "DataArea"
diff --git a/tests/core/test_action.py b/tests/core/test_action.py
index 9b4b9c0f5..2b9671ff5 100644
--- a/tests/core/test_action.py
+++ b/tests/core/test_action.py
@@ -7,7 +7,13 @@
     PushJobBaseAction,
     CatchProfileBaseAction,
 )
-from hrflow_connectors.utils.hrflow import Profile, Source, Job, Board
+from hrflow_connectors.utils.schemas import (
+    HrflowProfile,
+    HrflowSource,
+    HrflowJob,
+    HrflowBoard,
+)
+from hrflow_connectors.core.error import HrflowError
 import pytest
 import requests
 import responses
@@ -43,7 +49,10 @@ def test_BaseAction_apply_logics_with_empty_logics_list(
 
 def test_BaseAction_apply_logics_single_filter(hrflow_client, generated_data_list):
     def filter_element1_with_value1(element):
-        return element.get("element1") == "value1"
+        if element.get("element1") == "value1":
+            return element
+        else:
+            return None
 
     action = BaseAction(
         hrflow_client=hrflow_client(),
@@ -60,10 +69,16 @@ def filter_element1_with_value1(element):
 
 def test_BaseAction_apply_logics_two_filter(hrflow_client, generated_data_list):
     def filter_element1_with_value1(element):
-        return element.get("element1") == "value1"
+        if element.get("element1") == "value1":
+            return element
+        else:
+            return None
 
     def filter_element2_with_value1(element):
-        return element.get("element2") == "value1"
+        if element.get("element2") == "value1":
+            return element
+        else:
+            return None
 
     action = BaseAction(
         hrflow_client=hrflow_client(),
@@ -81,7 +96,7 @@ def test_BaseAction_apply_logics_single_filter_without_interaction(
     hrflow_client, generated_data_list
 ):
     def filter_nothing(element):
-        return True
+        return element
 
     action = BaseAction(
         hrflow_client=hrflow_client(),
@@ -204,7 +219,10 @@ def push(self, data):
             assert data_list == ["logic"]
 
     def my_logic(data):
-        return not data == "format"
+        if "format" in data:
+            return None
+        else:
+            return data
 
     action = MyPullAction(
         hrflow_client=hrflow_client,
@@ -234,7 +252,10 @@ def push(self, data):
             assert data_list == ["format"]
 
     def my_logic(data):
-        return not data == "pulllogic"
+        if data == "pulllogic":
+            return None
+        else:
+            return data
 
     action = MyPushAction(
         hrflow_client=hrflow_client,
@@ -343,9 +364,9 @@ def test_PullJobsBaseAction_get_all_references_from_board_failure(
     )
     try:
         all_reference_iter = action.get_all_references_from_board()
-        all_reference_list = list(all_reference_iter)
+        list(all_reference_iter)
         assert False
-    except RuntimeError:
+    except HrflowError:
         pass
 
 
@@ -427,18 +448,20 @@ def test_PullJobsBaseAction_hydrate_job_with_parsing(
         hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     section = dict(name="s", title=None, description="i speak english")
-    job = dict(reference="REF123", summary="I love Python", sections=[section])
+    job = HrflowJob.parse_obj(
+        dict(reference="REF123", summary="I love Python", sections=[section])
+    )
 
-    assert len(job.get("skills", [])) == 0
-    assert len(job.get("language", [])) == 0
+    assert job.skills == None
+    assert job.languages == None
 
     hydrated_job = action.hydrate_job_with_parsing(job)
 
-    assert len(hydrated_job["skills"]) == 1
-    assert hydrated_job["skills"][0] == dict(name="Python", type="hard", value=None)
+    assert len(hydrated_job.skills) == 1
+    assert hydrated_job.skills[0] == dict(name="Python", type="hard", value=None)
 
-    assert len(hydrated_job["languages"]) == 1
-    assert hydrated_job["languages"][0] == dict(name="english", value=None)
+    assert len(hydrated_job.languages) == 1
+    assert hydrated_job.languages[0] == dict(name="english", value=None)
 
 
 @responses.activate
@@ -458,15 +481,17 @@ def test_PullJobsBaseAction_hydrate_job_with_parsing_failure(
         hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     section = dict(name="s", title=None, description="i speak english")
-    job = dict(reference="REF123", summary="I love Python", sections=[section])
+    job = HrflowJob.parse_obj(
+        dict(reference="REF123", summary="I love Python", sections=[section])
+    )
 
-    assert len(job.get("skills", [])) == 0
-    assert len(job.get("language", [])) == 0
+    assert job.skills == None
+    assert job.languages == None
 
     try:
         action.hydrate_job_with_parsing(job)
         assert False
-    except RuntimeError:
+    except HrflowError:
         pass
 
 
@@ -487,15 +512,17 @@ def test_PullJobsBaseAction_hydrate_job_with_parsing_with_empty_summary_and_only
         hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     section = dict(name="s", title=None, description='<html attr="Python"></html>')
-    job = dict(reference="REF123", sections=[section])
+    job = HrflowJob.parse_obj(
+        dict(reference="REF123", sections=[section], skills=None, languages=None)
+    )
 
-    assert "skills" not in job
-    assert "language" not in job
+    assert job.skills == None
+    assert job.languages == None
 
     hydrated_job = action.hydrate_job_with_parsing(job)
 
-    assert "skills" not in hydrated_job
-    assert "language" not in hydrated_job
+    assert hydrated_job.skills == None
+    assert hydrated_job.skills == None
     assert hydrated_job == job
 
 
@@ -515,7 +542,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_job_ref_none(
     generate_indexing_get_response,
 ):
     # Generate job
-    job = dict(reference=None)
+    job = HrflowJob(reference=None)
 
     # Build Action
     action = PullJobsBaseAction(
@@ -531,7 +558,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_job_not_in_board(
     generate_indexing_get_response,
 ):
     # Generate job
-    job = dict(reference="REF1")
+    job = HrflowJob(reference="REF1")
 
     # generated response
     message = "Invalid parameters. Unable to find object: job"
@@ -563,7 +590,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_not_archived_job_in_boa
     generate_indexing_get_response,
 ):
     # Generate job
-    job = dict(reference="REF1")
+    job = HrflowJob(reference="REF1")
 
     # generated response
     message = "Job details"
@@ -594,7 +621,7 @@ def test_PullJobsBaseAction_check_reference_in_board_fail(
     hrflow_client, generate_indexing_get_response
 ):
     # Generate job
-    job = dict(reference="REF1")
+    job = HrflowJob(reference="REF1")
 
     # generated response
     message = "Fail"
@@ -619,8 +646,8 @@ def test_PullJobsBaseAction_check_reference_in_board_fail(
     try:
         action.check_reference_in_board(job)
         assert False
-    except RuntimeError:
-        assert True
+    except HrflowError:
+        pass
 
 
 @responses.activate
@@ -629,7 +656,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_without_pa
     generate_indexing_get_response,
 ):
     # Generate job
-    job = dict(reference="REF1")
+    job = HrflowJob.parse_obj(dict(reference="REF1"))
 
     # generated response
     message = "Job details"
@@ -658,7 +685,8 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_without_pa
     )
 
     ## create a matcher to check if the JSON Body sent by the Connector is in the right shape and has the right values
-    expected_body = dict(board_key="abc", key="klm", reference="REF1")
+    expected_body = HrflowJob(key="klm", reference="REF1").dict()
+    expected_body["board_key"] = "abc"
     match = [responses.matchers.json_params_matcher(expected_body)]
     responses.add(
         responses.PUT,
@@ -681,7 +709,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_without_pa
     generate_indexing_get_response,
 ):
     # Generate job
-    job = dict(reference="REF1")
+    job = HrflowJob(reference="REF1")
 
     # generated response
     message = "Job details"
@@ -734,10 +762,10 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_in_board_w
 ):
     # Generate job
     section = dict(name="s", title=None, description="i speak english")
-    job = dict(reference="REF1", summary="I love Python", sections=[section])
+    job = HrflowJob(reference="REF1", summary="I love Python", sections=[section])
 
-    assert len(job.get("skills", [])) == 0
-    assert len(job.get("language", [])) == 0
+    assert job.skills == None
+    assert job.languages == None
 
     # generated response
     message = "Job details"
@@ -774,14 +802,14 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_in_board_w
     )
 
     ## create a matcher to check if the JSON Body sent by the Connector is in the right shape and has the right values
-    hydrated_job = dict(job)
+    hydrated_job = job.dict()
     hydrated_job["skills"] = [dict(name="Python", type="hard", value=None)]
     hydrated_job["languages"] = [dict(name="english", value=None)]
     hydrated_job["certifications"] = []
     hydrated_job["courses"] = []
     hydrated_job["tasks"] = []
-
-    expected_body = dict(board_key="abc", key="klm", **hydrated_job)
+    hydrated_job["key"] = "klm"
+    expected_body = dict(board_key="abc",**hydrated_job)
     match = [responses.matchers.json_params_matcher(expected_body)]
     responses.add(
         responses.PUT,
@@ -800,7 +828,7 @@ def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_in_board_w
 
 
 def test_PullJobsBaseAction_get_all_references_from_stream(hrflow_client):
-    jobs_in_stream = [dict(reference="REF1"), dict(reference="REF2")]
+    jobs_in_stream = [HrflowJob(reference="REF1"), HrflowJob(reference="REF2")]
     references_in_stream = ["REF1", "REF2"]
 
     class TestPullJobsAction(PullJobsBaseAction):
@@ -870,7 +898,10 @@ def push(self, data):
             assert data_list == ["logic_after_parsing"]
 
     def my_logic(data):
-        return not data == "format"
+        if data == "format":
+            return None
+        else:
+            return data
 
     action = MyPullJobsAction(
         hrflow_client=hrflow_client,
@@ -906,7 +937,10 @@ def push(self, data):
             assert data_list == ["logic"]
 
     def my_logic(data):
-        return not data == "format"
+        if not data == "format":
+            return data
+        else:
+            return None
 
     action = MyPullJobsAction(
         hrflow_client=hrflow_client,
@@ -942,7 +976,10 @@ def push(self, data):
             assert data_list == ["logic"]
 
     def my_logic(data):
-        return not data == "format"
+        if not data == "format":
+            return data
+        else:
+            return None
 
     action = MyPullJobsAction(
         hrflow_client=hrflow_client,
@@ -959,8 +996,8 @@ def my_logic(data):
 @responses.activate
 def test_PullJobsBaseAction_push_success(hrflow_client):
     # Mock requests and check data sent
-    job = dict(key="efg", reference="REF123")
-    expected_body = dict(board_key="abc", **job)
+    job = HrflowJob(key="efg", reference="REF123")
+    expected_body = dict(board_key="abc", **job.dict())
     returned_value = dict(code=200, data=dict(key="efg"))
     match = [responses.matchers.json_params_matcher(expected_body)]
     responses.add(
@@ -979,8 +1016,8 @@ def test_PullJobsBaseAction_push_success(hrflow_client):
 @responses.activate
 def test_PullJobsBaseAction_push_failure(hrflow_client):
     # Mock requests and check data sent
-    job = dict(key="efg", reference="REF123")
-    expected_body = dict(board_key="abc", **job)
+    job = HrflowJob(key="efg", reference="REF123")
+    expected_body = dict(board_key="abc", **job.dict())
     returned_value = dict(code=400, message="Test")
     match = [responses.matchers.json_params_matcher(expected_body)]
     responses.add(
@@ -996,7 +1033,7 @@ def test_PullJobsBaseAction_push_failure(hrflow_client):
     try:
         action.push([job])
         assert False
-    except RuntimeError:
+    except HrflowError:
         pass
 
 
@@ -1020,14 +1057,14 @@ def test_PushProfileBaseAction_pull_success(hrflow_client):
     )
 
     # Pull data
-    profile = Profile(key="efg", source=Source(key="abc"))
+    profile = HrflowProfile(key="efg", source=dict(key="abc"))
     action = PushProfileBaseAction(hrflow_client=hrflow_client(), profile=profile)
     profile_list_got = action.pull()
 
     # Check returned value
     assert len(profile_list_got) == 1
 
-    profile_got = profile_list_got[0]
+    profile_got = profile_list_got[0].dict()
     assert profile_got["key"] == "efg"
 
 
@@ -1046,20 +1083,19 @@ def test_PushProfileBaseAction_pull_failure(hrflow_client):
     )
 
     # Pull data
-    profile = Profile(key="efg", source=Source(key="abc"))
+    profile = HrflowProfile(key="efg", source=dict(key="abc"))
     action = PushProfileBaseAction(hrflow_client=hrflow_client(), profile=profile)
 
     try:
         action.pull()
         assert False
-    except RuntimeError:
+    except HrflowError:
         pass
 
 
 @responses.activate
 def test_PushProfileBaseAction_execute(hrflow_client):
-    profile = Profile(key="efg", source=Source(key="abc"))
-
+    profile = HrflowProfile(key="efg", source=dict(key="abc"))
     # Mock the `pull` & `push` method to do nothing
     class MyPushProfileAction(PushProfileBaseAction):
         def pull(self):
@@ -1083,7 +1119,7 @@ def push(self, data):
 
 @responses.activate
 def test_PushJobBaseAction_execute(hrflow_client):
-    job = Job(key="efg", board=Board(key="abc"))
+    job = HrflowJob(key="efg", board=dict(key="abc"))
 
     # Mock the `pull` & `push` method to do nothing
     class MyPushJobAction(PushJobBaseAction):
@@ -1121,14 +1157,14 @@ def test_PushJobBaseAction_pull_success(hrflow_client):
     )
 
     # Pull data
-    job = Job(key="efg", board=Board(key="abc"))
+    job = HrflowJob(key="efg", board=dict(key="abc"))
     action = PushJobBaseAction(hrflow_client=hrflow_client(), job=job)
-    profile_list_got = action.pull()
+    job_list_got = action.pull()
 
     # Check returned value
-    assert len(profile_list_got) == 1
+    assert len(job_list_got) == 1
 
-    profile_got = profile_list_got[0]
+    profile_got = job_list_got[0].dict()
     assert profile_got["key"] == "efg"
 
 
@@ -1147,13 +1183,13 @@ def test_PushJobBaseAction_pull_failure(hrflow_client):
     )
 
     # Pull data
-    job = Job(key="efg", board=Board(key="abc"))
+    job = HrflowJob(key="efg", board=dict(key="abc"))
     action = PushJobBaseAction(hrflow_client=hrflow_client(), job=job)
 
     try:
         action.pull()
         assert False
-    except RuntimeError:
+    except HrflowError:
         pass
 
 
@@ -1269,5 +1305,5 @@ def test_CatchProfileBaseAction_push_success(hrflow_client):
     try:
         action.push(dict(source_key="abc", profile_file="base64"))
         assert False
-    except RuntimeError:
-        pass
\ No newline at end of file
+    except HrflowError:
+        pass
diff --git a/tests/core/test_auth.py b/tests/core/test_auth.py
index 77ce94f67..5fe840902 100644
--- a/tests/core/test_auth.py
+++ b/tests/core/test_auth.py
@@ -1,6 +1,8 @@
 import responses
 import requests
+from unittest import mock
 
+from hrflow_connectors.core.error import AuthError
 from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody, OAuth2EmailPasswordBody
 from hrflow_connectors.core.auth import (
     Auth,
@@ -9,6 +11,7 @@
     XSmartTokenAuth,
     MonsterBodyAuth,
     XTaleezAuth,
+    OAuth2Session,
 )
 
 
@@ -143,7 +146,7 @@ def test_OAuth2PasswordCredentialsBody_get_access_token_failure():
     try:
         access_token = auth.get_access_token()
         assert False
-    except RuntimeError:
+    except AuthError:
         pass
 
 
@@ -302,7 +305,7 @@ def test_OAuth2EmailPasswordBody_get_access_token_failure():
     try:
         access_token = auth.get_access_token()
         assert False
-    except RuntimeError:
+    except AuthError:
         pass
 
 
@@ -342,3 +345,219 @@ def test_XTaleezAuth():
     assert authenticated_prepared_request.headers.get("X-taleez-api-secret") == key
     assert len(authenticated_prepared_request.headers) == 2
 
+
+@responses.activate
+def test_OAuth2Session_get_auth_code_success():
+    get_auth_code_response = {
+        "auth_code": "ceciestlauthcode"
+    }
+    auth_code_url = "http://test.test/auth_code"
+    access_token_url = "http://test.test/access_token"
+    session_token_url = "http://test.test/session_token"
+
+    auth_code = "007"
+    client_id = "007"
+    client_secret = "double0"
+    username = "bond"
+    password = "jb"
+
+    # Instanciate Auth to test
+    auth = OAuth2Session(
+        auth_code_url=auth_code_url,
+        access_token_url=access_token_url,
+        session_token_url=session_token_url,
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+        name="BhRestToken",
+    )
+
+    with mock.patch('requests.post') as requests_post_mock:
+        # On demande au Mock `requests.get(...).url` de retourner le code
+        redirected_url = "http://test.test/login?code={}".format(get_auth_code_response["auth_code"])
+        requests_post_mock.return_value.url = redirected_url
+
+        auth_code = auth.get_auth_code()
+        assert auth_code == get_auth_code_response["auth_code"]
+
+        # Enfin, on vérifie que la requête envoyée était correcte
+        # Pour cela, on va vérifier les appels effectués au Mock
+        # `requests_get_mock.mock_calls` donne la liste des appels
+        # Par exemple : `[call('http://google.fr'), call().__str__()]`
+        # On doit donc vérifier que l'appel à l'api a utilisé les bons paramètres
+        body_auth_code = dict(
+                client_id=client_id,
+                response_type="code",
+                action="Login",
+                username=username,
+                password=password
+        )
+
+        expected_call = mock.call(auth_code_url, params=body_auth_code)
+        assert expected_call in requests_post_mock.mock_calls
+
+
+@responses.activate
+def test_OAuth2Session_get_auth_code_failure():
+    get_auth_code_response = {
+        "auth_code": "ceciestlauthcode"
+    }
+    auth_code_url = "http://test.test/auth_code"
+    access_token_url = "http://test.test/access_token"
+    session_token_url = "http://test.test/session_token"
+
+    auth_code = "007"
+    client_id = "007"
+    client_secret = "double0"
+    username = "bond"
+    password = "jb"
+
+    # Instanciate Auth to test
+    auth = OAuth2Session(
+        auth_code_url=auth_code_url,
+        access_token_url=access_token_url,
+        session_token_url=session_token_url,
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+        name="BhRestToken",
+    )
+
+    with mock.patch('requests.post') as requests_post_mock:
+        # On demande au Mock `requests.get(...).url` de retourner le code
+        redirected_url = "http://test.test/login?notcode={}".format(get_auth_code_response["auth_code"])
+        requests_post_mock.return_value.url = redirected_url
+
+        try:
+            auth.get_auth_code()
+            assert False
+        except AuthError:
+            pass
+
+
+
+@responses.activate
+def test_OAuth2Session_get_access_token():
+    get_access_token_response = {
+        "access_token": "ceciestlacesstoken"
+    }
+
+    auth_code_url = "http://test.test/auth_code"
+    access_token_url = "http://test.test/access_token"
+    session_token_url = "http://test.test/session_token"
+
+    auth_code = "007"
+    client_id = "007"
+    client_secret = "double0"
+    username = "bond"
+    password = "jb"
+
+    # Instanciate Auth to test
+    auth = OAuth2Session(
+        auth_code_url=auth_code_url,
+        access_token_url=access_token_url,
+        session_token_url=session_token_url,
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+        name="BhRestToken",
+    )
+
+    # build Mock for get_access_token request
+    get_access_token_request_body = dict(
+        grant_type="authorization_code",
+        code=auth_code,
+        client_id=client_id,
+        client_secret=client_secret,
+    )
+
+    get_access_match = [responses.matchers.query_param_matcher(get_access_token_request_body)]
+    responses.add(
+        responses.POST,
+        access_token_url,
+        status=200,
+        json=get_access_token_response,
+        match=get_access_match,
+    )
+
+    access_token = auth.get_access_token(auth_code)
+    assert access_token == get_access_token_response["access_token"]
+
+
+
+@responses.activate
+def test_OAuth2Session_get_session_token():
+    get_access_token_response = {
+        "BhRestToken": "ceciestlesessiontoken"
+    }
+
+    auth_code_url = "https://www.site.com/"
+    access_token_url = "http://test.test/access_token"
+    session_token_url = "http://test.test/session_token"
+
+    client_id = "007"
+    client_secret = "double0"
+    username = "bond"
+    password = "jb"
+    access_token = "ceciestlacesstoken"
+
+    # Instanciate Auth to test
+    auth = OAuth2Session(
+        auth_code_url=auth_code_url,
+        access_token_url=access_token_url,
+        session_token_url=session_token_url,
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+        name="BhRestToken",
+    )
+    # build Mock for get_access_token request
+    session_token_request_body = dict(
+        version="*",
+        access_token=access_token
+    )
+
+    get_access_match = [responses.matchers.query_param_matcher(session_token_request_body)]
+    responses.add(
+        responses.POST,
+        session_token_url,
+        status=200,
+        json=get_access_token_response,
+        match=get_access_match,
+    )
+
+    session_token = auth.get_session_token(access_token)
+    assert session_token == get_access_token_response["BhRestToken"]
+
+def test_OAuth2Session_call():
+    class MyOAuth2Session(OAuth2Session):
+        def get_auth_code(self):
+            return "auth_code"
+        
+        def get_access_token(self, auth_code):
+            assert auth_code == "auth_code"
+            return "access_token"
+        
+        def get_session_token(self, access_token):
+            assert access_token == "access_token"
+            return "session_token"
+
+    auth = MyOAuth2Session(
+        auth_code_url="auth_code_url",
+        access_token_url="access_token_url",
+        session_token_url="session_token_url",
+        client_id="client_id",
+        client_secret="client_secret",
+        username="username",
+        password="password",
+        name="BhRestToken",
+    )
+
+    request = requests.Request(method="POST", url="http://test.test/", headers=dict(abc="efg"))
+    prepared_request = request.prepare()
+    request_with_auth = auth(prepared_request)
+    assert request_with_auth.headers["BhRestToken"] == "session_token"
\ No newline at end of file
diff --git a/tests/core/test_connector.py b/tests/core/test_connector.py
index 23fdbe3bc..c80d9a899 100644
--- a/tests/core/test_connector.py
+++ b/tests/core/test_connector.py
@@ -1,33 +1,33 @@
 from hrflow_connectors.core.connector import Connector
 
 
-def test_Connector_pull_jobs():
+def test_Connector_pull_jobs(hrflow_client):
     try:
-        Connector.pull_jobs()
+        Connector.pull_jobs(hrflow_client)
         assert False
     except NotImplementedError:
         assert True
 
 
-def test_Connector_pull_profiles():
+def test_Connector_pull_profiles(hrflow_client):
     try:
-        Connector.pull_profiles()
+        Connector.pull_profiles(hrflow_client)
         assert False
     except NotImplementedError:
         assert True
 
 
-def test_Connector_push_job():
+def test_Connector_push_job(hrflow_client):
     try:
-        Connector.push_job()
+        Connector.push_job(hrflow_client)
         assert False
     except NotImplementedError:
         assert True
 
 
-def test_Connector_push_profile():
+def test_Connector_push_profile(hrflow_client):
     try:
-        Connector.push_profile()
+        Connector.push_profile(hrflow_client)
         assert False
     except NotImplementedError:
         assert True
\ No newline at end of file
diff --git a/tests/core/test_error.py b/tests/core/test_error.py
new file mode 100644
index 000000000..cc36f3f50
--- /dev/null
+++ b/tests/core/test_error.py
@@ -0,0 +1,61 @@
+import requests
+import responses
+
+from hrflow_connectors.core.error import PullError, PushError, HrflowError
+
+
+@responses.activate
+def test_PullError():
+    # Generate request response
+    responses.add(responses.GET, "http://test.test", body="server error", status=401)
+    response = requests.get("http://test.test")
+
+    # Expected error output
+    expected_output = "Failed to pull\n"
+    expected_output += "Message: `My message`\n"
+    expected_output += "Status code: `401`\n"
+    expected_output += "Server response: `b'server error'`"
+
+    # Test error
+    try:
+        raise PullError(response, Message="My message")
+    except PullError as e:
+        assert e.__str__() == expected_output
+
+
+@responses.activate
+def test_PushError():
+    # Generate request response
+    responses.add(responses.GET, "http://test.test", body="server error", status=401)
+    response = requests.get("http://test.test")
+
+    # Expected error output
+    expected_output = "Failed to push\n"
+    expected_output += "Message: `My message`\n"
+    expected_output += "Status code: `401`\n"
+    expected_output += "Server response: `b'server error'`"
+
+    # Test error
+    try:
+        raise PushError(response, Message="My message")
+    except PushError as e:
+        assert e.__str__() == expected_output
+
+
+def test_HrflowError():
+    # Create Hrflow response
+    response = dict(code=500, message="Internal error", data=[])
+
+    # Expected error output
+    expected_output = "Failed to communicate with Hrflow\n"
+    expected_output += "Message: `My message`\n"
+    expected_output += "Status code: `500`\n"
+    expected_output += "Hrflow message: `Internal error`"
+
+    # Test error
+    try:
+        raise HrflowError(
+            response, title="Failed to communicate with Hrflow", Message="My message"
+        )
+    except HrflowError as e:
+        assert e.__str__() == expected_output
\ No newline at end of file
diff --git a/tests/utils/test_adress_to_lat_long.py b/tests/utils/test_adress_to_lat_long.py
index 67db4f486..fe9c236e6 100644
--- a/tests/utils/test_adress_to_lat_long.py
+++ b/tests/utils/test_adress_to_lat_long.py
@@ -52,7 +52,7 @@ def test_cities_code_lat_long_mapping():
 
 
 @responses.activate
-def test_get_lat_lng(credentials):
+def test_get_lat_lng():
 
     request_url = "https://geocode.search.hereapi.com/v1/geocode"
     body = {
@@ -99,13 +99,13 @@ def test_get_lat_lng(credentials):
                     cities_codes_dict,
                     cities_names_dict,
                     departments_codes_dict,
-                    credentials["here"]["HERE_API_KEY"]
+                    api_key="abc_here_key"
                 )
         assert name == expected
 
 
 @responses.activate
-def test_get_lat_lng_not_found_by_here(credentials):
+def test_get_lat_lng_not_found_by_here():
 
     cities_codes_dict = get_cities_code_lat_long_mapping()
 
@@ -125,13 +125,13 @@ def test_get_lat_lng_not_found_by_here(credentials):
         cities_codes_dict,
         cities_names_dict,
         departments_codes_dict,
-        credentials["here"]["HERE_API_KEY"]
+        api_key="abc_here_key"
     )
     assert name is None
 
 
 @responses.activate
-def test_get_lat_lng_not_api_key_here(credentials):
+def test_get_lat_lng_not_api_key_here():
 
     cities_codes_dict = get_cities_code_lat_long_mapping()
 
diff --git a/tests/utils/test_config.py b/tests/utils/test_config.py
new file mode 100644
index 000000000..91ec08f0b
--- /dev/null
+++ b/tests/utils/test_config.py
@@ -0,0 +1,68 @@
+import os
+from hrflow_connectors.utils.config import ConfigError
+
+def test_Config_same_var_in_env_file(pytestconfig, config):
+    # Read `.env` file
+    env_path = os.path.join(pytestconfig.rootpath, ".env")
+    with open(env_path, "r") as env_file:
+        env_line_list = env_file.readlines()
+
+    def is_hrflow_connectors_var(line):
+        return line.startswith("HRFLOW_CONNECTORS_")
+
+    def clean_line(line):
+        return line.strip()
+
+    def remove_prefix(line):
+        prefix_last_char_position = len("HRFLOW_CONNECTORS_")
+        equal_char_position = line.index("=")
+        var_name = line[prefix_last_char_position:equal_char_position]
+        return var_name.strip()
+
+    cleaned_line_list = map(clean_line, env_line_list)
+    filtered_line_list = filter(is_hrflow_connectors_var, cleaned_line_list)
+    var_list_expected = map(remove_prefix, filtered_line_list)
+    var_list_expected = list(var_list_expected)
+    var_list_expected.sort()
+
+    # Get var list in Config
+    var_list_got = [var_name for var_name in config.__dict__]
+    var_list_got = list(var_list_got)
+    var_list_got.sort()
+
+    # Check var list
+    assert var_list_expected == var_list_got
+
+
+def test_Config_check_each_value(config):
+    # Get all environment variables
+    def is_hrflow_connectors_var(line):
+        return line.startswith("HRFLOW_CONNECTORS_")
+
+    def remove_prefix(line):
+        prefix_last_char_position = len("HRFLOW_CONNECTORS_")
+        return line[prefix_last_char_position:]
+
+    environement_var_list = [var_name for var_name in os.environ]
+    hrflow_connectors_var_list_expected = filter(
+        is_hrflow_connectors_var, environement_var_list
+    )
+    hrflow_connectors_var_list_expected = list(hrflow_connectors_var_list_expected)
+    
+    hrflow_connectors_var_expected = dict()
+    for var_name in hrflow_connectors_var_list_expected:
+        var_name_without_prefix = remove_prefix(var_name)
+        hrflow_connectors_var_expected[var_name_without_prefix] = os.environ[var_name]
+
+    # Get var in Config
+    hrflow_connectors_var_got = config.__dict__
+
+    # Check var
+    assert hrflow_connectors_var_expected == hrflow_connectors_var_got
+
+def test_Config_field_not_found(config):
+    try:
+        config.THIS_FIELD_DOES_NOT_EXIST
+        assert False
+    except ConfigError:
+        pass
\ No newline at end of file
diff --git a/tests/utils/test_event_parser.py b/tests/utils/test_event_parser.py
index 34c7e18fd..4d9a28507 100644
--- a/tests/utils/test_event_parser.py
+++ b/tests/utils/test_event_parser.py
@@ -1,4 +1,5 @@
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Job
+from hrflow_connectors.utils.hrflow import EventParser
+from hrflow_connectors.utils.schemas import HrflowProfile, HrflowJob
 
 
 def test_event_with_profile():
@@ -6,7 +7,7 @@ def test_event_with_profile():
     event = EventParser(request=request)
     profile = event.get_profile()
     assert profile is not None
-    assert isinstance(profile, Profile)
+    assert isinstance(profile, HrflowProfile)
     assert profile.key == "abc"
     assert profile.source.key == "efg"
 
@@ -24,7 +25,7 @@ def test_event_with_profile_in_listened_source():
     source_to_listen = ["xyz", "efg"]
     profile = event.get_profile(source_to_listen=source_to_listen)
     assert profile is not None
-    assert isinstance(profile, Profile)
+    assert isinstance(profile, HrflowProfile)
     assert profile.key == "abc"
     assert profile.source.key == "efg"
 
@@ -42,7 +43,7 @@ def test_event_with_job():
     event = EventParser(request=request)
     job = event.get_job()
     assert job is not None
-    assert isinstance(job, Job)
+    assert isinstance(job, HrflowJob)
     assert job.key == "abc"
     assert job.board.key == "efg"
 
@@ -60,7 +61,7 @@ def test_event_with_job_in_listened_board():
     board_to_listen = ["xyz", "efg"]
     job = event.get_job(board_to_listen=board_to_listen)
     assert job is not None
-    assert isinstance(job, Job)
+    assert isinstance(job, HrflowJob)
     assert job.key == "abc"
     assert job.board.key == "efg"
 
diff --git a/tests/utils/test_job_and_board.py b/tests/utils/test_job_and_board.py
deleted file mode 100644
index 4b888c508..000000000
--- a/tests/utils/test_job_and_board.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from hrflow_connectors.utils.hrflow import Job, Board
-
-
-def test_job():
-    job = Job(key="abc", board=Board(key="efg"))
-    assert job.key == "abc"
-    assert job.board.key == "efg"
-
-
-def test_board():
-    board = Board(key="efg")
-    assert board.key == "efg"
-
-
-def test_parse_job():
-    job_dict = dict(key="abc", board=dict(key="efg"))
-    job_obj = Job.parse_obj(job_dict)
-    assert job_obj.key == "abc"
-    assert job_obj.board.key == "efg"
\ No newline at end of file
diff --git a/tests/utils/test_profile_and_source.py b/tests/utils/test_profile_and_source.py
deleted file mode 100644
index a2e2dd769..000000000
--- a/tests/utils/test_profile_and_source.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from hrflow_connectors.utils.hrflow import Profile, Source
-
-def test_profile():
-    profile = Profile(key="abc", source=Source(key="efg"))
-    assert profile.key == "abc"
-    assert profile.source.key == "efg"
-
-def test_source():
-    source = Source(key="efg")
-    assert source.key == "efg"
-
-def test_parse_profile():
-    profile_dict = dict(key="abc", source=dict(key="efg"))
-    profile_obj = Profile.parse_obj(profile_dict)
-    assert profile_obj.key == "abc"
-    assert profile_obj.source.key == "efg"
\ No newline at end of file