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

Issue:
Refacto: rename core class & SAP Connector & fix monster `PushJobAction`
This merge will :
* fixe typo `formated` -> `formatted`
* rename class `...Action` in `core` -> `...BaseAction`
* refacto SAP connector
* fix monster `PushJobAction`


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f739987c6..f9c403f4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,4 +40,34 @@ All notable changes to this project will be documented in this file.
 ### Changed
 * Pipeline in Action and BoardAction to use `format_switcher` instead of `format`
 ### Fixed
-* HTTPStream : static private attributes become public attributes
\ No newline at end of file
+* HTTPStream : static private attributes become public attributes
+
+## Version 1.0.0 - 2022/01/19
+### Added
+* Add `Connector` interface
+* add `PullBaseAction` class
+* add `PullBaseAction` class
+* Breezy.hr Connector
+* Ceridian Connector
+* Greenhouse Connector
+* Monster Connector
+* Recruitee Connector
+* SAPSuccessfactors Connector
+* Smartrecruiters Connector : `PushProfileAction`
+* Taleez Connector
+* Workable Connector
+### Changed
+* Facilitate `import`
+* Rename `Action` -> `BaseAction`
+* Rename `BoardAction` -> `PullJobsBaseAction`
+* Rename `ProfileDestinationAction` -> `PushProfileBaseAction`
+* Rename `JobDestinationAction` -> `PushJobBaseAction`
+* `Auth` is compatible with `requests` auth
+* Change the package architecture
+* Move docs of each action into `docs/`
+* Improve documentation
+### Fixed
+* Unescape HTML in Hrflow.ai fields `summary` & `sections`
+### Removed
+* Remove `HTTPStream` and replace with `requests.Request`
+* Remove all crawlers : Craigslist and Indeed
\ No newline at end of file
diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index b08ed313d..041251824 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -12,7 +12,7 @@ In the action execution pipeline, here is where the `logics` are located:
 3. Apply `format` to each element
 4. `push` the data
 
-**All `Action`s have the following parameters:**
+**All actions have the following parameters:**
 * `logics` which is a list of function names. This will be used to filter the elements returned by `pull`.
 * `global_scope` is a dictionary containing the current scope's global variables.
 * `local_scope` is a dictionary containing the current scope's local variables.
@@ -30,12 +30,12 @@ Most of the time, you would just write `global_scope=globals()` and `local_scope
 
 ### Example
 ```python
-from hrflow_connectors.core.action import Action
+from hrflow_connectors.core.action import BaseAction
 
 def filter_element1_with_value1(element):
     return element.get("element1") == "value1"
 
-action = Action(
+action = BaseAction(
     logics=["filter_element1_with_value1"],
     global_scope=globals(),
     local_scope=locals(),
@@ -75,9 +75,9 @@ By default, all connectors have a `format` provided and ready to use. If you wan
 
 ### By inheriting & overriding `format`
 ```python
-from hrflow_connectors.core.action import Action
+from hrflow_connectors.core.action import BaseAction
 
-class ActionWithMyFormat(Action)
+class ActionWithMyFormat(BaseAction)
     def format(data):
         # my format
         job = dict(my_ref=data["id"])
@@ -88,7 +88,7 @@ action.execute()
 ```
 ### By using a format function external
 
-**All `Action`s have the following parameters:**
+**All actions have the following parameters:**
 * `format_function_name` which is the function name. This will be used to format each element returned by `pull`.
 * `global_scope` is a dictionary containing the current scope's global variables.
 * `local_scope` is a dictionary containing the current scope's local variables.
@@ -103,14 +103,14 @@ Most of the time, you would just write `global_scope=globals()` and `local_scope
 ℹ️ To find out more about the scopes and how the `eval` function works: https://docs.python.org/3/library/functions.html#eval
 
 ```python
-from hrflow_connectors.core.action import Action
+from hrflow_connectors.core.action import BaseAction
 
 def my_format(data):
     # my format
     job = dict(my_ref=data["id"])
     return job
 
-action = Action(
+action = BaseAction(
     format_function_name="my_format",
     global_scope=globals(),
     local_scope=locals(),
@@ -119,19 +119,18 @@ action.execute()
 ```
 
 ## Using parsing to enrich a job
-**All `Action` for connectors found in `connectors/boards` have an option to enrich a job with parsing.**
+**All `PullJobsAction`s have an option to enrich a job with parsing.**
 
 Parsing will parse all the text fields and extract the characteristics of the job and add them to the appropriate fields in ***Hrflow.ai***: `skills`, `languages`, `certifications`, `courses`, `tasks`, ...
 
 **To enable parsing, simply turn on the option `hydrate_with_parsing`.**
 ```python
-from hrflow_connectors.core.action import BoardAction
+from hrflow_connectors import MyConnector
 
-action = BoardAction(hydrate_with_parsing=True, ...)
-action.execute()
+MyConnector.pull_jobs(hydrate_with_parsing=True, ...)
 ```
 ## Automatically archive a job when it is deleted from the stream
-**All `Action` for connectors found in `connectors/boards` have an `archive_deleted_jobs_from_stream` option to automatically archive a Board job when it is deleted from the stream.**
+***All `PullJobsAction`s have an `archive_deleted_jobs_from_stream` option to automatically archive a Board job when it is deleted from the stream.**
 
 This can be useful for synchronizing a board with a job stream.
 In this case, you only need to enable the `archive_deleted_jobs_from_stream=True` option.
@@ -140,10 +139,9 @@ But in some cases, you may only want to add jobs without worrying that the job i
 
 Here is how it looks in an example :
 ```python
-from hrflow_connectors.core.action import BoardAction
+from hrflow_connectors import MyConnector
 
-action = BoardAction(archive_deleted_jobs_from_stream=True, ...)
-action.execute()
+MyConnector.pull_jobs(archive_deleted_jobs_from_stream=True, ...)
 ```
 
 ## Using `hrflow_connector` in a CATCH workflow to get a profile
@@ -156,7 +154,7 @@ This method retrieves information from a profile if it exists in `_request`.
 
 **Let's take a simple example:**
 ```python
-from hrflow_connectors.connectors.core.action import ProfileDestinationAction
+from hrflow_connectors import MyConnector
 from hrflow_connectors.utils.hrflow import EventParser
 
 def workflow(_request, settings):
@@ -166,15 +164,14 @@ def workflow(_request, settings):
     event = EventParser(request=_request)
     profile = event.get_profile()
     if profile is not None:
-        action = ProfileDestinationAction(profile=profile, ...)
-        response = action.execute()
+        response = MyConnector.push_profile(profile=profile, ...)
         return response
 ```
 
 If now you only want to process the profiles added **in the sources with the following keys `MY_SOURCE_KEY_1` and `MY_SOURCE_KEY_2`**.
 Then you just have to write :
 ```python
-from hrflow_connectors.connectors.core.action import ProfileDestinationAction
+from hrflow_connectors import MyConnector
 from hrflow_connectors.utils.hrflow import EventParser
 
 def workflow(_request, settings):
@@ -184,8 +181,7 @@ def workflow(_request, settings):
     event = EventParser(request=_request)
     profile = event.get_profile(source_to_listen=["MY_SOURCE_KEY_1", "MY_SOURCE_KEY_2"])
     if profile is not None:
-        action = ProfileDestinationAction(profile=profile, ...)
-        response = action.execute()
+        response = MyConnector.push_profile(profile=profile, ...)
         return response
 ```
 So **if a profile is added to the source with the key `OTHER_SOURCE_KEY`** then `get_profile` would return `None` and **that profile would be ignored** from the rest of the processing.
diff --git a/README.md b/README.md
index 1b331b0d6..38f11a382 100644
--- a/README.md
+++ b/README.md
@@ -25,21 +25,62 @@ This project is designed to simply and easily handle,
 complex HR integrations by using [**workflows**](https://developers.hrflow.ai/docs/workflows) feature.
 <br/>
 
+
 ## About HrFlow.ai
-  https://www.HrFlow.ai is **an API first company that provides the most sophisticated AI-Powered JOB & PROFILE API**. Corporates and Software vendors can leverage our technology layers to Parse, Enrich and Score both job and candidate data. The platform supports +200 apps, pipelines, and code integrations so you can automate workflows with your favorite tools.
-  - Our Developers documentation : https://developers.hrflow.ai/
-  - Our API list (Parsing, Revealing, Embedding, Searching, Scoring, Reasoning) : https://www.hrflow.ai/api
-  - Our cool demos labs : https://labs.hrflow.ai
+[HrFlow.ai](https://hrflow.ai/) is **an API first company that provides the most sophisticated AI-Powered Job & Profile API**. We help HR Tech, Staffing Agencies, and Large employers to thrive in a high-frequency labor market.
+
+**[HrFlow.ai](https://hrflow.ai/) is on a mission to make AI and data integration pipelines a commodity in the HR Industry:**
+  1. **Unify**: Link your Talent Data channels with a few clicks, so they can share data.
+  2. **Understand**: Leverage our AI solutions to process your Talent Data.
+  3. **Automate**: Sync data between your tools and build workflows that meet your business logic.
 
+## :electric_plug: List of Connectors
+
+| Name | Type | Available |
+| - | - | - |
+| **ADP** |  | :hourglass: |
+| [**Breezy.hr**](src/hrflow_connectors/connectors/breezyhr) | ATS | :heavy_check_mark: |
+| **Cegid (Meta4)** |  | :hourglass: |
+| [**Ceridian**](src/hrflow_connectors/connectors/ceridian) | HCM |:heavy_check_mark: |
+| **Cornerstone OnDemand** |  | :hourglass: |
+| [**Crosstalent**](src/hrflow_connectors/connectors/crosstalent) | ATS | :heavy_check_mark: |
+| **Digitalrecruiters** | ATS | :hourglass: |
+| **Fieldglass SAP** | Recruiting software | :hourglass: |
+| [**Flatchr**](src/hrflow_connectors/connectors/flatchr/) | ATS | :heavy_check_mark: |
+| [**Greenhouse**](src/hrflow_connectors/connectors/greenhouse) | ATS | :heavy_check_mark: |
+| **ICIMS** |  | :hourglass: |
+| **Jobvite** |  | :hourglass: |
+| **Kronos (UKG)** |  | :hourglass: |
+| **Laponi** | Job board | :hourglass: |
+| **Lever** |  | :hourglass:  |
+| **Mailchimp** | Marketing tools | :hourglass: |
+| **Microsoft Dynamics** | HCM CLoud | :hourglass: |
+| [**Monster**](src/hrflow_connectors/connectors/monster/) | Job board | :heavy_check_mark: |
+| **Oracle** | Cloud Apps | :hourglass: |
+| [**Recruitee**](src/hrflow_connectors/connectors/recruitee/) | ATS | :heavy_check_mark: |
+| **RecruitBox** |  | :hourglass: |
+| [**SAPSuccessfactors**](src/hrflow_connectors/connectors/sapsuccessfactors/) | Cloud Apps for HR | :heavy_check_mark: |
+| **Salesforce** |  | :hourglass: |
+| [**Smartrecruiters**](src/hrflow_connectors/connectors/smartrecruiters/) | ATS | :heavy_check_mark: |
+| **Staffme** | Job board | :hourglass: |
+| [**Taleez**](src/hrflow_connectors/connectors/taleez/)| ATS | :heavy_check_mark: |
+| **Talentsoft** |  | :hourglass: |
+| **Talentlink** |  | :hourglass: |
+| **Twilio** | Marketing tools | :hourglass: |
+| **Ultimate Software (UKG)** |  | :hourglass: |
+| [**Workable**](src/hrflow_connectors/connectors/workable/) | ATS | :heavy_check_mark: |
+| **Workday** |  | :hourglass: |
+| [**XML**](src/hrflow_connectors/connectors/xml/) | Job board | :heavy_check_mark: |
 
 ## 🪄 Quickstart
 ### What I can do?
 With Hrflow Connector, you can **synchronize** and **process** multiple **HR data streams** in just a few lines of code.
 
-You can do 3 types of actions:
-* `Job flow` :arrow_right: ***`Hrflow.ai Board`***
-* `Profile flow` :arrow_right: ***`Hrflow.ai Source`***
-* ***`Hrflow.ai`*** :arrow_right: `External destination`
+You can do 4 types of actions:
+* Pull jobs : `Job flow` :arrow_right: ***`Hrflow.ai Board`***
+* Pull profiles : `Profile flow` :arrow_right: ***`Hrflow.ai Source`***
+* Push job : ***`Hrflow.ai Board`*** :arrow_right: `External destination`
+* Push profile : ***`Hrflow.ai Source`*** :arrow_right: `External destination`
 
 The features offered by this package:
 * **Synchronize an entire data** stream with a ready-to-use solution
@@ -53,36 +94,18 @@ The features offered by this package:
 * [🔑 Get your API Key](https://developers.hrflow.ai/docs/api-authentification)
 
 1. **`pip install hrflow-connectors`**
-2. **I configure the connector** that I want to use. Let's take for example Crosstalent [`GetAllJobs`](src/hrflow_connectors/connectors/boards/crosstalent).
+2. **Configure the connector**. Let's take for example Greenhouse [Pull jobs](src/hrflow_connectors/connectors/greenhouse).
 ```python
+from hrflow_connectors import Greenhouse
 from hrflow import Hrflow
 
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.connectors.boards.crosstalent import GetAllJobs
-
-# 2.1 Hrflow client configuration
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-
-# 2.2 Configuring authentication to Crosstalent via the Salesforce API
-access_token_url = "https://test.salesforce.com/services/oauth2/token"
-auth = OAuth2PasswordCredentialsBody(
-    access_token_url=access_token_url,
-    client_id="MY_CLIENT_ID",
-    client_secret="MY_CLIENT_SECRET",
-    username="MY_USERNAME",
-    password="MY_PASSWORD",
-)
-
-# 2.3 General connector configuration
-action = GetAllJobs(
-    auth=auth,
-    subdomain="MY_SUBDOMAIN",
+Greenhouse.pull_jobs(
+    board_token="MY_GREENHOUSE_BOARD_TOKEN",
     hrflow_client=client,
-    board_key="MY_BOARD_KEY",
-    hydrate_with_parsing=True,
+    board_key="MY_HRFLOW_BOARD_KEY"
 )
 ```
-3.  I write the line of code `action.execute()` to **run the connector.**
 
 🐇 **TADA! You have just called your first connector.**
 
@@ -98,68 +121,9 @@ The project mainly uses 4 packages :
 * `requests=="2.26.0"`
 * `hrflow=="1.9.0"`
 * `pydantic=="1.7.4"`
-* `selenium=="3.141.0"`
 
 **To find the list of dependencies, you can look at the [`pyproject.toml`](pyproject.toml) file**
 
-## :electric_plug: List of Connectors
-- **ADP** (soon)
-- **Craigslist** : [`CraigslistFeed`](src/hrflow_connectors/connectors/boards/craigslist/)
-- **Careerbuilder** [`CareerBuilderFeed`](src/hrflow_connectors/connectors/boards/careerbuilder)
-- **Cegid(Meta4)** (soon)
-- **Ceridian** [`PullJobs`](src/hrflow_connectors/connectors/boards/ceridian)
-- **Cornerstone OnDemand** (soon)
-- **Crosstalent** : [`GetAllJobs`](src/hrflow_connectors/connectors/boards/crosstalent), [`PushProfile`](src/hrflow_connectors/connectors/destinations/crosstalent)
-- **Digitalrecruiters** (soon)
-- **Flatchr** : [`PushProfile`](src/hrflow_connectors/connectors/destinations/flatchr/), [`EnrichProfile`](src/hrflow_connectors/connectors/destinations/flatchr/)
-- **Greenhouse** : [`GetAllJobs`](src/hrflow_connectors/connectors/boards/greenhouse), [`PushProfile`](src/hrflow_connectors/connectors/destinations/greenhouse)
-- **Indeed** : [`IndeedFeed`](src/hrflow_connectors/connectors/boards/indeed)
-- **Kronos(UKG)** (soon)
-- **Laponi** (soon)
-- **Lever** (soon)
-- **Mailchimp** (soon)
-- **Monster** : [`PushJob`](src/hrflow_connectors/connectors/boards/monster), [`PullProfile`](src/hrflow_connectors/connectors/destinations/monster)
-- **Oracle** (soon)
-- **Recruitee** : [`PullJobs`](src/hrflow_connectors/connectors/boards/recruitee), [`PushProfile`](src/hrflow_connectors/connectors/destinations/recruitee)
-- **SAP Successfactors** [`PullJobs`](src/hrflow_connectors/connectors/boards/sapsuccessfactors), [`PushProfile`](src/hrflow_connectors/connectors/destinations/sapsuccessfactors)
-- **Salesforce** (soon)
-- **Smartrecruiters** : [`GetAllJobs`](src/hrflow_connectors/connectors/boards/smartrecruiters/), [`PushProfile`](src/hrflow_connectors/connectors/destinations/smartrecruiters)
-- **Staffme** (soon)
-- **Talentsoft** (soon)
-- **Twilio** (soon)
-- **Ultimate Software(UKG)** (soon)
-- **Workable** : [`PullJobs`](src/hrflow_connectors/connectors/boards/workable)
-- **Workday** (soon)
-
-## 🧐 What's inside?
-**A quick look at the project structure.**
-
-* **`legacy/`** : Folder containing the old code snippets that we used to connect the data streams in the workflows
-* **`src/hrflow_connectors/connectors/`** : Folder containing the connectors sorted according to the type of their actions
-  * **`boards/`** : `Job flow` :arrow_right: ***`Hrflow.ai Board`*** Folder containing the set of connectors that synchronise the job stream in a ***Hrflow.ai Board***.
-    * **`ConnectorName/`** : Connector folder named `ConnectorName`.
-      * **`actions.py`** : File containing all the actions of a connector.
-      * **`spec.py`** : File listing the available actions and the particularities of the connector (version, ...)
-      * **`README.md`** : File documenting the use of the connector.
-  * **`sources/`** : `Profile flow` :arrow_right: ***`Hrflow.ai Source`*** Folder containing the set of connectors that synchronise the profile stream in a ***Hrflow.ai Source***. The architecture is the same as for `boards/`.
-  * **`destinations/`** : ***`Hrflow.ai`*** :arrow_right: `External destination` Folder containing all the connectors that export talent data from Hrflow to an external destination. The architecture is the same as for `boards/`.
-* **`src/hrflow_connectors/core/`** : Containing the core of the connectors shared by all.
-* **`src/hrflow_connectors/utils/`** : Containing the useful out-of-context functions of the package.
-* **`tests/`** : Folder containing all unit and functional tests of the project.
-* **`.coveragerc`** : Configuration file for evaluating test coverage with `pytest-cov`.
-* **`.gitignore`** : This file tells git which files it should not track / not maintain a version history for.
-* **`AUTHORS.md`** : File listing the authors who participated in the project code.
-* **`CHANGELOG.md`** : File listing the changes made for each version.
-* **`CONTRIBUTING.md`** : File describing how to participate in the project.
-* **`LICENSE`** : Project license.
-* **`README.md`**: A text file containing useful reference information about this project.
-* **`VERSION`** : File containing the project version.
-* **`poetry.lock`** : File containing the exact versions of the dependencies to ensure that the package versions are consistent for everyone working on your project.
-* **`pyproject.toml`** : File that orchestrates the project and its dependencies.
-* **`pytest.ini`** : The main configuration file of pytest, which can change the running mode of pytest.
-* **`setup.cfg`** : A Python package that supports providing all of a Python distribution's metadata and build configuration rather than in the `setup.py` script.
-* **`setup.py`** : The minimal setup.py which calls the configuration in `setup.cfg`.
-
 ## :woman_technologist: Contributions
 
 Please feel free to contribute to the quality of this content by
@@ -170,7 +134,11 @@ appreciated.
 
 👉 **To find out more about how to proceed, the rules and conventions to follow, read carefully [`CONTRIBUTING.md`](CONTRIBUTING.md).**
 
+## 🔗 Resources
+* Our Developers documentation : https://developers.hrflow.ai/
+* Our API list (Parsing, Revealing, Embedding, Searching, Scoring, Reasoning) : https://www.hrflow.ai/api
+* Our cool demos labs : https://labs.hrflow.ai
 
 ## :page_with_curl: License
 
-See the [`LICENSE`](LICENSE) file for licensing information.
\ No newline at end of file
+See the [`LICENSE`](LICENSE) file for licensing information.
diff --git a/VERSION b/VERSION
index 0ea3a944b..afaf360d3 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.2.0
+1.0.0
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 51679e6bc..b159233d4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -155,18 +155,6 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
-[[package]]
-name = "configparser"
-version = "5.2.0"
-description = "Updated configparser from Python 3.8 for Python 2.6+."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "types-backports", "pytest-black (>=0.3.7)", "pytest-mypy"]
-
 [[package]]
 name = "coverage"
 version = "6.2"
@@ -181,17 +169,6 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
 [package.extras]
 toml = ["tomli"]
 
-[[package]]
-name = "crayons"
-version = "0.4.0"
-description = "TextUI colors for Python."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-colorama = "*"
-
 [[package]]
 name = "dataclasses"
 version = "0.8"
@@ -925,17 +902,6 @@ urllib3 = ">=1.25.10"
 [package.extras]
 tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"]
 
-[[package]]
-name = "selenium"
-version = "3.141.0"
-description = "Python bindings for Selenium"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-urllib3 = "*"
-
 [[package]]
 name = "send2trash"
 version = "1.8.0"
@@ -1069,19 +1035,6 @@ category = "dev"
 optional = false
 python-versions = "*"
 
-[[package]]
-name = "webdriver-manager"
-version = "3.5.2"
-description = "Library provides the way to automatically manage drivers for different browsers"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-configparser = "*"
-crayons = "*"
-requests = "*"
-
 [[package]]
 name = "webencodings"
 version = "0.5.1"
@@ -1116,7 +1069,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "36f7b5727d35090cd3a61b26aab2a5da3a10c00e7bc2e1c7d08e04756da89a47"
+content-hash = "e356606c4d37d4db0a27b1f616246c4852f185d882175210591b392a4c15dfda"
 
 [metadata.files]
 appdirs = [
@@ -1231,10 +1184,6 @@ colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
-configparser = [
-    {file = "configparser-5.2.0-py3-none-any.whl", hash = "sha256:e8b39238fb6f0153a069aa253d349467c3c4737934f253ef6abac5fe0eca1e5d"},
-    {file = "configparser-5.2.0.tar.gz", hash = "sha256:1b35798fdf1713f1c3139016cfcbc461f09edbf099d1fb658d4b7479fcaa3daa"},
-]
 coverage = [
     {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"},
     {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"},
@@ -1284,10 +1233,6 @@ coverage = [
     {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"},
     {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"},
 ]
-crayons = [
-    {file = "crayons-0.4.0-py2.py3-none-any.whl", hash = "sha256:e73ad105c78935d71fe454dd4b85c5c437ba199294e7ffd3341842bc683654b1"},
-    {file = "crayons-0.4.0.tar.gz", hash = "sha256:bd33b7547800f2cfbd26b38431f9e64b487a7de74a947b0fafc89b45a601813f"},
-]
 dataclasses = [
     {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
     {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
@@ -1602,32 +1547,24 @@ pyzmq = [
     {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"},
     {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"},
     {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"},
-    {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"},
-    {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"},
     {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"},
     {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"},
     {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"},
     {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"},
     {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"},
     {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"},
-    {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"},
-    {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"},
     {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"},
     {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"},
     {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"},
     {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"},
     {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"},
     {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"},
-    {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"},
-    {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"},
     {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"},
     {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"},
     {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"},
     {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"},
     {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"},
     {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"},
-    {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"},
-    {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"},
     {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"},
     {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"},
     {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"},
@@ -1635,8 +1572,6 @@ pyzmq = [
     {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"},
     {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"},
     {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"},
-    {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"},
-    {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"},
     {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"},
     {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"},
     {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"},
@@ -1737,10 +1672,6 @@ responses = [
     {file = "responses-0.16.0-py2.py3-none-any.whl", hash = "sha256:f358ef75e8bf431b0aa203cc62625c3a1c80a600dbe9de91b944bf4e9c600b92"},
     {file = "responses-0.16.0.tar.gz", hash = "sha256:a2e3aca2a8277e61257cd3b1c154b1dd0d782b1ae3d38b7fa37cbe3feb531791"},
 ]
-selenium = [
-    {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"},
-    {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"},
-]
 send2trash = [
     {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"},
     {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"},
@@ -1848,10 +1779,6 @@ wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
     {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
-webdriver-manager = [
-    {file = "webdriver_manager-3.5.2-py2.py3-none-any.whl", hash = "sha256:1e3aa366b95b80b260bd71aea3a63ea3f19a6d14308e0cd93e1144ca46b11ef4"},
-    {file = "webdriver_manager-3.5.2.tar.gz", hash = "sha256:0a0df5e5d5d234702fd5948fec6c3884940aff904425725f15d8bb9eeaee18e8"},
-]
 webencodings = [
     {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
     {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
diff --git a/pyproject.toml b/pyproject.toml
index 2851677b3..3d8c6de93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,6 @@ python = "^3.6"
 requests = "^2.26.0"
 pydantic = "1.7.4"
 hrflow = "^1.9.0"
-selenium = "3.141.0"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2.5"
@@ -18,7 +17,6 @@ pytest-cov = "^3.0.0"
 black = "20.8b0"
 unittest = "^0.0"
 responses = "^0.16.0"
-webdriver-manager = "^3.5.2"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/src/hrflow_connectors/__init__.py b/src/hrflow_connectors/__init__.py
index ce40b9d2d..c5101440b 100644
--- a/src/hrflow_connectors/__init__.py
+++ b/src/hrflow_connectors/__init__.py
@@ -14,3 +14,6 @@
     __version__ = "unknown"
 finally:
     del version, PackageNotFoundError
+
+from .connectors import *
+from .core.auth import *
diff --git a/src/hrflow_connectors/connectors/__init__.py b/src/hrflow_connectors/connectors/__init__.py
index e69de29bb..0f140b60b 100644
--- a/src/hrflow_connectors/connectors/__init__.py
+++ b/src/hrflow_connectors/connectors/__init__.py
@@ -0,0 +1,12 @@
+from .crosstalent import *
+from .smartrecruiters import *
+from .greenhouse import *
+from .xml import *
+from .flatchr import *
+from .workable import *
+from .recruitee import *
+from .ceridian import *
+from .sapsuccessfactors import *
+from .taleez import *
+from .monster import *
+from .breezyhr import *
diff --git a/src/hrflow_connectors/connectors/boards/careerbuilder/README.md b/src/hrflow_connectors/connectors/boards/careerbuilder/README.md
deleted file mode 100644
index c3055a585..000000000
--- a/src/hrflow_connectors/connectors/boards/careerbuilder/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# CareerBuilder Connector
-**CareerBuilder is an employment website which provides labor market information, talent management software, and other recruitment related services.**
-
-`CareerBuilder`  :arrow_right:  `Hrflow.ai`
-
-## CareerBuilderFeed
-`CareerBuilderFeed` scans the ***CareerBuilder*** website for job offers. It adds all these **jobs** to a ***Hrflow.ai Board***
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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 : `False`        |
-| `domain` :red_circle: | `str` | domain just after `careerbuilder.` for example domain =`fr` in `https:/www.careerbuilder.fr`        |
-| `job_search` :red_circle: | `str` | Name of the job position we want to search offers for in `https:/www.careerbuilder.fr`        |
-| `job_location` :red_circle: | `str` | Location of the job offers        |
-| `executable_path`  | `Optional[str]` | A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator        |
-| `binary_location`  | `Optional[str]` | Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`        |
-| `maximum_page_num`  | `Optional[int]` | Maximum `number of pages` you want to scroll, `career builder`pagination is designed as an infinite scroller loading. Default value : `None` (This means taking the maximum number of pages)        |
-| `sort_by_date`  | `bool`  | sort search results by date. Default value: `False` (By default, results are sorted by relevancy)
-
-:red_circle: : *required* 
-
-### Example
-
-```python
-from hrflow import Hrflow
-
-from hrflow_connectors.connectors.boards.careerbuilder import CareerBuilderFeed
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
-
-client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-
-action = CareerBuilderFeed(
-    executable_path="/opt/webdriver",
-    maximum_page_num=2,
-    domain="com",
-    hrflow_client=client,
-    job_search="Data Scientist",
-    job_location="California",
-    board_key="MY_BOARD_KEY",
-    hydrate_with_parsing=True,
-)
-action.execute()
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/careerbuilder/__init__.py b/src/hrflow_connectors/connectors/boards/careerbuilder/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/careerbuilder/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/careerbuilder/actions.py b/src/hrflow_connectors/connectors/boards/careerbuilder/actions.py
deleted file mode 100644
index ff7175200..000000000
--- a/src/hrflow_connectors/connectors/boards/careerbuilder/actions.py
+++ /dev/null
@@ -1,247 +0,0 @@
-from typing import Dict, Any, Iterator, Optional
-from ....core.action import BoardAction
-from ....utils.logger import get_logger
-from pydantic import Field
-from time import sleep
-from selenium import webdriver
-from selenium.common.exceptions import (
-    NoSuchElementException,
-    WebDriverException,
-    ElementNotInteractableException,
-    StaleElementReferenceException,
-)
-
-
-logger = get_logger()
-
-
-class CareerBuilderFeed(BoardAction):
-
-    archive_deleted_jobs_from_stream: bool = False
-    domain: str = Field(
-        ...,
-        description="domain just after `https://www.careerbuilder.` for example domain = `fr` in `https://www.careerbuilder.fr`",
-    )
-    job_search: str = Field(
-        ...,
-        description="Name of the job position we want to search offers for in 'careerbuilder'",
-    )
-    job_location: str = Field(..., description="Location of the job offers")
-    executable_path: Optional[str] = Field(
-        None,
-        description="A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator",
-    )
-    binary_location: Optional[str] = Field(
-        None,
-        description="Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`",
-    )
-    maximum_page_num: Optional[int] = Field(
-        None,
-        description="Maximum `number of pages` you want to scroll, `careerbuilder` website pagination is designed as an infinite scroller loading",
-    )
-    sort_by_date: bool = Field(
-        False,
-        description="by default, results are sorted by relevancy which gives the best results but may cause getting old results that are no longer available which provokes `selenium Timeout erros` in `format: driver.get('job_link')`. To sort by date, switch this variable to True",
-    )
-
-    @property
-    def base_url(self) -> str:
-        if self.domain == "de":  # German Careerbuilder website
-            return "https://www.jobs.de/"
-        else:
-            return "https://www.careerbuilder.{}".format(self.domain)
-
-    @property
-    def Crawler(self):
-        """
-        Selenium Crawler function
-        """
-        logger.info("Configuring Chrome Webdriver...")
-        chrome_options = webdriver.ChromeOptions()
-        chrome_options.add_argument("--headless")
-        chrome_options.add_argument("--no-sandbox")
-        chrome_options.add_argument("--disable-dev-shm-usage")
-        chrome_options.add_argument("--disable-gpu")
-        chrome_options.add_argument("--window-size=1280x1696")
-        chrome_options.add_argument("--hide-scrollbars")
-        chrome_options.add_argument("--enable-logging")
-        chrome_options.add_argument("--log-level=0")
-        chrome_options.add_argument("--v=99")
-        chrome_options.add_argument("--single-process")
-        chrome_options.add_argument("--ignore-certificate-errors")
-
-        if self.binary_location is not None:
-            logger.info(f"Chrome binary location : {self.binary_location}")
-            chrome_options.binary_location = self.binary_location
-
-        if self.executable_path is None:
-            driver = webdriver.Chrome(chrome_options)
-        else:
-            logger.info(f"Webdriver executable path : {self.executable_path}")
-            driver = webdriver.Chrome(
-                executable_path=self.executable_path, chrome_options=chrome_options
-            )
-        return driver
-
-    def pull(self) -> Iterator[Dict[str, Any]]:
-        """
-        Pull, the role of this function is to interact with careerbuilder webpage, click buttons and search offers based on job title and location.
-        it scraps all the job cards shown on the page, and for each job card it retrieves its individual link and reference
-
-        Returns:
-          Iterator[Dict[str, Any]]:  list of dictionaries iterator[{job_ref:job_link}]
-        """
-        driver = self.Crawler
-        try:
-            logger.info(f"Crawler get page : url=`{self.base_url}`")
-            driver.get(self.base_url)
-        except WebDriverException as e:
-            logger.error(f"Failed to get page : url=`{self.base_url}`")
-            logger.error(e)
-            error_message = f"This website in not available, check if `carrerbuilder.{self.domain}` is a valid domain"
-            raise ConnectionError(error_message)
-
-        # search job search and job location cases
-        search_key = driver.find_element_by_xpath('//*[@id="Keywords"]')
-        logger.info(f"Crawler send a query `{self.job_search}`")
-        search_key.send_keys(self.job_search)
-
-        location_key = driver.find_element_by_xpath('//*[@id="Location"]')
-        logger.info(f"Crawler send a query `{self.job_location}`")
-        location_key.send_keys(self.job_location)
-
-        # click on the search button after sending our keys
-        driver.find_element_by_xpath('//*[@id="sbmt"]').click()
-
-        if self.sort_by_date:
-            try:
-                logger.info(f"sorting resuts by date: {self.sort_by_date}")
-                driver.find_element_by_name("date").click()
-
-            except NoSuchElementException:  # case when there are no results, or this ain't an option on a specific website pass to make it more explicit down
-                pass
-        logger.info(f"Waiting 3 seconds for page to fully load.")
-        sleep(3)
-        try:  # In case there are more results than those shown so we need to load more jobs on the page
-            logger.info(
-                "Crawler loading more jobs if there are more results than shown initially"
-            )
-            load_more_jobs = driver.find_element_by_css_selector(
-                "#load_more_jobs > button"
-            )
-            page_num = 1  # first page of results
-            while load_more_jobs:
-                if (
-                    page_num == self.maximum_page_num
-                ):  # if page_num reaches limit set by user
-                    logger.info(
-                        f"reached maximum page limit set by customer {self.maximum_page_num}"
-                    )
-                    break
-                try:
-                    logger.info(f"Waiting 4 seconds to load more jobs.")
-                    sleep(4)  # give the driver time to find element and click
-                    load_more_jobs.click()
-                    page_num += 1
-                    logger.debug(f"loading page number: {page_num}")
-
-                except (
-                    ElementNotInteractableException,
-                    StaleElementReferenceException,
-                ):  # i.e (element is present in the DOM but interactions with that element will hit another element do to paint order, Thrown when a reference to an element is now “stale”) by order of exceptions
-                    logger.warning(
-                        f"error loading page number: {page_num}, there are no more jobs to load or the element to click on is stale."
-                    )
-                    load_more_jobs = False
-        except NoSuchElementException:  # Except if the driver don't need to scroll down to get all jobs we pass
-            logger.info("There is one or no page of results")
-            pass
-
-        # get all job cards web elements available on the page
-        logger.info(f"Give the driver 5 seconds to get all jobs.")
-        sleep(5)  # give the driver time to get all jobs, especially when there are thousands on one page
-        logger.info("Getting all job cards")
-        jobs = driver.find_elements_by_xpath(
-            "//*[@class='data-results-content block job-listing-item']"
-        )
-        if len(jobs) == 0:
-            error_message = f"Could not find any matching jobs on the page, check that your search keys: `{self.job_search}`, `{self.job_location}` are valid!"
-            raise NoSuchElementException(error_message)
-
-        elements_count = len(jobs)
-        logger.info(f"Number of jobs found for this search : {elements_count}")
-
-        # get the list of the links and references of job cards: [{job_ref:job_link} for job in jobs]
-        job_ref_link_list = [
-            {job.get_attribute("data-job-did"): job.get_attribute("href")}
-            for job in jobs
-        ]
-
-        job_refs_count = len(job_ref_link_list)
-        job_link_count = len(job_ref_link_list)
-        logger.info(f"Number of total job references found : {job_refs_count}")
-        logger.info(f"Number of total job links found : {job_link_count}")
-
-        return job_ref_link_list
-
-    def format(self, data: Dict[str, str]) -> Dict[str, Any]:
-        """
-        Format, generates a dictionary of a job attributes, for each job link(data value) the function scraps with selenium and parse useful attributes.
-
-        Args:
-            data Dict[str, str]: a dictionary with job reference as key and job link as value generated after the function pull is executed.
-
-        Returns:
-            Dict[str, Any]: a job in the HrFlow job object format
-        """
-        # getting job_ref and job_link from previously pulled data
-        ((job_ref, job_link),) = data.items()
-
-        job = dict()
-
-        logger.debug("Extracting job from its link web page")
-        driver = self.Crawler
-        logger.debug(f"Crawling `{job_link}`")
-        driver.get(job_link)
-        logger.debug(f"Scraping the web page...")
-
-        job["url"] = job_link
-        # name
-        job["name"] = driver.find_element_by_class_name("jdp_title_header").text
-        # job_details : CompanyName|Location|EmploymentType, a list of three elements can be [name,"",Type] and so on...
-        job_detail = driver.find_elements_by_xpath(
-            '//*[@id="jdp-data"]//span'
-        )  # can be a list of two or three elements
-        # location
-        location = job_detail[1].text
-        job["location"] = dict(text=location, lat=None, lng=None)
-        # JobType
-        if len(job_detail) >= 3:
-            employment_type = job_detail[2].text
-        else:
-            employment_type = None
-        # CompanyName
-        company_name = job_detail[0].text
-        # salary
-        salary = driver.find_element_by_xpath('//*[@id="cb-salcom-info"]/div').text
-        job["tags"] = [
-            dict(name="careerbuilder_companyName", value=company_name),
-            dict(name="careerbuilder_compensation", value=salary),
-            dict(name="careerbuilder_employment_type", value=employment_type),
-        ]
-        # description
-        description = driver.find_element_by_class_name("jdp-description-details").text
-        job["sections"] = [
-            {
-                "name": "career_builder_description",
-                "title": "description",
-                "description": description,
-            }
-        ]
-        job["summary"] = None
-        job["reference"] = job_ref
-        job["metadata"] = []
-        driver.quit()
-        logger.debug(f"The web page has been scraped")
-
-        return job
diff --git a/src/hrflow_connectors/connectors/boards/careerbuilder/spec.py b/src/hrflow_connectors/connectors/boards/careerbuilder/spec.py
deleted file mode 100644
index e32054273..000000000
--- a/src/hrflow_connectors/connectors/boards/careerbuilder/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import CareerBuilderFeed
-
-
-class Spec(BaseModel):
-    actions: Tuple[CareerBuilderFeed]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/ceridian/__init__.py b/src/hrflow_connectors/connectors/boards/ceridian/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/ceridian/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/ceridian/spec.py b/src/hrflow_connectors/connectors/boards/ceridian/spec.py
deleted file mode 100644
index 32a41a025..000000000
--- a/src/hrflow_connectors/connectors/boards/ceridian/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PullJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[PullJobs]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/craigslist/README.md b/src/hrflow_connectors/connectors/boards/craigslist/README.md
deleted file mode 100644
index fd270c382..000000000
--- a/src/hrflow_connectors/connectors/boards/craigslist/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Craigslist Connector
-**Craigslist is a website listing ads.**
-
-`Craigslist` :arrow_right: `Hrflow.ai`
-
-## CraigslistFeed
-`CraigslistFeed` scans the ***Craigslist*** site for job offers. It adds all these **jobs** to a ***Hrflow.ai Board***.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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 : `False`        |
-| `subdomain` :red_circle: | `str` | Subdomain just before `craigslist.org/d/emploi/search/jjj` for example subdomain = `paris` in `https://paris.craigslist.org/d/emploi/search/jjj`, it is also the localisation of the job offers        |
-| `executable_path`  | `Optional[str]` | A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator        |
-| `binary_location`  | `Optional[str]` | Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`        |
-
-:red_circle: : *required* 
-
-### Example
-
-```python
-from hrflow import Hrflow
-
-from hrflow_connectors.connectors.boards.craigslist import CraigslistFeed
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
-
-client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-
-action = CraigslistFeed(
-    executable_path="/opt/webdriver",
-    subdomain="Paris",
-    hrflow_client=client,
-    board_key="MY_BOARD_KEY",
-    hydrate_with_parsing=True,
-)
-action.execute()
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/craigslist/__init__.py b/src/hrflow_connectors/connectors/boards/craigslist/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/craigslist/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/craigslist/actions.py b/src/hrflow_connectors/connectors/boards/craigslist/actions.py
deleted file mode 100644
index 16e7bed1a..000000000
--- a/src/hrflow_connectors/connectors/boards/craigslist/actions.py
+++ /dev/null
@@ -1,182 +0,0 @@
-from typing import Dict, Any, Iterator, Optional
-from pydantic import Field
-from selenium import webdriver
-from selenium.common.exceptions import WebDriverException
-from re import search
-
-from ....core.action import BoardAction
-from ....utils.logger import get_logger
-
-logger = get_logger()
-
-
-class CraigslistFeed(BoardAction):
-    archive_deleted_jobs_from_stream: bool = False
-    subdomain: str = Field(
-        ...,
-        description="Subdomain just before `craigslist.org/d/emploi/search/jjj` for example subdomain = `paris` in `https://paris.craigslist.org/d/emploi/search/jjj`, it is also the localisation of the job offers",
-    )
-    executable_path: Optional[str] = Field(
-        None,
-        description="A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator",
-    )
-
-    binary_location: Optional[str] = Field(
-        None,
-        description="Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`",
-    )
-
-    jobs_per_page: int = Field(120, const=True)
-
-    @property
-    def base_url(self):
-
-        return "https://{}.craigslist.org/d/emploi/search/jjj".format(self.subdomain)
-
-    @property
-    def Crawler(self):
-        """
-        Selenium Crawler function
-        """
-        logger.info("Configuring Chrome Webdriver...")
-        chrome_options = webdriver.ChromeOptions()
-        chrome_options.add_argument("--headless")
-        chrome_options.add_argument("--no-sandbox")
-        chrome_options.add_argument("--disable-dev-shm-usage")
-        chrome_options.add_argument("--disable-gpu")
-        chrome_options.add_argument("--window-size=1280x1696")
-        chrome_options.add_argument("--hide-scrollbars")
-        chrome_options.add_argument("--enable-logging")
-        chrome_options.add_argument("--log-level=0")
-        chrome_options.add_argument("--v=99")
-        chrome_options.add_argument("--single-process")
-        chrome_options.add_argument("--ignore-certificate-errors")
-
-        if self.binary_location is not None:
-            logger.info(f"Chrome binary location : {self.binary_location}")
-            chrome_options.binary_location = self.binary_location
-
-        if self.executable_path is None:
-            driver = webdriver.Chrome(chrome_options)
-        else:
-            driver = webdriver.Chrome(
-                executable_path=self.executable_path, chrome_options=chrome_options
-            )
-
-        return driver
-
-    def pull(self) -> Iterator[str]:
-        """
-        Pull job links
-
-        The role of this function is to scrap and retrieve each job offer link for each job posted
-
-        Returns:
-            Iterator[str]: list of scrapped job links
-        """
-        job_link_list = []
-
-        driver = self.Crawler
-
-        try:
-            logger.info(f"Crawler get page : url=`{self.base_url}`")
-            driver.get(self.base_url)
-        except WebDriverException as e:
-            logger.error(f"Fail to get page : url=`{self.base_url}`")
-            logger.error(e)
-            error_message = f"This site in not available, check if {self.subdomain}.craiglist.org is a valid subdomain"
-            raise ConnectionError(error_message)
-
-        driver.maximize_window()
-        total_jobs = int(driver.find_element_by_xpath("//*[@class='totalcount']").text)
-        logger.info(f"Total jobs to find : {total_jobs}")
-        total_pages = total_jobs // self.jobs_per_page + 1
-        logger.info(f"Total pages to crawl : {total_pages}")
-
-        for page in range(0, total_pages):
-            logger.info(f"Crawling page=`{page + 1}` ...")
-            driver.get(self.base_url + "s=%s" % ((page + 1) * self.jobs_per_page))
-            # jobs rows
-            currant_page_job_list = driver.find_elements_by_xpath(
-                "//*[@id='search-results']/li"
-            )
-            current_page_job_count = len(currant_page_job_list)
-            logger.info(f"Number of jobs on this page : {current_page_job_count}")
-            # retrieve job link from each job row
-            job_link_list += [
-                job.find_element_by_tag_name("a").get_attribute("href")
-                for job in currant_page_job_list
-            ]
-
-        job_link_count = len(job_link_list)
-        logger.info(f"Total job links found on all pages : {job_link_count}")
-        return job_link_list
-
-    def format(self, job_link: str) -> Dict[str, Any]:
-        """
-        Format job
-
-        Generates a dictionary of a job attributes, for each job link the function scraps with selenium and parse useful attributes
-
-        Args:
-            job_link (str): job link parsed after the function pull is executed
-
-        Returns:
-            Dict[str, Any]: Job in the HrFlow job object format
-        """
-        job = dict()
-
-        logger.debug("Extracting job from a web page")
-        driver = self.Crawler
-
-        logger.debug(f"Crawler go to `{job_link}`")
-        driver.get(job_link)
-        logger.debug(f"Scraping the web page...")
-
-        # job name
-        job["name"] = driver.find_element_by_xpath("//*[@id='titletextonly']").text
-
-        # job reference
-        # Searching for a match to get the reference to the job
-        m = search("7(.+?).html", job_link)
-        if m is not None:
-            job["reference"] = m.group(1)
-
-        # job url
-        job["url"] = job_link
-
-        # job create_at
-        job["created_at"] = (
-            driver.find_elements_by_xpath("//*[@class='postinginfo reveal']")[0]
-            .find_element_by_tag_name("time")
-            .get_attribute("datetime")
-        )
-
-        # job summary
-        job["summary"] = None
-
-        # job location
-        job["location"] = dict(text=self.subdomain, lat=None, lng=None)
-
-        # job sections
-        job["sections"] = [
-            dict(
-                name="craigslist_description",
-                title="Description",
-                description=driver.find_element_by_xpath("//*[@id='postingbody']").text,
-            )
-        ]
-
-        # tags
-        tags = driver.find_elements_by_tag_name("b")
-        job["tags"] = [
-            dict(name="craigslist_compensation", value=tags[0].text),
-            dict(name="craigslist_employment_type", value=tags[1].text),
-        ]
-
-        job["ranges_date"] = []
-        job["ranges_float"] = []
-        job["metadatas"] = []
-
-        logger.debug(f"The web page has been scraped")
-        return job
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/craigslist/spec.py b/src/hrflow_connectors/connectors/boards/craigslist/spec.py
deleted file mode 100644
index 193bb6870..000000000
--- a/src/hrflow_connectors/connectors/boards/craigslist/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import CraigslistFeed
-
-
-class Spec(BaseModel):
-    actions: Tuple[CraigslistFeed]
diff --git a/src/hrflow_connectors/connectors/boards/crosstalent/__init__.py b/src/hrflow_connectors/connectors/boards/crosstalent/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/crosstalent/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/crosstalent/spec.py b/src/hrflow_connectors/connectors/boards/crosstalent/spec.py
deleted file mode 100644
index 85ed0050f..000000000
--- a/src/hrflow_connectors/connectors/boards/crosstalent/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import GetAllJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[GetAllJobs]
diff --git a/src/hrflow_connectors/connectors/boards/greenhouse/__init__.py b/src/hrflow_connectors/connectors/boards/greenhouse/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/greenhouse/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/greenhouse/actions.py b/src/hrflow_connectors/connectors/boards/greenhouse/actions.py
deleted file mode 100644
index 69cf09b8a..000000000
--- a/src/hrflow_connectors/connectors/boards/greenhouse/actions.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from typing import Iterator, Dict, Any
-from pydantic import Field
-import html
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
-from ....utils.clean_text import remove_html_tags
-
-logger = get_logger()
-
-
-class GetAllJobs(HTTPStream, BoardAction):
-    board_token: str = Field(
-        ...,
-        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",
-    )
-
-    @property
-    def base_url(self):
-        return (
-            "https://boards-api.greenhouse.io/v1/boards/{}/jobs/?content=true".format(
-                self.board_token
-            )
-        )
-
-    @property
-    def http_method(self):
-        return "GET"
-
-    def pull(self) -> Iterator[Dict[str, Any]]:
-        """
-        pull all jobs from a greenhouse job board
-
-        Returns:
-            Iterator[Dict[str, Any]]: list of all jobs with their content if available
-        """
-
-        response = self.send_request()
-        if response.status_code == 200:
-            job_dict = response.json()
-            total_info = job_dict["meta"]["total"]
-            logger.info(f"Total jobs found : {total_info}")
-            job_list = job_dict["jobs"]
-            return job_list
-        else:
-            logger.error(
-                f"Failed to get jobs from board: {self.board_token}, Check that your board token is valid"
-            )
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            raise ConnectionError(error_message.format(response.content))
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        format each job pulled from greenhouse job board into a HrFlow job object
-
-        Returns:
-            Dict[str, Any]: job in the HrFlow job object format
-        """
-
-        job = dict()
-        # name
-        job["name"] = data.get("title")
-        # summary
-        job["summary"] = None
-        # reference
-        job["reference"] = str(data.get("id"))
-        # url
-        job["url"] = data.get("absolute_url")
-        # location
-        location = data.get("location").get("name")
-        job["location"] = dict(text=location, lat=None, lng=None)
-        # sections
-        description_content = data.get("content")
-        # convert the escaped description content into html format
-        description_html = html.unescape(description_content)
-        # remove html tags to get clean text
-        text = remove_html_tags(description_html)
-
-        job["sections"] = [
-            dict(
-                name="greenhouse_description",
-                title="greenhouse_description",
-                description=text,
-            )
-        ]
-        # metadata
-        job["metadatas"] = data.get("metadata")
-        # tags
-        department = data.get("departments")
-        if department not in [None, []]:
-            department_name = department[0].get("name")
-            department_id = str(department[0].get("id"))
-        else:
-            department_name = "Undefined"
-            department_id = "Undefined"
-
-        office = data.get("offices")
-        if office not in [None, []]:
-            office_name = office[0].get("name")
-            office_id = str(office[0].get("id"))
-        else:
-            office_name = "Undefined"
-            office_id = "Undefined"
-
-        education = data.get("education")
-        employment = data.get("employment")
-
-        job["tags"] = [
-            dict(name="greenhouse_department-name", value=department_name),
-            dict(name="greenhouse_department-id", value=department_id),
-            dict(name="greenhouse_office-location", value=office_name),
-            dict(name="greenhouse_office-id", value=office_id),
-            dict(name="greenhouse_education", value=education),
-            dict(name="greenhouse_employment", value=employment),
-        ]
-        # updated_at
-        job["updated_at"] = data.get("updated_at")
-
-        return job
diff --git a/src/hrflow_connectors/connectors/boards/greenhouse/spec.py b/src/hrflow_connectors/connectors/boards/greenhouse/spec.py
deleted file mode 100644
index 94d26defa..000000000
--- a/src/hrflow_connectors/connectors/boards/greenhouse/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import GetAllJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[GetAllJobs]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/indeed/README.md b/src/hrflow_connectors/connectors/boards/indeed/README.md
deleted file mode 100644
index 045458cfa..000000000
--- a/src/hrflow_connectors/connectors/boards/indeed/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Indeed Connector
-**Indeed is a meta job search engine. It provides access to millions of job offers.**
-
-`Indeed` :arrow_right: `Hrflow.ai`
-
-## IndeedFeed
-`IndeedFeed` scans the ***Indeed*** site for job offers. It adds all these **jobs** to a ***Hrflow.ai Board***.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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 : `False`        |
-| `subdomain` :red_circle: | `str` | Subdomain just before `indeed.com` for example subdomain =`fr` in `https:/fr.indeed.com`        |
-| `job_search` :red_circle: | `str` | Name of the job position we want to search offers in `fr.indeed.com`        |
-| `job_location` :red_circle: | `str` | Location of the job offers        |
-| `executable_path`  | `Optional[str]` | A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator        |
-| `binary_location`  | `Optional[str]` | Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`        |
-| `max_page`  | `Optional[int]` | Maximum number of pages to search. Default value : `None` (This means taking the maximum number of pages)        |
-
-:red_circle: : *required* 
-
-### Example
-
-```python
-from hrflow import Hrflow
-
-from hrflow_connectors.connectors.boards.indeed import IndeedFeed
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
-
-client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-
-action = IndeedFeed(
-    executable_path="/opt/webdriver",
-    max_page=2,
-    subdomain="fr",
-    hrflow_client=client,
-    job_search="Software Engineer",
-    job_location="Paris",
-    board_key="MY_BOARD_KEY",
-    hydrate_with_parsing=True,
-)
-action.execute()
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/indeed/__init__.py b/src/hrflow_connectors/connectors/boards/indeed/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/indeed/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/indeed/actions.py b/src/hrflow_connectors/connectors/boards/indeed/actions.py
deleted file mode 100644
index 6ebac83ae..000000000
--- a/src/hrflow_connectors/connectors/boards/indeed/actions.py
+++ /dev/null
@@ -1,303 +0,0 @@
-from typing import Dict, Any, Iterator, Optional
-import re
-from pydantic import Field
-from selenium import webdriver
-from selenium.webdriver.common.keys import Keys
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
-from time import sleep
-from selenium.common.exceptions import NoSuchElementException
-
-
-from ....core.action import BoardAction
-from ....utils.logger import get_logger
-
-
-logger = get_logger()
-
-
-class IndeedFeed(BoardAction):
-    archive_deleted_jobs_from_stream: bool = False
-    subdomain: str = Field(
-        ...,
-        description="Subdomain just before `indeed.com` for example subdomain =`fr` in `https:/fr.indeed.com`",
-    )
-    job_search: str = Field(
-        ...,
-        description="Name of the job position we want to search offers in `fr.indeed.com`",
-    )
-    job_location: str = Field(..., description="Location of the job offers")
-
-    executable_path: Optional[str] = Field(
-        None,
-        description="A separate executable that Selenium WebDriver used to control Chrome. Make sure you install the chromedriver with the same version as your local Chrome navigator",
-    )
-
-    binary_location: Optional[str] = Field(
-        None,
-        description="Location of the binary chromium, usually in HrFlow workflows it equals `/opt/bin/headless-chromium`",
-    )
-
-    max_page: Optional[int] = Field(
-        None, description="Maximum number of pages to search"
-    )
-
-    job_per_page: int = Field(15, const=True)
-
-    @property
-    def url_base(self) -> str:
-        return "https:/{}.indeed.com".format(self.subdomain)
-
-    @property
-    def Crawler(self):
-        """
-        Selenium Crawler function
-        """
-        logger.info("Configuring Chrome Webdriver...")
-        chrome_options = webdriver.ChromeOptions()
-        chrome_options.add_argument("--headless")
-        chrome_options.add_argument("--no-sandbox")
-        chrome_options.add_argument("--disable-dev-shm-usage")
-        chrome_options.add_argument("--disable-gpu")
-        chrome_options.add_argument("--window-size=1280x1696")
-        chrome_options.add_argument("--hide-scrollbars")
-        chrome_options.add_argument("--enable-logging")
-        chrome_options.add_argument("--log-level=0")
-        chrome_options.add_argument("--v=99")
-        chrome_options.add_argument("--single-process")
-        chrome_options.add_argument("--ignore-certificate-errors")
-
-        if self.binary_location is not None:
-            logger.info(f"Chrome binary location : {self.binary_location}")
-            chrome_options.binary_location = self.binary_location
-
-        if self.executable_path is None:
-            driver = webdriver.Chrome(chrome_options)
-        else:
-            logger.info(f"Webdriver executable path : {self.executable_path}")
-            driver = webdriver.Chrome(
-                executable_path=self.executable_path, chrome_options=chrome_options
-            )
-
-        return driver
-
-    def path(self, job_offset: int) -> str:
-        """
-        Get the generated path
-
-        This function generates the path and pagination of the job offers search
-
-        Args:
-            job_offset (int): Job offset. For example job_offset = 10 if we want to start from page 2
-
-        Returns:
-            str: return the generated path. For example it returns `/emplois?q=data%20scientist&l=paris&start=10` if we are looking for Data Scientist offers in Paris in page 2
-        """
-        return "/emplois?q={query}&l={location}&start={start}".format(
-            query=self.job_search,
-            location=self.job_location,
-            start=job_offset,
-        )
-
-    def pull(self) -> Iterator[str]:
-        """pull [the role of this function is to interact with indeed, click buttons and search offers based on job title and location.
-        for each page we scrap all the job cards shown (usually 15 per page), and for each job card it retrieves its individual link]
-
-        Returns:
-            Iterator[str]: [list of scrapped jobLinks]
-        """
-        job_links_list = []
-        driver = self.Crawler
-
-        logger.info(f"Crawler go to `{self.url_base}`")
-        driver.get(self.url_base)
-        # Find the text box and enter the type of job the user wants to get offers data
-        search = driver.find_element_by_id("text-input-what")
-        logger.info(f"Crawler send a query `{self.job_search}`")
-        search.send_keys(self.job_search)
-        search.send_keys(Keys.RETURN)
-
-        # Get the search count str for example 'Page 1 de 993 emplois'
-        search_count_str = driver.find_element_by_id("searchCountPages").text
-        # retrieve the number of total related job offers from string 'for example from 'Page 1 de 993 emplois' we get job_search_count = 993'
-        search_count = (
-            search_count_str.split()
-        )  # split the string in a list of strings ['1', 'de', '993', 'emplois']
-        start = search_count.index("de")
-        end = search_count.index(
-            "emplois"
-        )  # find the words that surround the total number of jobs
-        job_search_count = int(
-            "".join([search_count[i] for i in range(start + 1, end)])
-        )  # group different parts of the number together for example if it is 'Page 1 de 1 993 emplois' we get 1993
-
-        logger.info(f"Number of jobs found for this query : {job_search_count}")
-        # If there is only one job offer result -corresponding to 'emploi'- or none a value error is raised, it is a rare case and the custom should make sure to type in a real job name.
-
-        total_page = int(
-            job_search_count / self.job_per_page
-        )  # for example for a total of 993 and a count of 15 we loop over 66 pages
-        logger.info(f"Number of pages found for this query : {total_page}")
-
-        if self.max_page is not None:
-            logger.info(f"Max pages requests by the user : {self.max_page}")
-            total_page = self.max_page
-
-        for page in range(0, total_page):
-            logger.info(f"Scraping the page `{page}` to get the list of jobs")
-            page_url = self.url_base + self.path(job_offset=self.job_per_page * page)
-            logger.info(f"Crawler go to `{page_url}`")
-            driver.get(page_url)
-            logger.info(f"Crawler sleep 3 secondes")
-            sleep(3)
-            try:  # get jobCards inside of a page
-                logger.info("Getting mosaic of jobs")
-                jobCards = WebDriverWait(driver, 10).until(
-                    EC.presence_of_element_located((By.ID, "mosaic-provider-jobcards"))
-                )
-                logger.info("Mosaic of jobs has been found")
-
-                # get joblink for each jobCard
-                logger.info("Finding each job link")
-                elements = jobCards.find_elements_by_class_name("tapItem")
-                elements_count = len(elements)
-                logger.info(f"Number of jobs found on this page : {elements_count}")
-                for x in elements:
-                    link = x.get_attribute("href")
-                    job_links_list.append(link)
-            finally:  # continue to next page
-                pass
-
-        job_link_count = len(job_links_list)
-        logger.info(f"Number of total job links found : {job_link_count}")
-        return job_links_list
-
-    def format(self, job_link: str) -> Dict[str, Any]:
-        """format [generates a dictionary of a job attributes, for each job link the function scraps with selenium and parse useful attributes.]
-
-        Args:
-            job_link (str): [job_link parsed after the function pull is executed. each job link is retrieved from the job_links_list scrapped in the pull function]
-
-        Returns:
-            Dict[str, Any]: [a job in the HrFlow job object format]
-        """
-        job = dict()
-
-        logger.debug("Extracting job from a web page")
-        driver = self.Crawler
-        logger.debug(f"Crawler go to `{job_link}`")
-        driver.get(job_link)
-        logger.debug(f"Scraping the web page...")
-
-        # name
-        job["name"] = driver.find_element_by_class_name(
-            "jobsearch-JobInfoHeader-title"
-        ).text
-
-        # reference
-        m = re.search("jk=(.+?)&", job_link)
-        if m:
-            job["reference"] = m.group(1)
-        else:
-            job["reference"] = None
-
-        # created_at, updated_at : isn't shown on ideed, TODO : convert "il y a n jours" into date time
-        job["created_at"] = None
-        job["updated_at"] = None
-
-        # location
-        # Sometimes there is a `COMPANY\nREVIEWS\nLOCATION'
-        # Sometimes there is a `COMPANY\nLOCATION'
-        subtitle_list = driver.find_element_by_class_name(
-            "jobsearch-DesktopStickyContainer-subtitle"
-        ).text.split("\n")
-
-        if len(subtitle_list) >= 2:
-            location = subtitle_list[-1]
-        else:
-            # In the case where there is only one item
-            # It is assumed that the single element in the list is not a location
-            location = "Undefined"
-
-        job["location"] = dict(text=location, lat=None, lng=None)
-
-        # url
-        job["url"] = job_link
-
-        # summary
-        try:
-            text = driver.find_element_by_id("jobDescriptionText").text
-        except NoSuchElementException:
-            text = None
-
-        job["summary"] = text
-
-        # description
-        job["sections"] = [
-            {"name": "description", "title": "description", "description": text}
-        ]
-        # compensation and jobType if they exist are in the same header so we get them both in one task of finding elements
-        try:
-            element = driver.find_element_by_class_name(
-                "jobsearch-JobMetadataHeader-item"
-            ).text
-        except NoSuchElementException:
-            element = "Null"
-
-        # compensation
-        # Need to be sure that we are parsing a salary and not a job Type because of indeed dynamic structure
-        items = element.split()
-        l = [
-            "Temps plein",
-            "Temps",
-            "plein",
-            "CDI",
-            "plein,",
-            "Stage",
-            "CDD",
-            "Apprentissage",
-            "Alternance",
-            "partiel",
-        ]
-        for item in l:
-            if item in items:
-                items.remove(item)
-        salary = " ".join([items[i] for i in range(len(items))])
-
-        # employment_type
-        # avoid that Selenium parse useless and erronous information due to Indeed dynamic website architecture for example it can give jobType = "38 000 € par an - CDI" or "2 369 € - 4 645 € par mois - Temps plein, CDD"
-        contract_type = ""
-        if element not in [
-            "CDI",
-            "CDD",
-            "Stage",
-            "Alternance",
-            "Temps plein",
-            "Temps partiel",
-            "Apprentissage",
-            "Contrat Pro",
-            "Stage, alternance",
-        ]:
-            if "CDI" in element.split():
-                contract_type = "CDI"
-            elif "plein" in element.split():
-                contract_type = "CDI"
-            elif "CDD" in element.split():
-                contract_type = "CDD"
-            else:
-                contract_type = "null"
-        jobType = contract_type
-
-        job["tags"] = [
-            dict(name="indeed_compensantion", value=salary),
-            dict(name="indeed_employment_type", value=jobType),
-        ]
-
-        job["ranges_date"] = list()
-        job["ranges_float"] = list()
-        job["metadatas"] = list()
-
-        driver.quit()
-        logger.debug(f"The web page has been scraped")
-        return job
diff --git a/src/hrflow_connectors/connectors/boards/indeed/spec.py b/src/hrflow_connectors/connectors/boards/indeed/spec.py
deleted file mode 100644
index 4ee25d87c..000000000
--- a/src/hrflow_connectors/connectors/boards/indeed/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import IndeedFeed
-
-
-class Spec(BaseModel):
-    actions: Tuple[IndeedFeed]
diff --git a/src/hrflow_connectors/connectors/boards/monster/README.md b/src/hrflow_connectors/connectors/boards/monster/README.md
deleted file mode 100644
index 78e143eea..000000000
--- a/src/hrflow_connectors/connectors/boards/monster/README.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Monster Connector
-**Monster is an ATS.**
-
-`Hrflow.ai` :arrow_right: `Monster`
-
-Lists of actions : 
-* [**`PushJob`**](#PushJob)
-
-## PushJob
-
-`PushJob` pushes a Hrflow.ai job to `Monster` via their API.
-
-This connector sends job in XML format.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `Username` :red_circle: | `String` | Username of the monster account        |
-| `Password` :red_circle: | String | Password of the monster account        |
-| `subdomain` :red_circle: | String | Subdomain monster just before `monster.com`. For example subdomain=`my_subdomain.my` in `https://my_subdomain.my.monster.com:8443/bgwBroker`       |
-| `hrflow_client` :red_circle: | hrflow.Hrflow | Hrflow client instance used to communicate with the Hrflow.ai API        |
-| `job` :red_circle: | `Job` | Job to push        |
-
-
-: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 import Hrflow
-
-from hrflow_connectors.connectors.boards.monster.actions import PushJob
-from hrflow_connectors.utils.hrflow import Job, Board
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    job = event.get_job()
-    if job is not None:
-        logger.info("job found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        action = PushJob(
-            username=credentials["monster"]["job"]["username"],
-            password=credentials["monster"]["job"]["password"],
-            subdomain="gateway",
-            hrflow_client=hrflow_client(),
-            job=job,
-        )
-        response = action.execute()
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/monster/spec.py b/src/hrflow_connectors/connectors/boards/monster/spec.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/hrflow_connectors/connectors/boards/recruitee/__init__.py b/src/hrflow_connectors/connectors/boards/recruitee/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/recruitee/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/recruitee/spec.py b/src/hrflow_connectors/connectors/boards/recruitee/spec.py
deleted file mode 100644
index 32a41a025..000000000
--- a/src/hrflow_connectors/connectors/boards/recruitee/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PullJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[PullJobs]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/__init__.py b/src/hrflow_connectors/connectors/boards/sapsuccessfactors/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/actions.py b/src/hrflow_connectors/connectors/boards/sapsuccessfactors/actions.py
deleted file mode 100644
index d21174ee7..000000000
--- a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/actions.py
+++ /dev/null
@@ -1,138 +0,0 @@
-from typing import Iterator, Dict, Any, Union
-from pydantic import Field
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
-from ....utils.clean_text import remove_html_tags
-from ....core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-
-logger = get_logger()
-
-
-class PullJobs(HTTPStream, BoardAction):
-    auth: Union[XAPIKeyAuth, OAuth2PasswordCredentialsBody]
-    top: int = Field(
-        20, description="show only the first n items, value by default = `20`"
-    )
-    api_server: str = Field(
-        ...,
-        description="the API server for your company from the list of API servers for SAP SuccessFactors data centers",
-    )
-
-    @property
-    def base_url(self):
-        return "https://{}/odata/v2/JobRequisitionLocale?$top={}&expand=jobRequisition".format(
-            self.api_server, self.top
-        )
-
-    @property
-    def http_method(self):
-        return "GET"
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["Accept"] = "application/json"
-
-    def pull(self) -> Iterator[Dict[str, Any]]:
-        """
-        pull all jobs from a local database that uses SAP successfactors API
-
-        Raises:
-            ConnectionError if the request fails with the error message
-
-        Returns:
-            Iterator[Dict[str, Any]]: list of all job requisitions with their content
-        """
-
-        response = self.send_request()
-        if response.ok:
-            logger.info(f"The request was successful !")
-            job_dict_list = response.json()["d"]["results"]
-            logger.info(f"Getting all the results totaling: {len(job_dict_list)}")
-            return job_dict_list
-        else:
-            logger.error(f"The request was unsuccessful !")
-            error_message = "Unable to pull the data ! Reason : `{}`"
-            logger.error(response.content)
-            raise ConnectionError(error_message.format(response.content))
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        format retrieved jobs into a HrFlow job object
-
-        Returns:
-            Dict[str, Any]: job in the HrFlow job object format
-        """
-        job = dict()
-        jobRequisition = data.get("jobRequisition")
-        # name
-        if data.get("jobTitle") is not None:
-            job["name"] = data.get("jobTitle")
-        else:
-            job["name"] = "Undefined"
-        # reference
-        job["reference"] = data.get("jobReqId")
-        # location
-        geojson = dict(
-            city=jobRequisition.get("city"),
-            country=jobRequisition.get("country"),
-            facility=jobRequisition.get("facility"),
-            province=jobRequisition.get("stateProvince"),
-        )
-        job["location"] = dict(
-            text=jobRequisition.get("location"),
-            city=jobRequisition.get("city"),
-            geojson=geojson,
-            lat=None,
-            lng=None,
-        )
-        # description
-        if data.get("jobDescription") is not None:
-            description = (
-                remove_html_tags(data.get("jobDescription"))
-                .replace("#13;", " ")
-                .replace("&", "")
-                .replace("&nbsp;", "")
-                .replace("quo;s", "")
-            )
-        else:
-            description = None
-        job["sections"] = [
-            dict(
-                name="sap_description", title="sap_description", description=description
-            )
-        ]
-        # tags
-        annual_salary = jobRequisition.get("annual_SA")
-        department = jobRequisition.get("department")
-        division = jobRequisition.get("division")
-        function = jobRequisition.get("function")
-        industry = jobRequisition.get("industry")
-        monthly_salary = jobRequisition.get("monthly_salary")
-        other_bonus = jobRequisition.get("otherBonus")
-        salary_base = jobRequisition.get("salaryBase")
-        salary_max = jobRequisition.get("salaryMax")
-        salary_min = jobRequisition.get("salaryMin")
-        job_start_date = jobRequisition.get("jobStartDate")
-        job["tags"] = [
-            dict(name="sap_annual_salary", value=annual_salary),
-            dict(name="sap_department", value=department),
-            dict(name="sap_function", value=function),
-            dict(name="sap_division", value=division),
-            dict(name="sap_industry", value=industry),
-            dict(name="sap_monthly_salary", value=monthly_salary),
-            dict(name="sap_other_bonus", value=other_bonus),
-            dict(name="sap_salary_base", value=salary_base),
-            dict(name="sap_salary_max", value=salary_max),
-            dict(name="sap_salary_min", value=salary_min),
-            dict(name="sap_job_start_date", value=job_start_date),
-        ]
-        job["metadatas"] = [
-            dict(name="sap_recruiter_team", value=jobRequisition.get("recruiterTeam")),
-            dict(name="sap_sourcer_team", value=jobRequisition.get("sourcerTeam")),
-            dict(
-                name="sap_hiring_manager_team",
-                value=jobRequisition.get("hiringManagerTeam"),
-            ),
-        ]
-        return job
diff --git a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/schemas.py b/src/hrflow_connectors/connectors/boards/sapsuccessfactors/schemas.py
deleted file mode 100644
index c09f2d591..000000000
--- a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/schemas.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from typing import Optional, Dict, Any 
-from pydantic import BaseModel
-
-class SAPSuccessFactorsJobRequistion(BaseModel):
-    annual_SA: str
-    location: Optional[str]
-    city : Optional[str] = None
-    country : Optional[str] = None
-    department : Optional[str] = None
-    division : Optional[str] = None
-    facility : Optional[str] = None
-    function : Optional[str] = None
-    industry : Optional[str] = None
-    monthly_salary: Optional[str] = None
-    salaryBase: Optional[str] = None
-    otherBonus: Optional[str] = None
-    salaryMax: Optional[str] = None
-    salaryMin: Optional[str] = None
-    stateProvince: Optional[str] = None
-    jobStartDate: Optional[str] = None
-    recruiterTeam: Optional[Dict[str, Any]] = None
-    hiringManagerTeam: Optional[Dict[str, Any]] = None
-    sourcerTeam: Optional[Dict[str, Any]] = None
-
-class SAPSuccessFactorsJob(BaseModel):
-    jobDescription : str
-    jobTitle: str    
-    jobReqId: str
-    jobRequisition: SAPSuccessFactorsJobRequistion
-
-
-
diff --git a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/spec.py b/src/hrflow_connectors/connectors/boards/sapsuccessfactors/spec.py
deleted file mode 100644
index 32a41a025..000000000
--- a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PullJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[PullJobs]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/smartrecruiters/__init__.py b/src/hrflow_connectors/connectors/boards/smartrecruiters/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/smartrecruiters/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/smartrecruiters/spec.py b/src/hrflow_connectors/connectors/boards/smartrecruiters/spec.py
deleted file mode 100644
index 85ed0050f..000000000
--- a/src/hrflow_connectors/connectors/boards/smartrecruiters/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import GetAllJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[GetAllJobs]
diff --git a/src/hrflow_connectors/connectors/boards/workable/__init__.py b/src/hrflow_connectors/connectors/boards/workable/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/workable/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/workable/spec.py b/src/hrflow_connectors/connectors/boards/workable/spec.py
deleted file mode 100644
index 32a41a025..000000000
--- a/src/hrflow_connectors/connectors/boards/workable/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PullJobs
-
-
-class Spec(BaseModel):
-    actions: Tuple[PullJobs]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/xml/__init__.py b/src/hrflow_connectors/connectors/boards/xml/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/boards/xml/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/xml/spec.py b/src/hrflow_connectors/connectors/boards/xml/spec.py
deleted file mode 100644
index 8f6b3328b..000000000
--- a/src/hrflow_connectors/connectors/boards/xml/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import XMLBoardAction
-
-
-class Spec(BaseModel):
-    actions: Tuple[XMLBoardAction]
diff --git a/src/hrflow_connectors/connectors/breezyhr/README.md b/src/hrflow_connectors/connectors/breezyhr/README.md
new file mode 100644
index 000000000..caf0cd44a
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/README.md
@@ -0,0 +1,11 @@
+# Breezyhr Connector
+> Breezyhr is an end-to-end recruiting software to help you attract & hire great employees with less effort
+
+
+
+🔗 https://breezy.hr/
+
+| 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/breezyhr/__init__.py b/src/hrflow_connectors/connectors/breezyhr/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/breezyhr/actions.py b/src/hrflow_connectors/connectors/breezyhr/actions.py
new file mode 100644
index 000000000..73781a79f
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/actions.py
@@ -0,0 +1,368 @@
+from typing import Iterator, Dict, Any, Optional
+from pydantic import Field
+import requests
+
+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
+
+logger = get_logger()
+
+
+class PullJobsAction(PullJobsBaseAction):
+
+    auth: OAuth2EmailPasswordBody
+    company_id: Optional[str] = Field(
+        None,
+        description="ID of company to pull jobs from in Breezy HR database associated with the authenticated user",
+    )
+    company_name: Optional[str] = Field(
+        None, description="the company associated with the authenticated user"
+    )
+
+    def pull(self) -> Iterator[Dict[str, Any]]:
+        """
+        Pull jobs from a Taleez jobs owner endpoint
+        Returns list of all jobs that have been pulled
+        """
+
+        def get_company_id() -> str:
+            """
+            Get the company id associated with the authenticated user company
+            """
+            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 = self.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"]
+
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = (
+            f"https://api.breezy.hr/v3/company/{get_company_id()}/positions?"
+        )
+        pull_jobs_request.auth = self.auth
+        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 this endpoint")
+            error_message = "Unable to pull the data ! Reason : `{}` `{}`"
+            raise RuntimeError(
+                error_message.format(response.status_code, response.content)
+            )
+
+        return response.json()
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format a Breezy Hr job object into a hrflow job object
+
+        Returns:
+            Dict[str, Any]: a job object in the hrflow job format
+        """
+        job = dict()
+
+        # Basic information
+        job["name"] = data.get("name")
+        logger.info(job["name"])
+        job["reference"] = data.get("_id")
+        logger.info(job["reference"])
+        job["summary"] = None
+
+        # Location
+        location = data.get("location")
+        country = location.get("country")
+        country_name = country.get("name")
+        city = location.get("city")
+        address = location.get("name")
+        geojson = dict(country=country_name, city=city)
+
+        job["location"] = dict(text=address, geojson=geojson, lat=None, lng=None)
+
+        # Sections
+        description = remove_html_tags(data.get("description"))
+        cleaned_description = description.replace("&nbsp;", " ")
+        job["sections"] = [
+            dict(
+                name="breezy_hr_description",
+                title="Breezy_hr_description",
+                description=cleaned_description,
+            )
+        ]
+        # tags
+        job["tags"] = []
+
+        def create_tag(field_name: str):
+            tag_name = "breezy_hr_{}".format(field_name)
+            tag_value = data.get(field_name)
+
+            if isinstance(tag_value, dict):
+                tag_name_value = tag_value.get("name")
+                tag = dict(name=tag_name, value=tag_name_value)
+                job["tags"].append(tag)
+            if isinstance(tag_value, str):
+                tag = dict(name=tag_name, value=tag_value)
+                job["tags"].append(tag)
+
+        create_tag("type")
+        create_tag("experience")
+        create_tag("education")
+        create_tag("department")
+        create_tag("requisition_id")
+        create_tag("category")
+        create_tag("candidate_type")
+        is_remote = dict(name="breezy_hr_remote", value=location.get("is_remote"))
+        job["tags"].append(is_remote)
+
+        job["created_at"] = data.get("creation_date")
+        job["updated_at"] = data.get("updated_date")
+
+        job["metadatas"] = data.get("tags")
+        return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    auth: OAuth2EmailPasswordBody
+    company_id: Optional[str] = Field(
+        None,
+        description="ID of company to pull jobs from in Breezy HR database associated with the authenticated user",
+    )
+    company_name: Optional[str] = Field(
+        None, description="the company associated with the authenticated user"
+    )
+    position_id: str = Field(
+        ..., description="Id of the position to create a new candidate for"
+    )
+    origin: Optional[str] = Field(
+        "sourced",
+        description="will indicate in Breezy if the candidate should be marked as sourced or applied",
+    )
+    cover_letter: Optional[str] = None
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format a Hrflow profile object into a breezy hr profile object
+
+        Args:
+            data (Dict[str, Any]): Hrflow Profile to format
+
+        Returns:
+            Dict[str, Any]: a BreezyHr formatted profile object
+        """
+
+        profile = dict()
+        info = data.get("info")
+        profile["name"] = info.get("full_name")
+        profile["address"] = info.get("location").get("text")
+        profile["email_address"] = info.get("email")
+        profile["phone_number"] = info.get("phone")
+        profile["summary"] = info.get("summary")
+        if self.origin is not None:
+            profile["origin"] = self.origin
+
+        profile["work_history"] = []
+
+        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"
+                )
+                format_experience["title"] = experience["title"]
+                format_experience["summary"] = experience["description"]
+                if experience["date_start"] is not None:
+                    date_iso = from_str_to_datetime((experience["date_start"]))
+                    format_experience["start_year"] = date_iso.year
+                    format_experience["start_month"] = date_iso.month
+                if experience["date_end"] is not None:
+                    date_end_iso = from_str_to_datetime((experience["date_end"]))
+                    format_experience["end_year"] = date_end_iso.year
+                    format_experience["end_month"] = date_end_iso.month
+
+                profile["work_history"].append(format_experience)
+
+        format_experiences()
+
+        profile["education"] = []
+
+        def format_educations():
+            educations = data.get("educations")
+            for education in educations:
+                format_education = dict()
+                if education["school"] == "":
+                    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"]
+                profile["education"].append(format_education)
+
+        format_educations()
+
+        profile["social_profiles"] = []
+
+        def format_urls() -> None:
+            """
+            format_urls, add links and websites to Taleez profile Social links
+            """
+            urls = info.get("urls")
+            if isinstance(urls, list):
+                for url in urls:
+                    type = url.get("type")
+                    link = url.get("url")
+                    if isinstance(link, str):
+                        profile["social_profiles"][type] = link
+            attachments = info.get("attachments")
+            if isinstance(attachments, list):
+                for attachment in attachments:
+                    file_name = attachment.get("file_name")
+                    public_url = attachment.get("public_url")
+                    if isinstance(public_url, str):
+                        profile["social_profiles"][file_name] = public_url
+
+        format_urls()
+        if self.cover_letter is not None:
+            profile["cover_letter"] = self.cover_letter
+
+        # add profile skills to tags
+        profile["tags"] = []
+        skills = data.get("skills")
+        if isinstance(skills, list):
+            for skill in skills:
+                if isinstance(skill, dict):
+                    profile["tags"].append(skill["name"])
+
+        return profile
+
+    def push(self, data: Dict[str, Any]) -> None:
+        """
+        Push a Hrflow profile object to a BreezyHr candidate pool for a position
+
+        Args:
+            data (Dict[str, Any]): profile to push
+        """
+        profile = next(data)
+        auth = self.auth
+        session = requests.Session()
+
+        def get_company_id() -> str:
+            """
+            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"]
+
+        self.company_id = get_company_id()
+
+        def get_candidate() -> Optional[Dict[str, Any]]:
+            """
+            Get candidate. If candidate does not exists, return None.
+
+            Returns:
+                Optional[Dict[str, Any]]: a dictionary that contains candidate id and name, if candidate doesn't exist it returns None
+            """
+            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()
+            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()
+
+        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()
+
+        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}`"
+                )
diff --git a/src/hrflow_connectors/connectors/breezyhr/connector.py b/src/hrflow_connectors/connectors/breezyhr/connector.py
new file mode 100644
index 000000000..ffb2ff824
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/connector.py
@@ -0,0 +1,79 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any, Union, List
+
+from ...core.connector import Connector
+from ...core.auth import OAuth2EmailPasswordBody
+
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Breezyhr(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow, board_key: str, auth: OAuth2EmailPasswordBody, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all your company available jobs in Breezyhr via their Breezy API. It adds all these jobs to a Hrflow.ai Board.
+
+        `Breezyhr` -> `Hrflow.ai`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (OAuth2EmailPasswordBody): Auth instance to identify and communicate with the platform
+            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 `[]`
+            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(
+            hrflow_client=hrflow_client, board_key=board_key, auth=auth, **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: OAuth2EmailPasswordBody,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        position_id: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Breezyhr` via their Salesforce API.
+
+        `Hrflow.ai` -> `Breezyhr`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (OAuth2EmailPasswordBody): Auth instance to identify and communicate with the platform
+            profile (Profile): Profile to push
+            position_id str: Id of the position to create a new candidate for, required.
+            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`
+            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 `[]`
+            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,
+            position_id=position_id,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md b/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md
new file mode 100644
index 000000000..68f12cc91
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/docs/pull_jobs.md
@@ -0,0 +1,53 @@
+# Pull jobs
+`Breezyhr` :arrow_right: `Hrflow.ai`
+
+`PullJobsAction` gets all your company available jobs in Breezy.hr via their ***Breezyhr API***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+
+**Links to Breezy.hr documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Get company_id](https://developer.breezy.hr/docs/companies)          | Endpoint to retrieve the companies associated with the authenticated user in case the user didn't specify his company ID required to `PullJobsAction`, the request method is `GET`            |
+| [Get all positions](https://developer.breezy.hr/docs/company-positions)          | Endpoint to retirve all positions for given company, required paramter is the company id, the request method is `GET`             |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `OAuth2EmailPasswordBody` | Auth instance to identify and communicate with the platform        |
+| `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`      |
+
+:red_circle: : *required*
+
+## Example
+
+```python
+from hrflow_connectors import Breezyhr
+from hrflow import Hrflow
+from hrflow_connectors import OAuth2EmailPasswordBody
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+auth = OAuth2EmailPasswordBody(
+            access_token_url="https://api.breezy.hr/v3/signin",
+            email="EMAIL",
+            password="PASSWORD",
+)
+
+Breezyhr.pull_jobs(
+    auth=auth,
+    subdomain="MY_SUBDOMAIN",
+    hrflow_client=client,
+    board_key="MY_BOARD_KEY",
+    company_name="MY_COMPANY_NAME",
+    hydrate_with_parsing=True,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md b/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md
new file mode 100644
index 000000000..0724155cd
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/docs/push_profile.md
@@ -0,0 +1,59 @@
+# Push profile
+`Hrflow.ai` :arrow_right: `Breezyhr`
+
+`PushProfileAction` pushes a Hrflow.ai profile to your company's `Breezyhr` candidate pool via their ***Breezy API***.
+
+**Links to Breezy.hr documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Get company_id](https://developer.breezy.hr/docs/companies)          | Endpoint to retrieve the companies associated with the authenticated user in case the user didn't specify his company ID required to `PushProfileAction`, the request method is `GET`           |
+| [Post candidate](https://developer.breezy.hr/docs/company-position-candidates-add)         |   Endpoint to Add a new candidate to a poisition, required parameters are company id and position id, the request method is `POST`    |
+|[Update a candidate on a position](https://developer.breezy.hr/docs/company-position-candidate-update)           | Endpoint to update a candidate on a position,in case the candidate already exists, required parameters are company id, position id and candidate id, the request method is `PUT`|
+|[Get candidate id](https://developer.breezy.hr/docs/company-candidates-search)| Endpoint to search that a candidate already exists to retrieve his id, required parameters are company id and candidate's email address, the request method is `GET`|
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `OAuth2EmailPasswordBody` | Auth instance to identify and communicate with the platform        |
+| `position_id` :red_circle: | `str` | Id of the position to create a new candidate for, required.      |
+| `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`      |
+| `origin` | `Optional[str]` | Indicates if the candidate is `sourced` or `applied`, Default value `sourced`      |
+| `cover_letter` | `Optional[str]` | Candidate cover letter, Default value `None`      |
+
+
+:red_circle: : *required*
+
+## Example
+
+```python
+from hrflow_connectors import Breezyhr
+from hrflow import Hrflow
+from hrflow_connectors import OAuth2EmailPasswordBody
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = OAuth2EmailPasswordBody(
+            access_token_url="https://api.breezy.hr/v3/signin",
+            email="EMAIL",
+            password="PASSWORD",
+)
+
+Breezyhr.push_profile(
+    auth=auth,
+    subdomain="SUBDOMAIN",
+    hrflow_client=client,
+    company_name="MY_COMPANY_NAME",
+    position_id="MY_POSITION_ID",
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/breezyhr/schemas.py b/src/hrflow_connectors/connectors/breezyhr/schemas.py
new file mode 100644
index 000000000..304f7aac6
--- /dev/null
+++ b/src/hrflow_connectors/connectors/breezyhr/schemas.py
@@ -0,0 +1,107 @@
+from typing import Dict, Any, Optional, List
+from pydantic import BaseModel, Field
+
+
+class Type(BaseModel):
+    id: str
+    name: str
+
+
+class Experience(BaseModel):
+    id: str
+    name: str
+
+
+class Country(BaseModel):
+    name: str
+    id: str
+
+
+class Location(BaseModel):
+    country: Country
+    city: str
+    is_remote: bool
+    name: str
+
+
+class Category(BaseModel):
+    id: str
+    name: str
+
+
+class ApplicationForm(BaseModel):
+    name: str
+    headline: str
+    summary: str
+    profile_photo: str
+    address: str
+    email_address: str
+    phone_number: str
+    resume: str
+    work_history: str
+    education: str
+    cover_letter: str
+    questionnaire_in_experience: bool
+
+
+class BreezyJobModel(BaseModel):
+    _id: str = Field(..., description="position id")
+    type: Type = Field(..., description="job type")
+    state: str = Field(
+        ...,
+        description="state of the position posting, published or internal and so on",
+    )
+    name: str = Field(..., description="job title")
+    friendly_id: str = Field(
+        ..., description="another id of the job which combines its title and its id"
+    )
+    experience: Experience
+    location: Location
+    education: str
+    department: str
+    description: str = Field(..., description="Job category")
+    category: Category
+    application_form: Optional[ApplicationForm] = Field(
+        ..., description="job Application for"
+    )
+    creator_id: Optional[str]
+    creation_date: str
+    updated_date: str
+    all_users: List[str]
+    all_admins: List[str]
+    candidate_type: str
+    tags: List
+    org_type: str
+
+
+class WorkHistoryItem(BaseModel):
+    company_name: str
+    title: str
+    summary: str
+    start_month: int
+    start_year: int
+    end_month: int
+    end_year: int
+
+
+class EducationItem(BaseModel):
+    school_name: str
+    field_of_study: str
+    start_year: int
+    end_year: int
+
+
+class BreezyProfileModel(BaseModel):
+    name: str
+    email_address: str
+    phone_number: str
+    summary: str
+    tags: Optional[List[str]]
+    source: Optional[str]
+    origin: Optional[str]
+    address: str
+    work_history: List[WorkHistoryItem]
+    education: List[EducationItem]
+    social_profiles: Optional[List[str]]
+    custom_attributes: Optional[List[Dict[str, Any]]]
+    cover_letter: Optional[str]
diff --git a/src/hrflow_connectors/connectors/ceridian/README.md b/src/hrflow_connectors/connectors/ceridian/README.md
new file mode 100644
index 000000000..629844617
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/README.md
@@ -0,0 +1,8 @@
+# Ceridian Dayforce Connector
+> Cloud HCM software that brings together real-time payroll, HR, benefits, time reporting, talent and workforce management to deliver intelligence at work.
+
+🔗 https://www.ceridian.com/
+
+| Actions |
+| ------- |
+| [**Pull jobs**](docs/pull_jobs.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/ceridian/__init__.py b/src/hrflow_connectors/connectors/ceridian/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/ceridian/actions.py b/src/hrflow_connectors/connectors/ceridian/actions.py
similarity index 77%
rename from src/hrflow_connectors/connectors/boards/ceridian/actions.py
rename to src/hrflow_connectors/connectors/ceridian/actions.py
index fdebaf4d6..3e4b86ec2 100644
--- a/src/hrflow_connectors/connectors/boards/ceridian/actions.py
+++ b/src/hrflow_connectors/connectors/ceridian/actions.py
@@ -1,52 +1,42 @@
 from typing import Iterator, Dict, Any
 from pydantic import Field
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
+import requests
 
-logger = get_logger()
+from ...core.action import PullJobsBaseAction
 
 
-class PullJobs(HTTPStream, BoardAction):
+class PullJobsAction(PullJobsBaseAction):
+
     subdomain: str = Field(..., description="subdomain just before `dayforcehcm.com`")
     client_name_space: str = Field(
         ...,
         description="Uniquely identifies the client's Dayforce instance. Is needed to login",
     )
 
-    @property
-    def base_url(self):
-        return "https://{}.dayforcehcm.com/Api/{}/V1/JobFeeds".format(
-            self.subdomain, self.client_name_space
-        )
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[Dict[str, Any]]:
         """
         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
         """
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = f"https://{self.subdomain}.dayforcehcm.com/Api/{self.client_name_space}/V1/JobFeeds"
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+
+        if not response.ok:
 
-        response = self.send_request()
-        if response.ok:
-            job_dict_list = response.json()
-            return job_dict_list
-        else:
-            logger.error(
-                f"Failed to pull jobs, check that your subdomain: {self.subdomain}, and client name space: {self.client_name_space}"
-            )
             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]:
         """
@@ -57,10 +47,12 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
             Dict[str, Any]: a job into the hrflow job object format
         """
         job = dict()
-        #basic information
+        # basic information
         job["name"] = data.get("Title")
         job["summary"] = None
-        job["reference"] = str(data.get("ReferenceNumber")) + str(data.get("ParentRequisitionCode"))
+        job["reference"] = str(data.get("ReferenceNumber")) + str(
+            data.get("ParentRequisitionCode")
+        )
         job["url"] = data.get("JobDetailsUrl")
         # location
         location = data.get("City")
@@ -69,7 +61,7 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         postal_code = data.get("PostalCode")
         geojson = dict(state=state, country=country, postal_code=postal_code)
         job["location"] = dict(text=location, lat=None, lng=None, geojson=geojson)
-        #sections
+        # sections
         description = data.get("Description")
         job["sections"] = [
             dict(
diff --git a/src/hrflow_connectors/connectors/ceridian/connector.py b/src/hrflow_connectors/connectors/ceridian/connector.py
new file mode 100644
index 000000000..9b4bb1438
--- /dev/null
+++ b/src/hrflow_connectors/connectors/ceridian/connector.py
@@ -0,0 +1,45 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from .actions import PullJobsAction
+
+
+class Ceridian(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow,
+        board_key: str,
+        subdomain: str,
+        client_name_space: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs listed on Ceridian Dayforce specific endpoints. It adds all these jobs to a Hrflow.ai Board.
+
+        `Ceridian Dayforce` -> `Hrflow.ai`
+
+        Args:
+            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): 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 `[]`
+            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`
+            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(
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            subdomain=subdomain,
+            client_name_space=client_name_space,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/ceridian/README.md b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
similarity index 68%
rename from src/hrflow_connectors/connectors/boards/ceridian/README.md
rename to src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
index 58281f8bc..0b823b44b 100644
--- a/src/hrflow_connectors/connectors/boards/ceridian/README.md
+++ b/src/hrflow_connectors/connectors/ceridian/docs/pull_jobs.md
@@ -1,12 +1,15 @@
-# Ceridian Dayforce Connector
-**Ceridian Dayforce is a comprehensive cloud platform that combines HR, payroll, benefits, workforce management, and talent management in a single application.**
+# Pull jobs
 
 `Ceridian Dayforce` :arrow_right: `Hrflow.ai`
 
-## PullJobs
-`PullJobs` gets all available jobs listed on ***Ceridian Dayforce specific endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs listed on ***Ceridian Dayforce specific endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+**Links to Ceridian Dayforce documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+|[Get Job Feeds](https://developers.dayforce.com/Build/API-Explorer/Recruiting/Get-Job-Postings.aspx)         | Endpoint to get job postings that have been posted externally and are available through the candidate portal, a clientNamespace parameter is required, the reauest method is `Get`            |
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -22,22 +25,19 @@
 | `client_name_space` :red_circle: | `str` | Uniquely identifies the client's Dayforce instance for example. Is needed to login |
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Ceridian
 from hrflow import Hrflow
-from hrflow_connectors.connectors.boards.ceridian import PullJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-action = PullJobs(
+
+Ceridian.pull_jobs(
     subdomain="MY_SUBDOMAIN",
     client_name_space="MY_CLIENT_NAME_SPACE"
     hrflow_client=client,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/ceridian/schemas.py b/src/hrflow_connectors/connectors/ceridian/schemas.py
similarity index 93%
rename from src/hrflow_connectors/connectors/boards/ceridian/schemas.py
rename to src/hrflow_connectors/connectors/ceridian/schemas.py
index a939f8712..e853b4c1b 100644
--- a/src/hrflow_connectors/connectors/boards/ceridian/schemas.py
+++ b/src/hrflow_connectors/connectors/ceridian/schemas.py
@@ -19,4 +19,4 @@ class CeridianDayforceJobModel(BaseModel):
     LastUpdated: str
     ReferenceNumber: int
     ParentRequisitionCode: int
-    IsVirtualLocation: bool
\ No newline at end of file
+    IsVirtualLocation: bool
diff --git a/src/hrflow_connectors/connectors/crosstalent/README.md b/src/hrflow_connectors/connectors/crosstalent/README.md
new file mode 100644
index 000000000..35b44511b
--- /dev/null
+++ b/src/hrflow_connectors/connectors/crosstalent/README.md
@@ -0,0 +1,9 @@
+# Crosstalent Connector
+> Crosstalent designs and implements innovative solutions for human resources management.
+
+🔗 https://www.crosstalent.eu/fr/
+
+| 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/crosstalent/__init__.py b/src/hrflow_connectors/connectors/crosstalent/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/crosstalent/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/crosstalent/actions.py b/src/hrflow_connectors/connectors/crosstalent/actions.py
similarity index 81%
rename from src/hrflow_connectors/connectors/boards/crosstalent/actions.py
rename to src/hrflow_connectors/connectors/crosstalent/actions.py
index 1ed8b818f..e7273d181 100644
--- a/src/hrflow_connectors/connectors/boards/crosstalent/actions.py
+++ b/src/hrflow_connectors/connectors/crosstalent/actions.py
@@ -1,13 +1,14 @@
 from typing import Iterator, Dict, Any
+from pydantic import Field
+import requests
 
-from ....core.auth import OAuth2PasswordCredentialsBody
-from ....core.http import HTTPStream
-from ....core.action import BoardAction
+from ...core.auth import OAuth2PasswordCredentialsBody
 
-from pydantic import Field
+from ...core.action import PullJobsBaseAction, PushProfileBaseAction
+from ...utils.hrflow import generate_workflow_response
 
 
-class GetAllJobs(HTTPStream, BoardAction):
+class PullJobsAction(PullJobsBaseAction):
     auth: OAuth2PasswordCredentialsBody = Field(
         ..., description="Auth instance to identify and communicate with the platform"
     )
@@ -16,24 +17,24 @@ class GetAllJobs(HTTPStream, BoardAction):
         description="Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`",
     )
 
-    @property
-    def base_url(self):
-        return "https://{}.salesforce.com/services/apexrest/crta/HrFlowGetJobOffers/".format(
-            self.subdomain
-        )
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[Dict[str, Any]]:
-        response = self.send_request()
-        if response.status_code == 200:
-            return response.json()
-        else:
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = f"https://{self.subdomain}.salesforce.com/services/apexrest/crta/HrFlowGetJobOffers/"
+        pull_jobs_request.auth = self.auth
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+
+        if not response.ok:
             error_message = "Unable to pull the data ! Reason : `{}`"
             raise ConnectionError(error_message.format(response.content))
 
+        return response.json()
+
     def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         job = dict()
 
@@ -269,7 +270,7 @@ def create_metadata(field_name: str):
                 metadata = dict(name=metadata_name, value=metadata_value)
                 job["metadatas"].append(metadata)
 
-        ## TODO Add metadata
+        ## Add metadata
         create_metadata("crta__CT_Code__c")
         create_metadata("crta__CT_Designation__c")
         create_metadata("crta__CT_Support__c")
@@ -292,3 +293,53 @@ def create_metadata(field_name: str):
         create_metadata("Besoin_client__c")
 
         return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+    auth: OAuth2PasswordCredentialsBody
+    subdomain: str = Field(
+        ...,
+        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):
+        firstname = data["info"].get("first_name")
+        lastname = data["info"].get("last_name")
+        email = data["info"].get("email")
+        key = data["key"]
+
+        # Case if lastname is not defined after CV parsing
+        # This field is required for Crosstalent
+        if lastname is None or lastname == "":
+            data["info"]["last_name"] = "N/A"
+
+        # Case if email is not defined after CV parsing
+        # This field is required for Crosstalent
+        if email is None or email == "":
+            if firstname is not None and lastname is not None:
+                email = f"{firstname}.{lastname}@vulcain.com"
+            else:
+                email = f"{key}@vulcain.com"
+            data["info"]["email"] = email
+
+        return data
+
+    def push(self, data):
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        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
+        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}`"
+            )
diff --git a/src/hrflow_connectors/connectors/crosstalent/connector.py b/src/hrflow_connectors/connectors/crosstalent/connector.py
new file mode 100644
index 000000000..570286a5a
--- /dev/null
+++ b/src/hrflow_connectors/connectors/crosstalent/connector.py
@@ -0,0 +1,81 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import OAuth2PasswordCredentialsBody
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Crosstalent(Connector):
+    @staticmethod
+    def pull_jobs(
+        auth: OAuth2PasswordCredentialsBody,
+        hrflow_client: Hrflow,
+        board_key: str,
+        subdomain: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs via their Salesforce API. It adds all these jobs to a Hrflow.ai Board.
+
+        `Crosstalent` -> `Hrflow.ai`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            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 `[]`
+            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,
+            subdomain=subdomain,
+            **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: OAuth2PasswordCredentialsBody,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        subdomain: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Crosstalent` via their Salesforce API.
+
+        `Hrflow.ai` -> `Crosstalent`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            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 `[]`
+            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,
+            subdomain=subdomain,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/crosstalent/README.md b/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
similarity index 67%
rename from src/hrflow_connectors/connectors/boards/crosstalent/README.md
rename to src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
index 9a263d657..1d642cd36 100644
--- a/src/hrflow_connectors/connectors/boards/crosstalent/README.md
+++ b/src/hrflow_connectors/connectors/crosstalent/docs/pull_jobs.md
@@ -1,12 +1,9 @@
-# Crosstalent Connector
-**Crosstalent designs and implements innovative solutions for human resources management.**
-
+# Pull jobs
 `Crosstalent` :arrow_right: `Hrflow.ai`
 
-## GetAllJobs
-`GetAllJobs` gets all available jobs via their ***Salesforce API***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs via their ***Salesforce API***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -21,38 +18,29 @@
 | `auth` :red_circle: | `OAuth2PasswordCredentialsBody` | Auth instance to identify and communicate with the platform        |
 | `subdomain` :red_circle: | `str` | Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`        |
 
-:red_circle: : *required* 
+:red_circle: : *required*
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Crosstalent
 from hrflow import Hrflow
-
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.connectors.boards.crosstalent import GetAllJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+from hrflow_connectors import OAuth2PasswordCredentialsBody
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-
-access_token_url = "https://test.salesforce.com/services/oauth2/token"
 auth = OAuth2PasswordCredentialsBody(
-    access_token_url=access_token_url,
+    access_token_url="https://test.salesforce.com/services/oauth2/token",
     client_id="MY_CLIENT_ID",
     client_secret="MY_CLIENT_SECRET",
     username="MY_USERNAME",
     password="MY_PASSWORD",
 )
 
-action = GetAllJobs(
+Crosstalent.pull_jobs(
     auth=auth,
     subdomain="MY_SUBDOMAIN",
     hrflow_client=client,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md b/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md
new file mode 100644
index 000000000..842dd9040
--- /dev/null
+++ b/src/hrflow_connectors/connectors/crosstalent/docs/push_profile.md
@@ -0,0 +1,44 @@
+# Push profile
+`Hrflow.ai` :arrow_right: `Crosstalent`
+
+`PushProfileAction` pushes a Hrflow.ai profile to `Crosstalent` via their ***Salesforce API***.
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `OAuth2PasswordCredentialsBody` | Auth instance to identify and communicate with the platform        |
+| `subdomain` :red_circle: | `str` | Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`        |
+
+:red_circle: : *required*
+
+## Example
+```python
+from hrflow_connectors import Crosstalent
+from hrflow import Hrflow
+from hrflow_connectors import OAuth2PasswordCredentialsBody
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = OAuth2PasswordCredentialsBody(
+    access_token_url="https://test.salesforce.com/services/oauth2/token",
+    client_id="CLIENT_ID",
+    client_secret="CLIENT_SECRET",
+    username="USERNAME",
+    password="PASSWORD",
+)
+
+Crosstalent.push_profile(
+    auth=auth,
+    subdomain="SUBDOMAIN",
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/crosstalent/README.md b/src/hrflow_connectors/connectors/destinations/crosstalent/README.md
deleted file mode 100644
index be8577c71..000000000
--- a/src/hrflow_connectors/connectors/destinations/crosstalent/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Crosstalent Connector
-**Crosstalent designs and implements innovative solutions for human resources management.**
-
-`Hrflow.ai` :arrow_right: `Crosstalent`
-
-## PushProfile
-`PushProfile` pushes a Hrflow.ai profile to `Crosstalent` via their ***Salesforce API***.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `OAuth2PasswordCredentialsBody` | Auth instance to identify and communicate with the platform        |
-| `subdomain` :red_circle: | `str` | Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.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 import Hrflow
-
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.connectors.destinations.crosstalent import PushProfile
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        access_token_url = "https://test.salesforce.com/services/oauth2/token"
-        auth = OAuth2PasswordCredentialsBody(
-            access_token_url=access_token_url,
-            client_id=settings["CLIENT_ID"],
-            client_secret=settings["CLIENT_SECRET"],
-            username=settings["USERNAME"],
-            password=settings["PASSWORD"],
-        )
-
-        action = PushProfile(
-            auth=auth,
-            subdomain=settings["SUBDOMAIN"],
-            hrflow_client=client,
-            profile=profile,
-        )
-        response = action.execute()
-        return response
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/crosstalent/__init__.py b/src/hrflow_connectors/connectors/destinations/crosstalent/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/crosstalent/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/crosstalent/actions.py b/src/hrflow_connectors/connectors/destinations/crosstalent/actions.py
deleted file mode 100644
index 717ff5668..000000000
--- a/src/hrflow_connectors/connectors/destinations/crosstalent/actions.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from ....core.auth import OAuth2PasswordCredentialsBody
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....utils.hrflow import generate_workflow_response
-
-from pydantic import Field
-from typing import Dict, Any
-
-
-class PushProfile(ProfileDestinationAction, HTTPStream):
-    payload: Dict[str, Any] = dict()
-    auth: OAuth2PasswordCredentialsBody
-    subdomain: str = Field(
-        ...,
-        description="Subdomain Crosstalent just before `salesforce.com`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.salesforce.com/ABC`",
-    )
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-
-    @property
-    def base_url(self):
-        return "https://{}.salesforce.com/services/apexrest/crta/HrFlowCreateProfile".format(
-            self.subdomain
-        )
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def format(self, data):
-        firstname = data["info"].get("first_name")
-        lastname = data["info"].get("last_name")
-        email = data["info"].get("email")
-        key = data["key"]
-
-        # Case if lastname is not defined after CV parsing
-        # This field is required for Crosstalent
-        if lastname is None or lastname == "":
-            data["info"]["last_name"] = "N/A"
-
-        # Case if email is not defined after CV parsing
-        # This field is required for Crosstalent
-        if email is None or email == "":
-            if firstname is not None and lastname is not None:
-                email = f"{firstname}.{lastname}@vulcain.com"
-            else:
-                email = f"{key}@vulcain.com"
-            data["info"]["email"] = email
-
-        return data
-
-    def push(self, data):
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to Crosstalent failed : `{}`".format(response.content)
-            )
diff --git a/src/hrflow_connectors/connectors/destinations/crosstalent/spec.py b/src/hrflow_connectors/connectors/destinations/crosstalent/spec.py
deleted file mode 100644
index 20d1fb6ac..000000000
--- a/src/hrflow_connectors/connectors/destinations/crosstalent/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import ProfileDestinationAction
-
-
-class Spec(BaseModel):
-    actions: Tuple[ProfileDestinationAction]
diff --git a/src/hrflow_connectors/connectors/destinations/flatchr/README.md b/src/hrflow_connectors/connectors/destinations/flatchr/README.md
deleted file mode 100644
index 2cef920fe..000000000
--- a/src/hrflow_connectors/connectors/destinations/flatchr/README.md
+++ /dev/null
@@ -1,148 +0,0 @@
-# Flatchr Connector
-**Flatchr is an ATS.**
-
-`Hrflow.ai` :arrow_right: `Flatchr`
-
-Lists of actions :
-* [**`PushProfile`**](#pushprofile)
-* [**`EnrichProfile`**](#enrichprofile)
-
-## PushProfile
-
-`PushProfile` pushes a Hrflow.ai profile to `Flatchr` via their API.
-
-This connector sends **only basic information**:
-* First name
-* Last name
-* Email
-* CV
-
-**To add more information to the profile**, you need to enrich the created profile with the [**`EnrichProfile`**](#enrichprofile) connector.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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        |
-| `subdomain` :red_circle: | `str` | Subdomain flatchr just before `flatchr.io`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.flatchr.io/ABC`        |
-| `vacancy` :red_circle: | `str` | The pool in which candidates will be placed. Findable in the URL        |
-
-: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 import Hrflow
-
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.flatchr import PushProfile
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        auth = AuthorizationAuth(value=settings["FLATCHR_KEY"])
-
-        action = PushProfile(
-            auth=auth,
-            subdomain="careers",
-            hrflow_client=client,
-            profile=profile,
-            vacancy="k0M5O9ylKZnxbQBy",
-        )
-        response = action.execute()
-```
-
-## EnrichProfile
-
-`EnrichProfile` allows to enrich a flatchr profile with information from a ***Hrflow.ai Profile***. 
-
-⚠️ **The *Flatchr Profile* must first be created with for example the [**`PushProfile`**](#pushprofile) connector.**
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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        |
-| `subdomain` :red_circle: | `str` | Subdomain flatchr just before `flatchr.io`. For example subdomain=`my_subdomain.my` in `http://my_subdomain.my.flatchr.io/ABC`        |
-| `vacancy` :red_circle: | `str` | The pool in which candidates will be placed. Findable in the URL        |
-| `compagny` :red_circle: | `str` | The id of the compagny        |
-
-: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 import Hrflow
-
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.flatchr import PushProfile, EnrichProfile
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        auth = AuthorizationAuth(value=settings["FLATCHR_KEY"])
-
-        push_profile_action = PushProfile(
-            auth=auth,
-            subdomain="careers",
-            hrflow_client=client,
-            profile=profile,
-            vacancy="k0M5O9ylKZnxbQBy",
-        )
-        push_profile_action.execute()
-
-        enrich_profile_action = EnrichProfile(
-            auth=auth,
-            subdomain="api",
-            hrflow_client=client,
-            profile=profile,
-            vacancy="k0M5O9ylKZnxbQBy",
-            compagny="LEZBvp5b4LdMoVmg",
-        )
-        response = enrich_profile_action.execute()
-        return response
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/flatchr/__init__.py b/src/hrflow_connectors/connectors/destinations/flatchr/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/flatchr/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/flatchr/spec.py b/src/hrflow_connectors/connectors/destinations/flatchr/spec.py
deleted file mode 100644
index f963a319b..000000000
--- a/src/hrflow_connectors/connectors/destinations/flatchr/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PushProfile, EnrichProfile
-
-
-class Spec(BaseModel):
-    actions: Tuple[PushProfile, EnrichProfile]
diff --git a/src/hrflow_connectors/connectors/destinations/greenhouse/README.md b/src/hrflow_connectors/connectors/destinations/greenhouse/README.md
deleted file mode 100644
index f48828a6c..000000000
--- a/src/hrflow_connectors/connectors/destinations/greenhouse/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Greenhouse Connector
-**Greenhouse is an ATS.**
-
-`Hrflow.ai` :arrow_right: `Greenhouse`
-
-## PushProfile
-`PushProfile` pushes a `Profile` from a ***HrFlow Source*** to a ***Greenhouse*** Jobs pool..
-
-## SCHEMAS
-add `schemas.py` as ***basemodel*** for a ***Greenhouse profile object***.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `Union[AuthorizationAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
-| `job_id` :red_circle: | `List[int]` | List of jobs internal ids to which the candidate should be added |
-| `on_behalf_of` :red_circle: | `str` | The ID of the user sending the profile, or the person he is sending the profile on behalf of |
-
-:red_circle: : *required* 
-
-### Example
-
-```python
-from hrflow import Hrflow
-
-from hrflow_connectors.connectors.destinations.greenhouse import PushProfile
-from hrflow_connectors.connectors.core.auth import OAuth2PasswordCredentialsBody, AuthorizationAuth
-from hrflow_connectors.connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-
-        auth = AuthorizationAuth(
-        name = 'Authorization',
-        value= settings['AUTHORIZATION'],
-    )
-
-        action = PushProfile(
-            auth=auth,
-            job_id=settings["JOB_ID"],
-            on_behalf_of=settings["ON_BEHALF_OF"],
-            hrflow_client=client,
-            profile=profile,
-        )
-        response = action.execute()
-        return response
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/greenhouse/__init__.py b/src/hrflow_connectors/connectors/destinations/greenhouse/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/greenhouse/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/greenhouse/actions.py b/src/hrflow_connectors/connectors/destinations/greenhouse/actions.py
deleted file mode 100644
index 264885a1f..000000000
--- a/src/hrflow_connectors/connectors/destinations/greenhouse/actions.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from pydantic import Field
-from typing import Dict, Any, Optional, Union, List
-from ....utils.logger import get_logger
-
-logger = get_logger()
-
-
-class PushProfile(ProfileDestinationAction, HTTPStream):
-
-    auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]
-    payload: Dict[str, Any] = dict()
-    job_id: List[int] = Field(
-        ...,
-        description="List of jobs internal ids to which the candidate should be added",
-    )
-    on_behalf_of: str = Field(..., description="The ID of the user sending the profile, or the person he is sending the profile on behalf of")
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-        self.headers["on-behalf-of"] = self.on_behalf_of
-
-    @property
-    def base_url(self):
-        return "https://harvest.greenhouse.io/v1/candidates"
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        Format a profile hrflow object to a greenhouse profile object
-        Args:
-            profile (Dict[str, Any]): profile object in the hrflow profile format
-        Returns:
-            Dict[str, Any]: profile in the greenhouse candidate  format
-        """
-        profile = dict()
-        profile["applications"] = []
-        for id in self.job_id:
-            profile["applications"].append(dict(job_id=id))
-
-        if self.job_id is not None:
-            profile["job_id"] = self.job_id
-
-        profile["first_name"] = data.get("info").get("first_name")
-        profile["last_name"] = data.get("info").get("last_name")
-        profile["external_id"] = data.get("reference")
-
-        if data.get("attachments") not in [[], None]:
-            profile["resume"] = data.get("attachments")[0]["public_url"]
-
-        phone_number = data.get("info").get("phone")
-        profile["phone_numbers"] = [dict(value=phone_number, type="mobile")]
-
-        email = data.get("info").get("email")
-        profile["email_addresses"] = [dict(value=email, type="personal")]
-
-        address = data.get("info").get("location").get("text")
-        profile["addresses"] = [dict(value=address, type="home")]
-
-        profile["notes"] = data.get("text")
-
-        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"]))
-            return website_list
-
-        if get_social_media_urls() not in [[], None]:
-            profile["social_media_addresses"] = get_social_media_urls()
-
-        if data["experiences"] not in [[], None]:
-            last_experience = data["experiences"][0]
-            profile["company"] = last_experience["company"]
-            profile["title"] = last_experience["title"]
-            profile["employments"] = []
-            for experience in data["experiences"]:
-                if (
-                    experience["title"]
-                    and experience["company"]
-                    and experience["date_start"]
-                ) not in ["", None]:
-                    profile["employments"].append(
-                        dict(
-                            company_name=experience["company"],
-                            title=experience["title"],
-                            start_date=experience["date_start"],
-                            end_date=experience["date_end"],
-                        )
-                    )
-
-        return profile
-
-    def push(self, data: Dict[str, Any]):
-        """
-        Push profile
-
-        Args:
-            data (Dict[str, Any]): Profile
-        """
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        logger.debug(f"{response.status_code},{response.content}")
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to Greenhouse failed : {}, `{}`".format(
-                    response.status_code, response.content
-                )
-            )
diff --git a/src/hrflow_connectors/connectors/destinations/greenhouse/spec.py b/src/hrflow_connectors/connectors/destinations/greenhouse/spec.py
deleted file mode 100644
index afbaea8a0..000000000
--- a/src/hrflow_connectors/connectors/destinations/greenhouse/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PushProfile
-
-
-class Spec(BaseModel):
-    actions: Tuple[PushProfile]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/monster/README.md b/src/hrflow_connectors/connectors/destinations/monster/README.md
deleted file mode 100644
index 9cc27c63f..000000000
--- a/src/hrflow_connectors/connectors/destinations/monster/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Monster Connector
-**Monster is an ATS.**
-
-`Monster` :arrow_right: `HrFlow`
-
-Lists of actions :
-* [**`PullProfile`**](#PullProfile)
-
-## PullProfile
-
-`PullProfile` catches a Monster profile to `Hrflow.ai`.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `source_key` :red_circle: | `String` | Source key where the profiles to be added will be stored        |
-| `request` :red_circle: | `Dict[str, Any]` | Body to format in HrFlow Profile        |
-| `file_field` | `String` | Name of the field containing the file       |
-| `hrflow_client` :red_circle: | hrflow.Hrflow | Hrflow client instance used to communicate with the Hrflow.ai API        |
-
-
-: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 import Hrflow
-
-from hrflow_connectors.connectors.destinations.flatchr import PullProfile
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        action = PullProfile(
-            hrflow_client=hrflow_client(),
-            request=profile,
-            source_key="8df6a1247b1a95e0b84f5226093ff2c58e60cdf1",
-            file_field="FileContents",
-        )
-        response = action.execute()
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/monster/actions.py b/src/hrflow_connectors/connectors/destinations/monster/actions.py
deleted file mode 100644
index 0e0527746..000000000
--- a/src/hrflow_connectors/connectors/destinations/monster/actions.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from ....core.action import CatchProfile
-import xml.etree.ElementTree
-from ....utils.logger import get_logger
-from ....utils.hrflow import generate_workflow_response
-
-from pydantic import Field
-from typing import Iterator, Union, Dict, Any
-
-TalentDataType = Union[str, xml.etree.ElementTree.Element, Dict[str, Any]]
-logger = get_logger()
-
-
-class PullProfile(CatchProfile):
-
-    def format(self, request: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        Format the input data into a push-ready data schema
-        WARNING: If you want to map the format function to rewrite a pipeline of an `Action` from a connector,
-        you should use the function `format_switcher`, not the `format` of the parent class.
-        If you take `format` from the parent class, then `format_function_name` will be ignored
-        and only `format` will be used to format the data.
-        Args:
-            request (Dict[str, Any]): body we want to adapt to the output format
-        Returns:
-            Dict[str, Any]: parameters to put in the parsing endpoint
-        """
-
-        def get_binary_resume(FileContents):
-            byte_array = bytearray(FileContents)
-            binary_resume = bytes(byte_array)
-            return binary_resume
-
-        hrflow_tags = [{
-                "name": "JobRefID",
-                "value": request["JobRefID"]
-            }]
-        output_data = {
-            "source_key": self.source_key,
-            "profile_file": get_binary_resume(self.request["FileContents"]),
-            "tags": hrflow_tags
-        }
-        return output_data
diff --git a/src/hrflow_connectors/connectors/destinations/monster/schemas.py b/src/hrflow_connectors/connectors/destinations/monster/schemas.py
deleted file mode 100644
index f19551c1c..000000000
--- a/src/hrflow_connectors/connectors/destinations/monster/schemas.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import Optional, Union, List
-from pydantic import BaseModel, Field
-
-
-class MonsterProfile(BaseModel):
-    City: str = Field(..., Description="City of the profile")
-    CountryCode: str = Field(..., Description="CountryCode of the profile")
-    EmailAddress: str = Field(..., Description="Email address of the profile")
-    FileContents: List[int] = Field(..., Description="Content of the resume of the profile on the form of a list of int")
-    FileExt: str = Field(..., Description="This contains the resume in its original format as it was made available "
-                                          "by the applicant. It is a C#, unsigned Byte Array. If the resume was "
-                                          "created interactively on Monster, the resume is provided as consistently "
-                                          "formatted HTML. If an applicant originally uploaded a resume to Monster ("
-                                          "e.g., as a PDF), it is delivered in the original document format. Resume "
-                                          "size can be up to 10 MB. Due to some additional data and overhead coming "
-                                          "from the serialization / deserialization process, XML format should be set "
-                                          "to accept up to 8 MB/ JSON format should be set to accept up to 20 MB.")
-    FirstName: str = Field(..., Description="First name of the profile")
-    JobRefID: str = Field(..., Description="Reference of the profile")
-    LastName: str = Field(..., Description="Last name of the profile")
-    PhoneNumber: str = Field(..., Description="Phone number of the profile")
-    ResumeValue: str = Field(..., Description="This is a unique alpha numeric value on the Monster side.")
-    State: str
-    VendorField: str = Field(..., Description="Commentary from the job")
-    WorkAuthorization: int = Field(..., Description="This is a value of whether the applicant is authorized to work "
-                                                    "in the country provided in the countryCode")
-    ZIPCode: str = Field(..., Description="ZIP code of the profile")
diff --git a/src/hrflow_connectors/connectors/destinations/monster/spec.py b/src/hrflow_connectors/connectors/destinations/monster/spec.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/hrflow_connectors/connectors/destinations/recruitee/__init__.py b/src/hrflow_connectors/connectors/destinations/recruitee/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/recruitee/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/recruitee/actions.py b/src/hrflow_connectors/connectors/destinations/recruitee/actions.py
deleted file mode 100644
index ac22a7ff1..000000000
--- a/src/hrflow_connectors/connectors/destinations/recruitee/actions.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....core.auth import AuthorizationAuth
-from pydantic import Field
-from typing import Dict, Any, Optional, List
-from ....utils.logger import get_logger
-
-logger = get_logger()
-
-
-class PushProfile(ProfileDestinationAction, HTTPStream):
-
-    auth: AuthorizationAuth
-    payload: Dict[str, Any] = dict()
-    company_id: str = Field(
-        ..., description="Company ID. A company subdomain can also be used."
-    )
-    offer_id: Optional[List[int]] = Field(
-        None,
-        description="Offers to which the candidate will be assigned with default stage. You can also pass one ID as offer_id",
-    )
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-
-    @property
-    def base_url(self):
-        return "https://api.recruitee.com/c/{}/candidates".format(self.company_id)
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def push(self, data: Dict[str, Any]):
-        """
-        Push profile
-        Args:
-            data (Dict[str, Any]): Profile
-        """
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        logger.debug(f"{response.status_code},{response.content}")
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to Recruitee failed : {}, `{}`".format(
-                    response.status_code, response.content
-                )
-            )
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        format a HrFlow Profile object into a Recruitee profile Object
-        returns Dict[str, Any]: a profile in the format of Recruitee profiles
-        """
-        profile = dict()
-        info = data.get("info")
-        profile["name"] = info.get("full_name")
-        if data.get("attachments") not in [None, []]:
-            profile["remote_cv_url"] = data.get("attachments")[0].get("public_url")
-        profile["emails"] = [str(info.get("email"))]
-        profile["phones"] = [str(info.get("phone"))]
-
-        def urls():
-            urls = info.get("urls")
-            website_list = []
-            for url in urls:
-                if url["url"] not in ["", None, []]:
-                    website_list.append(url["url"])
-            return website_list
-
-        if urls() not in ["", None, []]:
-            profile["links"] = urls()
-        #Recruitee profile format is in the following format dict(candidate = profile, offers = [offer_id1,...])
-        output_data = dict(candidate = profile)
-        if self.offer_id is not None:
-            output_data["offers"] = self.offer_id
-
-        return output_data
diff --git a/src/hrflow_connectors/connectors/destinations/recruitee/schemas.py b/src/hrflow_connectors/connectors/destinations/recruitee/schemas.py
deleted file mode 100644
index 8b96ee260..000000000
--- a/src/hrflow_connectors/connectors/destinations/recruitee/schemas.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from typing import List, Optional
-
-from pydantic import BaseModel
-
-
-class RecruiteeProfile(BaseModel):
-    name: str
-    emails: Optional[List[str]]
-    phones: Optional[List[str]]
-    social_links: Optional[List[str]]
-    links: Optional[List[str]]
-    cover_letter: Optional[str]
-
-
-class RecruiteeCandidate(BaseModel):
-    candidate: RecruiteeProfile
-    offers: Optional[List[int]]
diff --git a/src/hrflow_connectors/connectors/destinations/recruitee/spec.py b/src/hrflow_connectors/connectors/destinations/recruitee/spec.py
deleted file mode 100644
index afbaea8a0..000000000
--- a/src/hrflow_connectors/connectors/destinations/recruitee/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PushProfile
-
-
-class Spec(BaseModel):
-    actions: Tuple[PushProfile]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/README.md b/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/README.md
deleted file mode 100644
index c5622327d..000000000
--- a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/README.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# SAP SuccessFactors Connector
-**SAP SuccessFactors is an ATS.**
-
-`Hrflow.ai` :arrow_right: `SAP SuccessFactors`
-
-## PushProfile
-`PushProfile` pushes a `Profile` from a ***HrFlow Source*** to a ***SAP SuccessFactors*** Jobs pool..
-
-## SCHEMAS
-add `schemas.py` as ***basemodel*** for a ***SAP SuccessFactors candidate object***.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `Union[XAPIKeyAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
-| `api_server` :red_circle: | `str` | api_server: the `api_server` in `https://{api-server}/odata/v2`. For example api_server=`apisalesdemo8.successfactors.com` in `https://apisalesdemo8.successfactors.com/odata/v2`        |
-
-:red_circle: : *required* 
-
-### Example
-
-```python
-from hrflow import Hrflow
-
-from hrflow_connectors.connectors.destinations.sapsuccessfactors import PushProfile
-from hrflow_connectors.connectors.core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from hrflow_connectors.connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-
-        auth = XAPIKeyAuth(
-            name = "APIKey",
-        value= settings['MY_API_KEY'],
-    )
-
-        action = PushProfile(
-            auth=auth,
-            api_server="MY_API_SERVER",
-            hrflow_client=client,
-            profile=profile,
-        )
-        response = action.execute()
-        return response
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/__init__.py b/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/actions.py b/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/actions.py
deleted file mode 100644
index 2af8ada19..000000000
--- a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/actions.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from pydantic import Field
-from typing import Dict, Any, Union
-import dateutil.parser as dp
-from ....utils.logger import get_logger
-
-logger = get_logger()
-
-
-class PushProfile(ProfileDestinationAction, HTTPStream):
-
-    auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]
-    payload: Dict[str, Any] = dict()
-    api_server: str = Field(
-        ...,
-        description="the API server for your company from the list of API servers for SAP SuccessFactors data centers",
-    )
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-        self.headers["Accept"] = "application/json"
-
-    @property
-    def base_url(self):
-        return "https://{}/odata/v2/Candidate".format(self.api_server)
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def format(self, profile: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        formats a hrflow profile into a sap successfactors candidate
-
-        Returns:
-            Dict[str, Any]: a json respresentation of a SAP successfactors candidate
-        """
-
-        candidate = dict()
-        info = profile.get("info")
-
-        candidate["address"] = info.get("location").get("text")
-        candidate["cellPhone"] = info.get("phone")
-        fields = info.get("location").get("fields")
-        if fields not in [None, []]:
-            candidate["country"] = fields.get("country")[:-1]
-            candidate["city"] = fields.get("city")
-            candidate["zip"] = fields.get("postcode")
-
-        candidate["primaryEmail"] = info.get("email")
-        candidate["firstName"] = info.get("first_name")
-        candidate["lastName"] = info.get("last_name")
-        candidate["currentTitle"] = info.get("summary")
-
-        def format_start_date(date):
-            return "/Date({})/".format(int(dp.parse(date).timestamp()))
-
-        def format_end_date(date):
-            # add 10 seconds to avoid that end_date is less or equal than start_date
-            return "/Date({})/".format(int(dp.parse(date).timestamp() + 10))
-
-        if profile.get("educations") is not None:
-
-            def format_education(education):
-                result = dict()
-
-                if (
-                    education.get("date_end") is not None
-                    and education.get("date_start") is not None
-                ):
-                    result["endDate"] = format_end_date(education.get("date_end"))
-                    result["startDate"] = format_start_date(education.get("date_start"))
-
-                result["school"] = education.get("school")
-                result["schoolAddress"] = education.get("location").get("text")
-                if result["schoolAddress"] is None:
-                    result["schoolAddress"] = "Undefined"
-                return result
-
-            candidate["education"] = dict(results=[])
-            for education in profile.get("educations"):
-                candidate["education"]["results"].append(format_education(education))
-
-        if profile.get("experiences") is not None:
-
-            def format_experience(experience):
-                result = dict()
-                result["employer"] = experience.get("company")
-                result["employerAddress"] = experience.get("location").get("text")
-                if result["employerAddress"] is None:
-                    result["employerAddress"] = "Undefined"
-                if (
-                    experience.get("date_end") is not None
-                    and experience.get("date_start") is not None
-                ):
-                    result["endDate"] = format_end_date(experience.get("date_end"))
-                    result["startDate"] = format_start_date(
-                        experience.get("date_start")
-                    )
-                return result
-
-            candidate["outsideWorkExperience"] = dict(results=[])
-            for experience in profile.get("experiences"):
-                candidate["outsideWorkExperience"]["results"].append(
-                    format_experience(experience)
-                )
-
-        return candidate
-
-    def push(self, data: Dict[str, Any]):
-        """
-        Push profile
-        Args:
-            data (Dict[str, Any]): Profile
-        """
-        profile_already_exist = b'{\n"error" : {\n"code" : "COE_GENERAL_SERVER_FAILURE", "message" : {\n"lang" : "en-US", "value" : "[COE0019]Couldn\'t create candidate due to return value:Candidate already exists with the index 0"\n}\n}\n}'
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        logger.debug(f"{response.status_code}, {response.content}")
-        if response.status_code >= 400:
-            if response.content == profile_already_exist:
-                logger.warning(f"Candidate already exists")
-            else:
-                raise RuntimeError(
-                    "Push profile to sapsuccesfactors api-server: {} failed : `{}`".format(
-                        self.api_server, response.content
-                    )
-                )
diff --git a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/spec.py b/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/spec.py
deleted file mode 100644
index afbaea8a0..000000000
--- a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PushProfile
-
-
-class Spec(BaseModel):
-    actions: Tuple[PushProfile]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/smartrecruiters/README.md b/src/hrflow_connectors/connectors/destinations/smartrecruiters/README.md
deleted file mode 100644
index ac91ec08e..000000000
--- a/src/hrflow_connectors/connectors/destinations/smartrecruiters/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Smart Recruiters Connector
-**Smart Recruiters is an ATS.**
-
-`Hrflow.ai` :arrow_right: `Smart Recruiters`
-
-## PushProfile
-`PushProfile` pushes a HrFlow.ai profile from a ***Hrflow.ai Source*** to `SmartRecruiters` via the ***SmartRecruiter*** API.
-
-### Parameters
-
-| Field | Type | Description |
-| ----- | ---- | ----------- |
-| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `XSmartTokenAuth` | Auth instance to identify and communicate with the platform       |
-| `job_id` :red_circle: | `str` | Id of a Job to which you want to assign a candidate when it’s created. For example job_id=`78d3ef91-8868-4ff3-b35d-9debf9d6f31f` in `https://api.smartrecruiters.com/jobs/78d3ef91-8868-4ff3-b35d-9debf9d6f31f/candidates`        |
-
-: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 import Hrflow
-
-from hrflow_connectors.core.auth import XSmartTokenAuth
-from hrflow_connectors.connectors.destinations.smartrecruiters import PushProfile
-from hrflow_connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-        auth = XSmartTokenAuth(value=settings["MY_SMART_TOKEN"])
-
-        action = PushProfile(
-            auth=auth,
-            job_id="3696cad0-a9b0-4a40-9cd7-4cc5feb1a509",
-            hrflow_client=client,
-            profile=profile,
-        )
-        response = action.execute()
-        return response
-```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/smartrecruiters/__init__.py b/src/hrflow_connectors/connectors/destinations/smartrecruiters/__init__.py
deleted file mode 100644
index c579c8f39..000000000
--- a/src/hrflow_connectors/connectors/destinations/smartrecruiters/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .actions import *
-from .spec import *
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/smartrecruiters/actions.py b/src/hrflow_connectors/connectors/destinations/smartrecruiters/actions.py
deleted file mode 100644
index 13893c5f6..000000000
--- a/src/hrflow_connectors/connectors/destinations/smartrecruiters/actions.py
+++ /dev/null
@@ -1,165 +0,0 @@
-from pydantic import Field
-from typing import Dict, Any
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....core.auth import XSmartTokenAuth
-from ....utils.hrflow import generate_workflow_response
-
-
-class PushProfile(ProfileDestinationAction, HTTPStream):
-    payload: Dict[str, Any] = dict()
-    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` ",
-    )
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-
-    @property
-    def base_url(self):
-        return "https://api.smartrecruiters.com/jobs/{}/candidates".format(self.job_id)
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def format(self, profile: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        Format a profile hrflow object to a smartrecruiters profile object
-
-        Args:
-            profile (Dict[str, Any]): [profile object in the hrflow profile format]
-
-        Returns:
-            Dict[str, Any]: [profile in the SmartRecruiters candidate application format]
-        """
-
-        value_or_undefined = lambda s: s or "Undefined"
-
-        def format_project(project):
-            formated_project_dict = dict()
-            # current
-            formated_project_dict["current"] = False
-            # start date
-            start_datetime_str = project.get("date_start")
-            if start_datetime_str is None:
-                # datetime is either in format YYYY or YYYY-MM...
-                # if there is none we force this value to avoid conflict with smartrecruiters profile object
-                start_date = "XXXX"
-            else:
-                start_date = start_datetime_str.split("T")[0]
-
-            formated_project_dict["startDate"] = start_date
-
-            end_datetime_str = project.get("date_end")
-            if end_datetime_str is None:
-                end_date = "XXXX"
-            else:
-                end_date = end_datetime_str.split("T")[0]
-
-            formated_project_dict["endDate"] = end_date
-            formated_project_dict["location"] = value_or_undefined(
-                project["location"]["text"]
-            )
-            formated_project_dict["description"] = project["description"]
-
-            return formated_project_dict
-
-        def format_educations(educations):
-            formated_education_list = []
-
-            for education_entity in educations:
-                formated_education = format_project(education_entity)
-                if education_entity.get("school") is None:
-                    formated_education["instituion"] = "Undefined"
-                else:
-                    formated_education["institution"] = education_entity.get("school")
-
-                if education_entity.get("title") is None:
-                    formated_education["degree"] = "Undefined"
-                else:
-                    formated_education["degree"] = education_entity.get("title")
-                formated_education["major"] = "Undefined"
-
-                formated_education_list.append(formated_education)
-
-            return formated_education_list
-
-        def format_experiences(experiences):
-            formated_experience_list = []
-
-            for exp in experiences:
-                formated_exp = format_project(exp)
-                if exp["title"] is None:
-                    formated_exp["title"] = "Undefined"
-                else:
-                    formated_exp["title"] = exp["title"]
-                if exp["company"] is None:
-                    formated_exp["company"] = "Undefined"
-                else:
-                    formated_exp["company"] = exp["company"]
-
-                formated_experience_list.append(formated_exp)
-
-            return formated_experience_list
-
-        info = profile["info"]
-        smart_candidate = dict()
-        smart_candidate["firstName"] = info["first_name"]
-        smart_candidate["lastName"] = info["last_name"]
-        smart_candidate["email"] = info["email"]
-        smart_candidate["phoneNumber"] = info["phone"]
-
-        if info["location"]["fields"] not in [
-            [],
-            None,
-        ]:  # check if fields is not an undefined list
-            smart_candidate["location"] = dict(
-                city=value_or_undefined(info["location"]["fields"].get("city", {})),
-                country=value_or_undefined(
-                    info["location"]["fields"].get("country", {})
-                ),
-                region=value_or_undefined(info["location"]["fields"].get("state", {})),
-                lat=info["location"]["lat"]
-                if info["location"]["lat"] is not None
-                else 0,
-                lng=info["location"]["lng"]
-                if info["location"]["lng"] is not None
-                else 0,
-            )
-        else:
-            smart_candidate["location"] = dict(
-                country="Undefined",
-                region="Undefined",
-                City="Undefined",
-                lat=0,
-                lng=0,
-            )
-
-        smart_candidate["web"] = dict(info["urls"])
-        smart_candidate["tags"] = []
-        smart_candidate["education"] = format_educations(profile.get("educations"))
-        smart_candidate["experience"] = format_experiences(profile.get("experiences"))
-        smart_candidate["attachments"] = profile.get("attachments")
-        smart_candidate["consent"] = True
-
-        return smart_candidate
-
-    def push(self, data: Dict[str, Any]):
-        """
-        Push profile
-
-        Args:
-            data (Dict[str, Any]): Profile
-        """
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to SmartRecruiters failed : `{}`".format(response.content)
-            )
diff --git a/src/hrflow_connectors/connectors/destinations/smartrecruiters/spec.py b/src/hrflow_connectors/connectors/destinations/smartrecruiters/spec.py
deleted file mode 100644
index afbaea8a0..000000000
--- a/src/hrflow_connectors/connectors/destinations/smartrecruiters/spec.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from pydantic import BaseModel
-from typing import Tuple
-
-from .actions import PushProfile
-
-
-class Spec(BaseModel):
-    actions: Tuple[PushProfile]
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/flatchr/README.md b/src/hrflow_connectors/connectors/flatchr/README.md
new file mode 100644
index 000000000..550dda039
--- /dev/null
+++ b/src/hrflow_connectors/connectors/flatchr/README.md
@@ -0,0 +1,8 @@
+# Flatchr Connector
+> Flatchr is a software company that offers recruitment and application management software for human resources.
+
+🔗 https://www.flatchr.io/
+
+| Actions |
+| ------- |
+| [**Push profile**](docs/push_profile.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/flatchr/__init__.py b/src/hrflow_connectors/connectors/flatchr/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/flatchr/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/destinations/flatchr/actions.py b/src/hrflow_connectors/connectors/flatchr/actions.py
similarity index 73%
rename from src/hrflow_connectors/connectors/destinations/flatchr/actions.py
rename to src/hrflow_connectors/connectors/flatchr/actions.py
index 5b24f8376..5326eb9c7 100644
--- a/src/hrflow_connectors/connectors/destinations/flatchr/actions.py
+++ b/src/hrflow_connectors/connectors/flatchr/actions.py
@@ -1,51 +1,34 @@
-from ....core.auth import AuthorizationAuth
-from ....core.action import ProfileDestinationAction
-from ....core.http import HTTPStream
-from ....utils.hrflow import generate_workflow_response
-
 from pydantic import Field
 from typing import Dict, Any, Iterator
 import base64
 import requests
 
+from ...core.auth import AuthorizationAuth
+
+from ...core.action import PushProfileBaseAction
+from ...utils.hrflow import generate_workflow_response
+from ...utils.logger import get_logger
+
+logger = get_logger()
 
-class PushProfile(ProfileDestinationAction, HTTPStream):
-    payload: Dict[str, Any] = dict()
+
+class PushProfileAction(PushProfileBaseAction):
     auth: AuthorizationAuth
-    subdomain: str = Field(
-        ...,
-        description="Subdomain flatchr just before `flatchr.io`. For example subdomain=`my_subdomain.my` in "
-        "`http://my_subdomain.my.flatchr.io/ABC`",
-    )
     vacancy: str = Field(
         ...,
         description="The pool in which candidates will be placed. Findable in the URL",
     )
+    company: str = Field(
+        ...,
+        description="The id of the company",
+    )
 
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-        self.headers["Accept"] = "*/*"
-
-    @property
-    def base_url(self):
-        return "https://{}.flatchr.io/vacancy/candidate/json".format(self.subdomain)
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
 
-    @property
-    def http_method(self):
-        return "POST"
+        ######################
+        ### Create Profile ###
+        ######################
 
-    def push(self, data):
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to flatchr failed : `{}`".format(response.content)
-            )
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         def get_candidate_attachments(hrflow_profile):
             attachments_list = hrflow_profile.get("attachments")
             # We try to find 'resume' (which is 'original' after traitments).
@@ -69,7 +52,7 @@ def get_candidate_attachments(hrflow_profile):
                 f"No email for hrflow_profile {data.get('reference')} but one is mandatory"
             )
 
-        profile = {
+        create_profile_body = {
             "vacancy": self.vacancy,
             "firstname": info.get("first_name")
             if info.get("first_name") is not None
@@ -85,51 +68,11 @@ def get_candidate_attachments(hrflow_profile):
                 "data": get_candidate_attachments(data),
             },
         }
-        return profile
 
+        ######################
+        ### Enrich Profile ###
+        ######################
 
-class EnrichProfile(ProfileDestinationAction, HTTPStream):
-    payload: Dict[str, Any] = dict()
-    auth: AuthorizationAuth
-    subdomain: str = Field(
-        ...,
-        description="Subdomain flatchr just before `flatchr.io`. For example subdomain=`my_subdomain.my` in "
-        "`http://my_subdomain.my.flatchr.io/ABC`",
-    )
-    vacancy: str = Field(
-        ...,
-        description="The pool in which candidates will be placed. Findable in the URL",
-    )
-    compagny: str = Field(
-        ...,
-        description="The id of the compagny",
-    )
-
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "application/json"
-
-    @property
-    def base_url(self):
-        return "https://{0}.flatchr.io/company/{1}/search/candidate".format(
-            self.subdomain, self.compagny
-        )
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def push(self, data):
-        self.payload.clear()
-        profile = next(data)
-        self.payload.update(profile)
-        response = self.send_request()
-        if response.status_code >= 400:
-            raise RuntimeError(
-                "Push profile to flatchr failed : `{}`".format(response.content)
-            )
-
-    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         def get_education_list(hrflow_profile):
             hrflow_education_list = hrflow_profile["educations"]
             flatchr_education_list = []
@@ -206,7 +149,11 @@ def get_experiences_list(hrflow_profile):
             return flatchr_experiences_list
 
         def get_name():
-            name = {"formattedName": "Undefined", "given": "Undefined", "family": "Undefined"}
+            name = {
+                "formattedName": "Undefined",
+                "given": "Undefined",
+                "family": "Undefined",
+            }
             return name
 
         def get_phone(hrflow_profile):
@@ -261,7 +208,7 @@ def get_employment_positions(hrflow_profile):
             "experience": int(data.get("experiences_duration")),
         }
 
-        profile = {
+        enrich_profile_body = {
             "app_name": "HRMatch",
             "reference": data["info"].get("email"),
             "name": "parsing",
@@ -269,4 +216,51 @@ def get_employment_positions(hrflow_profile):
             "value": value,
         }
 
-        return 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,
+        )
+        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"]
+
+        session = requests.Session()
+
+        # Prepare create request
+        logger.info("Preparing `create profile` request")
+        create_profile_request = requests.Request()
+        create_profile_request.method = "POST"
+        create_profile_request.url = "http://careers.flatchr.io/vacancy/candidate/json"
+
+        create_profile_request.auth = self.auth
+        create_profile_request.json = create_profile_body
+        prepared_create_profile_request = create_profile_request.prepare()
+
+        # Prepare enrich request
+        logger.info("Preparing `enrich profile` request")
+        enrich_profile_request = requests.Request()
+        enrich_profile_request.method = "POST"
+        enrich_profile_request.url = (
+            f"http://api.flatchr.io/company/{self.company}/search/candidate"
+        )
+        enrich_profile_request.auth = self.auth
+        enrich_profile_request.json = enrich_profile_body
+        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}`"
+            )
+
+        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}`")
diff --git a/src/hrflow_connectors/connectors/flatchr/connector.py b/src/hrflow_connectors/connectors/flatchr/connector.py
new file mode 100644
index 000000000..822546faa
--- /dev/null
+++ b/src/hrflow_connectors/connectors/flatchr/connector.py
@@ -0,0 +1,47 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import AuthorizationAuth
+from ...utils.hrflow import Profile
+from .actions import PushProfileAction
+
+
+class Flatchr(Connector):
+    @staticmethod
+    def push_profile(
+        auth: AuthorizationAuth,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        vacancy: str,
+        company: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a Hrflow.ai profile to `Flatchr` via their API.
+
+        `Hrflow.ai` -> `Flatchr`
+
+        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
+            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 `[]`
+            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,
+            vacancy=vacancy,
+            company=company,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md b/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md
new file mode 100644
index 000000000..c81fb1afe
--- /dev/null
+++ b/src/hrflow_connectors/connectors/flatchr/docs/push_profile.md
@@ -0,0 +1,47 @@
+# Push profile
+`Hrflow.ai` :arrow_right: `Flatchr`
+
+`PushProfileAction` pushes a Hrflow.ai profile to `Flatchr` via their API.
+
+**Links to Flatchr documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Push profile](https://developers.flatchr.io/?python#formulaire-de-candidature) | Push a candidate with minimum information + a CV, the request method is `POST` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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        |
+| `vacancy` :red_circle: | `str` | The pool in which candidates will be placed. Findable in the URL        |
+| `compagny` :red_circle: | `str` | The id of the compagny        |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Flatchr
+from hrflow import Hrflow
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+client = Hrflow(api_secret="X-API-KEY", api_user="X-USER-EMAIL")
+auth = AuthorizationAuth(value="FLATCHR_KEY")
+Flatchr.push_profile(
+    auth=auth,
+    hrflow_client=client,
+    profile=profile,
+    vacancy="k0M5O9ylKZnxbQBy",
+    company="LEZBvp5b4LdMoVmg",
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/flatchr/schemas.py b/src/hrflow_connectors/connectors/flatchr/schemas.py
similarity index 66%
rename from src/hrflow_connectors/connectors/destinations/flatchr/schemas.py
rename to src/hrflow_connectors/connectors/flatchr/schemas.py
index 602b5e6dc..9a14f75ba 100644
--- a/src/hrflow_connectors/connectors/destinations/flatchr/schemas.py
+++ b/src/hrflow_connectors/connectors/flatchr/schemas.py
@@ -18,31 +18,51 @@ class FlatchrCreationProfile(BaseModel):
     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")
-    resume: FlatchrResume = Field(..., Description="Type of format for the CV: document (if cv base64, default), "
-                                                   "json (if cv, hr-xml format)")
-    email: str = Field(..., Description="Email will be used as a reference for the profile. The enrichment request "
-                                        "aim the right profile to enrich with the email.")
+    token: str = Field(
+        ..., Description="API key. It is created in the Flatchr interface"
+    )
+    resume: FlatchrResume = Field(
+        ...,
+        Description="Type of format for the CV: document (if cv base64, default), "
+        "json (if cv, hr-xml format)",
+    )
+    email: str = Field(
+        ...,
+        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")
+    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")
+    answers: Optional[FlatchrAnswers] = Field(
+        ..., Description="Answers to questions/tags"
+    )
 
 
 class FlatchrAdressProfile(BaseModel):
-    formattedAddress: str = Field(..., Description="Adress where the education were done")
+    formattedAddress: str = Field(
+        ..., Description="Adress where the education were done"
+    )
 
 
 class FlatchrCommunicationProfile(BaseModel):
-    address: List[FlatchrAdressProfile] = Field(..., Description="List of adresses where the education were done")
+    address: List[FlatchrAdressProfile] = Field(
+        ..., Description="List of adresses where the education were done"
+    )
 
 
 class FlatchrInstitutionProfile(BaseModel):
-    communication: FlatchrCommunicationProfile = Field(..., Description="List of adresses")
-    name: str = Field(..., Description="Name of the institution in which the education was provided")
+    communication: FlatchrCommunicationProfile = Field(
+        ..., Description="List of adresses"
+    )
+    name: str = Field(
+        ..., Description="Name of the institution in which the education was provided"
+    )
 
 
 class FlatchrEducationLevelCode(BaseModel):
@@ -56,9 +76,15 @@ class FlatchrEducationDegree(BaseModel):
 
 
 class FlatchrEducationProfile(BaseModel):
-    institution: FlatchrInstitutionProfile = Field(..., Description="Institution object")
-    educationLevelCodes: List[FlatchrEducationLevelCode] = Field(..., Description="List of all the degrees")
-    educationDegrees: List[FlatchrEducationDegree] = Field(..., Description="List of all the degrees")
+    institution: FlatchrInstitutionProfile = Field(
+        ..., Description="Institution object"
+    )
+    educationLevelCodes: List[FlatchrEducationLevelCode] = Field(
+        ..., Description="List of all the degrees"
+    )
+    educationDegrees: List[FlatchrEducationDegree] = Field(
+        ..., Description="List of all the degrees"
+    )
     end: str = Field(..., Description="Date on which the degree has ended")
     descriptions: List[str] = Field(..., Description="Description of the education")
 
@@ -72,7 +98,9 @@ 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")
-    geoLocation: FlatchrGeoLocation = Field(..., Description="GeoLocation object of the employment location")
+    geoLocation: FlatchrGeoLocation = Field(
+        ..., Description="GeoLocation object of the employment location"
+    )
     formattedAddress: str = Field(..., Description="Address of the employment location")
 
 
@@ -81,7 +109,9 @@ class FlatchrCommunication1(BaseModel):
 
 
 class FlatchrOrganization(BaseModel):
-    communication: FlatchrCommunication1 = Field(..., Description="Communication1 object")
+    communication: FlatchrCommunication1 = Field(
+        ..., Description="Communication1 object"
+    )
     name: str = Field(..., Description="Address of the employment")
 
 
@@ -91,7 +121,9 @@ class FlatchrJobCategory(BaseModel):
 
 class FlatchrPositionHistory(BaseModel):
     organization: FlatchrOrganization = Field(..., Description="Organization object")
-    jobCategories: List[FlatchrJobCategory] = Field(..., Description="Category of the job")
+    jobCategories: List[FlatchrJobCategory] = Field(
+        ..., 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")
@@ -99,7 +131,9 @@ class FlatchrPositionHistory(BaseModel):
 
 class FlatchrEmploymentProfile(BaseModel):
     title: str = Field(..., Description="Title of the employment")
-    positionHistories: List[FlatchrPositionHistory] = Field(..., Description="List of the position histories object")
+    positionHistories: List[FlatchrPositionHistory] = Field(
+        ..., Description="List of the position histories object"
+    )
     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")
@@ -121,19 +155,29 @@ class FlatchrEmailProfile(BaseModel):
 
 
 class FlatchrValueProfile(BaseModel):
-    education: List[FlatchrEducationProfile] = Field(..., Description="List of educations")
-    employment: List[FlatchrEmploymentProfile] = Field(..., Description="List of employments")
+    education: List[FlatchrEducationProfile] = Field(
+        ..., Description="List of educations"
+    )
+    employment: List[FlatchrEmploymentProfile] = Field(
+        ..., Description="List of employments"
+    )
     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")
+    email: List[FlatchrEmailProfile] = Field(
+        ..., Description="Email adress 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: List[str] = Field(
+        ..., Description="Employment adress 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")
+    reference: str = Field(
+        ..., Description="Reference of the profile which is the email adress"
+    )
     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/README.md b/src/hrflow_connectors/connectors/greenhouse/README.md
new file mode 100644
index 000000000..3f288abb0
--- /dev/null
+++ b/src/hrflow_connectors/connectors/greenhouse/README.md
@@ -0,0 +1,9 @@
+# Greenhouse Connector
+> Greenhouse's applicant tracking system and recruiting software is designed to help make companies great at hiring and hire for what's next.
+
+🔗 https://www.greenhouse.io/
+
+| 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/greenhouse/__init__.py b/src/hrflow_connectors/connectors/greenhouse/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/greenhouse/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/greenhouse/actions.py b/src/hrflow_connectors/connectors/greenhouse/actions.py
new file mode 100644
index 000000000..11a5ac836
--- /dev/null
+++ b/src/hrflow_connectors/connectors/greenhouse/actions.py
@@ -0,0 +1,229 @@
+from typing import Iterator, Dict, Any, Union, List
+from pydantic import Field
+import html
+import requests
+
+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
+
+logger = get_logger()
+
+
+class PullJobsAction(PullJobsBaseAction):
+    board_token: str = Field(
+        ...,
+        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]]:
+        """
+        Pull all jobs from a greenhouse job board
+
+        Returns:
+            Iterator[Dict[str, Any]]: 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 = f"https://boards-api.greenhouse.io/v1/boards/{self.board_token}/jobs/?content=true"
+        pull_jobs_request.auth = self.auth
+        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 board: `{self.board_token}`. Check that your board token is valid."
+            )
+            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
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        format each job pulled from greenhouse job board into a HrFlow job object
+
+        Returns:
+            Dict[str, Any]: job in the HrFlow job object format
+        """
+
+        job = dict()
+        # name
+        job["name"] = data.get("title")
+        # summary
+        job["summary"] = None
+        # reference
+        job["reference"] = str(data.get("id"))
+        # url
+        job["url"] = data.get("absolute_url")
+        # location
+        location = data.get("location").get("name")
+        job["location"] = dict(text=location, lat=None, lng=None)
+        # sections
+        description_content = data.get("content")
+        # convert the escaped description content into html format
+        description_html = html.unescape(description_content)
+        # remove html tags to get clean text
+        text = remove_html_tags(description_html)
+
+        job["sections"] = [
+            dict(
+                name="greenhouse_description",
+                title="greenhouse_description",
+                description=text,
+            )
+        ]
+        # metadata
+        job["metadatas"] = data.get("metadata")
+        # tags
+        department = data.get("departments")
+        if department not in [None, []]:
+            department_name = department[0].get("name")
+            department_id = str(department[0].get("id"))
+        else:
+            department_name = "Undefined"
+            department_id = "Undefined"
+
+        office = data.get("offices")
+        if office not in [None, []]:
+            office_name = office[0].get("name")
+            office_id = str(office[0].get("id"))
+        else:
+            office_name = "Undefined"
+            office_id = "Undefined"
+
+        education = data.get("education")
+        employment = data.get("employment")
+
+        job["tags"] = [
+            dict(name="greenhouse_department-name", value=department_name),
+            dict(name="greenhouse_department-id", value=department_id),
+            dict(name="greenhouse_office-location", value=office_name),
+            dict(name="greenhouse_office-id", value=office_id),
+            dict(name="greenhouse_education", value=education),
+            dict(name="greenhouse_employment", value=employment),
+        ]
+        # updated_at
+        job["updated_at"] = data.get("updated_at")
+
+        return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]
+    job_id: List[int] = Field(
+        ...,
+        description="List of jobs internal ids to which the candidate should be added",
+    )
+    on_behalf_of: str = Field(
+        ...,
+        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]:
+        """
+        Format a profile hrflow object to a greenhouse profile object
+        Args:
+            profile (Dict[str, Any]): profile object in the hrflow profile format
+        Returns:
+            Dict[str, Any]: profile in the greenhouse candidate  format
+        """
+        profile = dict()
+        profile["applications"] = []
+        for id in self.job_id:
+            profile["applications"].append(dict(job_id=id))
+
+        if self.job_id is not None:
+            profile["job_id"] = self.job_id
+
+        profile["first_name"] = data.get("info").get("first_name")
+        profile["last_name"] = data.get("info").get("last_name")
+        profile["external_id"] = data.get("reference")
+
+        if data.get("attachments") not in [[], None]:
+            profile["resume"] = data.get("attachments")[0]["public_url"]
+
+        phone_number = data.get("info").get("phone")
+        profile["phone_numbers"] = [dict(value=phone_number, type="mobile")]
+
+        email = data.get("info").get("email")
+        profile["email_addresses"] = [dict(value=email, type="personal")]
+
+        address = data.get("info").get("location").get("text")
+        profile["addresses"] = [dict(value=address, type="home")]
+
+        profile["notes"] = data.get("text")
+
+        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"]))
+            return website_list
+
+        if get_social_media_urls() not in [[], None]:
+            profile["social_media_addresses"] = get_social_media_urls()
+
+        if data["experiences"] not in [[], None]:
+            last_experience = data["experiences"][0]
+            profile["company"] = last_experience["company"]
+            profile["title"] = last_experience["title"]
+            profile["employments"] = []
+            for experience in data["experiences"]:
+                if (
+                    experience["title"]
+                    and experience["company"]
+                    and experience["date_start"]
+                ) not in ["", None]:
+                    profile["employments"].append(
+                        dict(
+                            company_name=experience["company"],
+                            title=experience["title"],
+                            start_date=experience["date_start"],
+                            end_date=experience["date_end"],
+                        )
+                    )
+
+        return profile
+
+    def push(self, data: Dict[str, Any]):
+        """
+        Push profile
+        Args:
+            data (Dict[str, Any]): Profile
+        """
+
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        push_profile_request.method = "POST"
+        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
+        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
+                )
+            )
diff --git a/src/hrflow_connectors/connectors/greenhouse/connector.py b/src/hrflow_connectors/connectors/greenhouse/connector.py
new file mode 100644
index 000000000..73ccc03a5
--- /dev/null
+++ b/src/hrflow_connectors/connectors/greenhouse/connector.py
@@ -0,0 +1,79 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any, Union, List
+
+from ...core.connector import Connector
+from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
+
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Greenhouse(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow, board_key: str, board_token: str, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs listed on Greenhouse board. It adds all these **jobs** to a Hrflow.ai Board.
+
+        `Greenhouse` -> `Hrflow.ai`
+
+        Args:
+            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 `[]`
+            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(
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            board_token=board_token,
+            **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth],
+        job_id: List[int],
+        on_behalf_of: str,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a `Profile` from a HrFlow Source to a Greenhouse Jobs pool.
+
+        `Hrflow.ai` -> `Greenhouse`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            auth (Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]): Auth instance to identify and communicate with the platform
+            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 `[]`
+            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,
+            job_id=job_id,
+            on_behalf_of=on_behalf_of,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/greenhouse/README.md b/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
similarity index 74%
rename from src/hrflow_connectors/connectors/boards/greenhouse/README.md
rename to src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
index e41dd487b..1b586cfe8 100644
--- a/src/hrflow_connectors/connectors/boards/greenhouse/README.md
+++ b/src/hrflow_connectors/connectors/greenhouse/docs/pull_jobs.md
@@ -1,12 +1,15 @@
-# Greenhouse Connector
-**Greenhouse is an ATS.**
-
+# Pull jobs
 `Greenhouse` :arrow_right: `Hrflow.ai`
 
-## GetAllJobs
-`GetAllJobs` gets all available jobs listed on ***Greenhouse board***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs listed on ***Greenhouse board***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+
+**Links to Greenhouse documentation on the endpoints used :**
 
-### Parameters
+| Endpoints | Description |
+| --------- | ----------- |
+| [Job Board](https://developers.greenhouse.io/job-board.html) | Endpoint to retrieve all jobs from a greenhouse job board which is publicly available, only required parameter is a board token or board name, the request method is `GET`|
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -22,25 +25,18 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Greenhouse
 from hrflow import Hrflow
 
-from hrflow_connectors.connectors.boards.greenhouse import GetAllJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
-
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
 
-action = GetAllJobs(
-    board_token="MY_BOARD_TOKEN",
+Greenhouse.pull_jobs(
     hrflow_client=client,
+    board_token="MY_BOARD_TOKEN",
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md b/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md
new file mode 100644
index 000000000..0645fd999
--- /dev/null
+++ b/src/hrflow_connectors/connectors/greenhouse/docs/push_profile.md
@@ -0,0 +1,53 @@
+# Push profile
+
+`Hrflow.ai` :arrow_right: `Greenhouse`
+
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***Greenhouse*** Jobs pool.
+
+**Links to Greenhouse documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Push profile](https://developers.greenhouse.io/harvest.html#post-add-candidate) | Endpoint to add candidate using greenhouse harvest API, the request method is `POST` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `Union[AuthorizationAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
+| `job_id` :red_circle: | `List[int]` | List of jobs internal ids to which the candidate should be added |
+| `on_behalf_of` :red_circle: | `str` | The ID of the user sending the profile, or the person he is sending the profile on behalf of |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Greenhouse
+from hrflow import Hrflow
+from hrflow_connectors.connectors import OAuth2PasswordCredentialsBody, AuthorizationAuth
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = AuthorizationAuth(
+    name = 'Authorization',
+    value= 'MY_AUTHORIZATION',
+)
+
+response = Greenhouse.push_profile(
+    auth=auth,
+    job_id="JOB_ID",
+    on_behalf_of="ON_BEHALF_OF",
+    hrflow_client=client,
+    profile=profile,
+)
+return response
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/greenhouse/schemas.py b/src/hrflow_connectors/connectors/greenhouse/schemas.py
similarity index 80%
rename from src/hrflow_connectors/connectors/destinations/greenhouse/schemas.py
rename to src/hrflow_connectors/connectors/greenhouse/schemas.py
index afe181e3d..472cd60ea 100644
--- a/src/hrflow_connectors/connectors/destinations/greenhouse/schemas.py
+++ b/src/hrflow_connectors/connectors/greenhouse/schemas.py
@@ -1,7 +1,41 @@
-from typing import Optional, List
+from typing import Optional, List, Any
 from pydantic import BaseModel, Field
 
+# Job Model
+class Location(BaseModel):
+    name: str
 
+
+class Department(BaseModel):
+    id: int
+    name: str
+    parent_id: Any
+    child_ids: Optional[List[int]]
+
+
+class Office(BaseModel):
+    id: int
+    name: str
+    location: str
+    parent_id: Optional[int]
+    child_ids: Optional[List[int]]
+
+
+class GreenhouseJobModel(BaseModel):
+    id: int
+    internal_job_id: int
+    title: str
+    updated_at: Optional[str]
+    requisition_id: Optional[str]
+    location: Location
+    absolute_url: str
+    metadata: Any
+    content: str
+    departments: List[Department]
+    offices: List[Office]
+
+
+# Profile Model
 class PhoneNumber(BaseModel):
     value: str
     type: str
diff --git a/src/hrflow_connectors/connectors/monster/README.md b/src/hrflow_connectors/connectors/monster/README.md
new file mode 100644
index 000000000..3be708370
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/README.md
@@ -0,0 +1,9 @@
+# Monster Connector
+> Monster is your source for jobs and career opportunities. Search for jobs, read career advice from Monster's job experts, and find hiring and recruiting advice.
+
+🔗 https://www.monster.com/
+
+| Actions |
+| ------- |
+| [**Catch profile**](docs/catch_profile.md) |
+| [**Push job**](docs/push_job.md) |
diff --git a/src/hrflow_connectors/connectors/monster/__init__.py b/src/hrflow_connectors/connectors/monster/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/monster/actions.py b/src/hrflow_connectors/connectors/monster/actions.py
similarity index 67%
rename from src/hrflow_connectors/connectors/boards/monster/actions.py
rename to src/hrflow_connectors/connectors/monster/actions.py
index 846ebeee1..fef0a9d9e 100644
--- a/src/hrflow_connectors/connectors/boards/monster/actions.py
+++ b/src/hrflow_connectors/connectors/monster/actions.py
@@ -1,29 +1,40 @@
-from ....core.action import JobDestinationAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
-from ....utils.hrflow import Job
-from ....utils.hrflow import generate_workflow_response
-
+import xml.etree.ElementTree
 from pydantic import Field
-from typing import Dict, Any, Iterator
-import base64
+from typing import Union, Dict, Any
 import requests
 
+from ...core.action import PushJobBaseAction, CatchProfileBaseAction
+from ...utils.logger import get_logger
+
+
 logger = get_logger()
 
 
-class PushJob(JobDestinationAction, HTTPStream):
-    payload: str = ""
+class CatchProfileAction(CatchProfileBaseAction):
+    def format(self, request: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format the input data into a push-ready data schema
+        Args:
+            request (Dict[str, Any]): body we want to adapt to the output format
+        Returns:
+            Dict[str, Any]: parameters to put in the parsing endpoint
+        """
 
-    username: str = Field(
-        ...,
-        description="username which will be format in the XML",
-    )
+        def get_binary_resume(FileContents):
+            byte_array = bytearray(FileContents)
+            binary_resume = bytes(byte_array)
+            return binary_resume
+
+        hrflow_tags = [{"name": "JobRefID", "value": request["JobRefID"]}]
+        output_data = {
+            "source_key": self.source_key,
+            "profile_file": get_binary_resume(self.request["FileContents"]),
+            "tags": hrflow_tags,
+        }
+        return output_data
 
-    password: str = Field(
-        ...,
-        description="password which will be format in the XML",
-    )
+
+class PushJobAction(PushJobBaseAction):
 
     subdomain: str = Field(
         ...,
@@ -31,30 +42,6 @@ class PushJob(JobDestinationAction, HTTPStream):
         "`https://my_subdomain.my.monster.com:8443/bgwBroker`",
     )
 
-    def build_request_headers(self):
-        super().build_request_headers()
-        self.headers["content-type"] = "text/xml"
-
-    @property
-    def base_url(self):
-        return "https://{}.monster.com:8443/bgwBroker".format(self.subdomain)
-
-    @property
-    def http_method(self):
-        return "POST"
-
-    def push(self, data):
-        job = next(data)
-        self.payload = job
-        response = self.send_request()
-        if response.status_code >= 400:
-            logger.info(response.content)
-            logger.info(self.payload)
-            logger.info(type(self.payload))
-            raise RuntimeError(
-                "Push job to monster failed : `{}`".format(response.content)
-            )
-
     def format(self, data: Dict[str, Any]) -> bytes:
 
         xml_job_str = """<?xml version="1.0" encoding="UTF-8"?>
@@ -156,7 +143,9 @@ def format_job_informations(formater, hrflow_job):
 
             JobStatus = find_in_tags(hrflow_job["tags"], "JobStatus")
             if JobStatus != None:
-                formater["JobStatus"] = f'\n        <JobStatus monsterId="{JobStatus}"/>'
+                formater[
+                    "JobStatus"
+                ] = f'\n        <JobStatus monsterId="{JobStatus}"/>'
 
         def format_job_reference(formater, hrflow_job):
             formater["jobRefCode"] = f'"{hrflow_job["key"]}"'
@@ -175,13 +164,19 @@ def format_salary(formater, hrflow_job):
 
             if Salary_range != None:
                 SalaryMin = Salary_range[0]
-                formater["SalaryMin"] = f'\n          <SalaryMin>{SalaryMin}</SalaryMin>'
+                formater[
+                    "SalaryMin"
+                ] = f"\n          <SalaryMin>{SalaryMin}</SalaryMin>"
                 SalaryMax = Salary_range[1]
-                formater["SalaryMax"] = f'\n          <SalaryMax>{SalaryMax}</SalaryMax>'
+                formater[
+                    "SalaryMax"
+                ] = f"\n          <SalaryMax>{SalaryMax}</SalaryMax>"
 
             CompensationType = find_in_tags(hrflow_job.get("tags"), "CompensationType")
             if CompensationType != None:
-                formater["CompensationType"] = f'\n          <CompensationType monsterId="{CompensationType}"/>'
+                formater[
+                    "CompensationType"
+                ] = f'\n          <CompensationType monsterId="{CompensationType}"/>'
 
         def format_location(formater, hrflow_job):
             formater["StreetAddress"] = ""
@@ -205,17 +200,25 @@ def format_autorefresh(formater, hrflow_job):
             formater["Autorefresh"] = ""
             Autorefresh = find_in_tags(hrflow_job.get("tags"), "Autorefresh")
             if Autorefresh != None:
-                formater["Autorefresh"] = """\n            <Autorefresh desired="true">
+                formater[
+                    "Autorefresh"
+                ] = """\n            <Autorefresh desired="true">
                       <Frequency>{frequency}</Frequency>
-                    </Autorefresh>""".format(frequency=Autorefresh)
+                    </Autorefresh>""".format(
+                    frequency=Autorefresh
+                )
 
         def format_careeradnetwork(formater, hrflow_job):
             formater["CareerAdNetwork"] = ""
             CareerAdNetwork = find_in_tags(hrflow_job.get("tags"), "CareerAdNetwork")
             if CareerAdNetwork != None:
-                formater["CareerAdNetwork"] = """\n            <CareerAdNetwork desired="true">
+                formater[
+                    "CareerAdNetwork"
+                ] = """\n            <CareerAdNetwork desired="true">
                       <Duration>{frequency}</Duration>
-                    </CareerAdNetwork>""".format(frequency=CareerAdNetwork)
+                    </CareerAdNetwork>""".format(
+                    frequency=CareerAdNetwork
+                )
 
         def format_jobcategory(formater, hrflow_job):
             formater["JobCategory"] = "11"
@@ -233,29 +236,35 @@ def format_industries(formater, hrflow_job):
             formater["Industry"] = ""
             Industry = find_in_tags(hrflow_job.get("tags"), "Industry")
             if Industry != None:
-                formater["Industry"] = """\n          <Industries>
+                formater[
+                    "Industry"
+                ] = """\n          <Industries>
                     <Industry>
                       <IndustryName monsterId="{IndustryName}"/>
                     </Industry>
-                  </Industries>""".format(IndustryName=Industry)
+                  </Industries>""".format(
+                    IndustryName=Industry
+                )
 
         def format_credentials(formater, hrflow_job):
-            formater["username"] = self.username
-            formater["password"] = self.password
-
-        creation_pipeline = [format_job_informations,
-                             format_created_at,
-                             format_job_reference,
-                             format_salary,
-                             format_location,
-                             format_description,
-                             format_duration,
-                             format_autorefresh,
-                             format_careeradnetwork,
-                             format_jobcategory,
-                             format_joboccupation,
-                             format_credentials,
-                             format_industries]
+            formater["username"] = "{username}"
+            formater["password"] = "{password}"
+
+        creation_pipeline = [
+            format_job_informations,
+            format_created_at,
+            format_job_reference,
+            format_salary,
+            format_location,
+            format_description,
+            format_duration,
+            format_autorefresh,
+            format_careeradnetwork,
+            format_jobcategory,
+            format_joboccupation,
+            format_credentials,
+            format_industries,
+        ]
 
         formater = dict()
 
@@ -263,4 +272,23 @@ def format_credentials(formater, hrflow_job):
             function(formater, data)
 
         job = xml_job_str.format(**formater)
-        return job.encode('utf-8')
+        return job.encode("utf-8")
+
+    def push(self, data):
+        job = next(data)
+
+        # Preparing the request to push the job
+        session = requests.Session()
+        push_job_request = requests.Request()
+        push_job_request.method = "POST"
+        push_job_request.url = f"https://{self.subdomain}.monster.com:8443/bgwBroker"
+        push_job_request.auth = self.auth
+        push_job_request.data = job
+        push_job_request.headers = {"content-type": "text/xml"}
+        prepared_request = push_job_request.prepare()
+
+        # 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))
diff --git a/src/hrflow_connectors/connectors/monster/connector.py b/src/hrflow_connectors/connectors/monster/connector.py
new file mode 100644
index 000000000..caf30ee27
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/connector.py
@@ -0,0 +1,68 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import MonsterBodyAuth
+from ...utils.hrflow import Job
+from .actions import CatchProfileAction, PushJobAction
+
+
+class Monster(Connector):
+    @staticmethod
+    def catch_profile(
+        hrflow_client: Hrflow, source_key: str, request: Dict[str, Any], **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `CatchProfileAction` catches a Monster profile to `Hrflow.ai`.
+
+        `Monster` -> `HrFlow.ai`
+
+        Args:
+            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 `[]`
+            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 = CatchProfileAction(
+            hrflow_client=hrflow_client,
+            source_key=source_key,
+            request=request,
+            **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_job(
+        auth: MonsterBodyAuth, hrflow_client: Hrflow, subdomain: str, job: Job, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushJobAction` pushes a Monster job to `Hrflow.ai`.
+
+        `HrFlow.ai` -> `Monster`
+
+        Args:
+            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 `[]`
+            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 = PushJobAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            subdomain=subdomain,
+            job=job,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/monster/docs/catch_profile.md b/src/hrflow_connectors/connectors/monster/docs/catch_profile.md
new file mode 100644
index 000000000..06b1800a0
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/docs/catch_profile.md
@@ -0,0 +1,38 @@
+# Catch profile
+`Monster` :arrow_right: `HrFlow.ai`
+
+`CatchProfileAction` catches a Monster profile to `Hrflow.ai`.
+
+**Links to Monster documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+|[Catch Profile](https://partner.monster.com/apply-with-monster-implementing)|Endpoint to catch a profile application |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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        |
+| `source_key` :red_circle: | `String` | Source key where the profiles to be added will be stored        |
+| `request` :red_circle: | `Dict[str, Any]` | Body to format in HrFlow Profile        |
+
+
+:red_circle: : *required* 
+
+```python
+from hrflow_connectors import Monster
+from hrflow import Hrflow
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+
+Monster.catch_profile(
+    hrflow_client=client,
+    request=_request,
+    source_key="8df6a1247b1a95e0b84f5226093ff2c58e60cdf1",
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/monster/docs/push_job.md b/src/hrflow_connectors/connectors/monster/docs/push_job.md
new file mode 100644
index 000000000..586f72690
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/docs/push_job.md
@@ -0,0 +1,40 @@
+# Push job
+`HrFlow.ai` :arrow_right: `Monster`
+
+`PushJobAction` pushes a Monster job to `Hrflow.ai`.
+
+**Links to Monster documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+|[Push Jobs](https://partner.monster.com/real-time-posting-devguide)|Endpoint to real time posting of jobs in Monster  |
+
+## Parameters
+
+| Field                        | Type | Description |
+|------------------------------| ---- | ----------- |
+| `auth` :red_circle:          | `MonsterBodyAuth` | Auth instance to identify and communicate with the platform        |
+| `subdomain` :red_circle:     | String | Subdomain monster just before `monster.com`. For example subdomain=`my_subdomain.my` in `https://my_subdomain.my.monster.com:8443/bgwBroker`       |
+| `hrflow_client` :red_circle: | `hrflow.Hrflow` | Hrflow client instance used to communicate with the Hrflow.ai API        |
+| `job` :red_circle:           | `Job` | Job to push        |
+
+
+:red_circle: : *required* 
+
+
+```python
+from hrflow_connectors import Monster
+from hrflow import Hrflow
+from hrflow.utils.hrflow import Job, Board
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+
+job = Job(key="MY_JOB_KEY", board=Board("MY_BOARD_KEY"))
+auth = MonsterBodyAuth(username="MY_USER_NAME",password="MY_PASSWORD")
+
+Monster.catch_profile(
+    auth=auth,
+    subdomain="gateway",
+    hrflow_client=client,
+    job=job,
+)
diff --git a/src/hrflow_connectors/connectors/monster/schemas.py b/src/hrflow_connectors/connectors/monster/schemas.py
new file mode 100644
index 000000000..45c9f10a1
--- /dev/null
+++ b/src/hrflow_connectors/connectors/monster/schemas.py
@@ -0,0 +1,38 @@
+from typing import Optional, Union, List
+from pydantic import BaseModel, Field
+
+
+class MonsterProfile(BaseModel):
+    City: str = Field(..., Description="City of the profile")
+    CountryCode: str = Field(..., Description="CountryCode of the profile")
+    EmailAddress: str = Field(..., Description="Email address of the profile")
+    FileContents: List[int] = Field(
+        ...,
+        Description="Content of the resume of the profile on the form of a list of int",
+    )
+    FileExt: str = Field(
+        ...,
+        Description="This contains the resume in its original format as it was made available "
+        "by the applicant. It is a C#, unsigned Byte Array. If the resume was "
+        "created interactively on Monster, the resume is provided as consistently "
+        "formatted HTML. If an applicant originally uploaded a resume to Monster ("
+        "e.g., as a PDF), it is delivered in the original document format. Resume "
+        "size can be up to 10 MB. Due to some additional data and overhead coming "
+        "from the serialization / deserialization process, XML format should be set "
+        "to accept up to 8 MB/ JSON format should be set to accept up to 20 MB.",
+    )
+    FirstName: str = Field(..., Description="First name of the profile")
+    JobRefID: str = Field(..., Description="Reference of the profile")
+    LastName: str = Field(..., Description="Last name of the profile")
+    PhoneNumber: str = Field(..., Description="Phone number of the profile")
+    ResumeValue: str = Field(
+        ..., Description="This is a unique alpha numeric value on the Monster side."
+    )
+    State: str
+    VendorField: str = Field(..., Description="Commentary from the job")
+    WorkAuthorization: int = Field(
+        ...,
+        Description="This is a value of whether the applicant is authorized to work "
+        "in the country provided in the countryCode",
+    )
+    ZIPCode: str = Field(..., Description="ZIP code of the profile")
diff --git a/src/hrflow_connectors/connectors/recruitee/README.md b/src/hrflow_connectors/connectors/recruitee/README.md
new file mode 100644
index 000000000..bb0129b18
--- /dev/null
+++ b/src/hrflow_connectors/connectors/recruitee/README.md
@@ -0,0 +1,9 @@
+# Recruitee Connector
+> Collaborative hiring software and modern applicant tracking system with easy setup, great UI, and top-rated customer support. Try free for 18 days!
+
+🔗 https://recruitee.com/
+
+| 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/recruitee/__init__.py b/src/hrflow_connectors/connectors/recruitee/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/recruitee/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/recruitee/actions.py b/src/hrflow_connectors/connectors/recruitee/actions.py
similarity index 54%
rename from src/hrflow_connectors/connectors/boards/recruitee/actions.py
rename to src/hrflow_connectors/connectors/recruitee/actions.py
index d7a74ee97..88f4d1423 100644
--- a/src/hrflow_connectors/connectors/boards/recruitee/actions.py
+++ b/src/hrflow_connectors/connectors/recruitee/actions.py
@@ -1,39 +1,43 @@
-from typing import Iterator, Dict, Any
+from typing import Iterator, Dict, Any, Optional, List
 from pydantic import Field
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
-from ....utils.clean_text import remove_html_tags
+import requests
+
+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
 
 logger = get_logger()
 
 
-class PullJobs(HTTPStream, BoardAction):
+class PullJobsAction(PullJobsBaseAction):
+
     subdomain: str = Field(
         ..., description="the subdomain of your company's careers site."
     )
 
-    @property
-    def base_url(self):
-        return "https://{}.recruitee.com/api/offers".format(self.subdomain)
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[Dict[str, Any]]:
         """
         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
         """
-        response = self.send_request()
-        if response.ok:
-            job_dict_list = response.json()["offers"]
-            return job_dict_list
-        else:
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = f"https://{self.subdomain}.recruitee.com/api/offers"
+        pull_jobs_request.auth = self.auth
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        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"
             )
@@ -41,6 +45,9 @@ def pull(self) -> Iterator[Dict[str, Any]]:
             raise ConnectionError(
                 error_message.format(response.status_code, response.content)
             )
+        response_dict = response.json()
+        job_list = response_dict["offers"]
+        return job_list
 
     def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         """
@@ -110,3 +117,67 @@ def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         ]
 
         return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    auth: AuthorizationAuth
+    company_id: str = Field(
+        ..., description="Company ID. A company subdomain can also be used."
+    )
+    offer_id: Optional[List[int]] = Field(
+        None,
+        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]:
+        """
+        format a HrFlow Profile object into a Recruitee profile Object
+        returns Dict[str, Any]: a profile in the format of Recruitee profiles
+        """
+        profile = dict()
+        info = data.get("info")
+        profile["name"] = info.get("full_name")
+        if data.get("attachments") not in [None, []]:
+            profile["remote_cv_url"] = data.get("attachments")[0].get("public_url")
+        profile["emails"] = [str(info.get("email"))]
+        profile["phones"] = [str(info.get("phone"))]
+
+        def urls():
+            urls = info.get("urls")
+            website_list = []
+            for url in urls:
+                if url["url"] not in ["", None, []]:
+                    website_list.append(url["url"])
+            return website_list
+
+        if urls() not in ["", None, []]:
+            profile["links"] = urls()
+        # Recruitee profile format is in the following format dict(candidate = profile, offers = [offer_id1,...])
+        output_data = dict(candidate=profile)
+        if self.offer_id is not None:
+            output_data["offers"] = self.offer_id
+
+        return output_data
+
+    def push(self, data):
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        push_profile_request.method = "POST"
+        push_profile_request.url = (
+            f"https://api.recruitee.com/c/{self.company_id}/candidates"
+        )
+        push_profile_request.auth = self.auth
+        push_profile_request.json = profile
+        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}`"
+            )
diff --git a/src/hrflow_connectors/connectors/recruitee/connector.py b/src/hrflow_connectors/connectors/recruitee/connector.py
new file mode 100644
index 000000000..01d47a9b5
--- /dev/null
+++ b/src/hrflow_connectors/connectors/recruitee/connector.py
@@ -0,0 +1,78 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any, List
+
+from ...core.connector import Connector
+from ...core.auth import AuthorizationAuth
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Recruitee(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow, board_key: str, subdomain: str, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs listed on Recruitee company endpoints. It adds all these jobs to a Hrflow.ai Board.
+
+        `Recruitee` -> `Hrflow.ai`
+
+        Args:
+            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 `[]`
+            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(
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            subdomain=subdomain,
+            **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: AuthorizationAuth,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        company_id: str,
+        offer_id: Optional[List[int]],
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a `Profile` from a HrFlow Source to a Recruitee company endpoint and a optionally a Jobs pool.
+
+        `Hrflow.ai` -> `Recruitee`
+
+        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
+            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 `[]`
+            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`
+            offer_id (Optional[List[int]], optional): Offers to which the candidate will be assigned with default stage. You can also pass one ID as offer_id. Default value `None`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            profile=profile,
+            company_id=company_id,
+            offer_id=offer_id,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/recruitee/README.md b/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
similarity index 72%
rename from src/hrflow_connectors/connectors/boards/recruitee/README.md
rename to src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
index 7616229e9..32545293f 100644
--- a/src/hrflow_connectors/connectors/boards/recruitee/README.md
+++ b/src/hrflow_connectors/connectors/recruitee/docs/pull_jobs.md
@@ -1,12 +1,16 @@
-# Recruitee Connector
-**Recruitee is a recruiting software.**
+# Pull jobs
 
 `Recruitee` :arrow_right: `Hrflow.ai`
 
-## PullJobs
-`PullJobs` gets all available jobs listed on ***Recruitee company endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs listed on ***Recruitee company endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+**Links to Recruitee documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Get offers](https://docs.recruitee.com/reference/offers) | Endpoint that returns a collection of published company jobs, a company subdomain parameter is required, the request method is `GET`|
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -22,21 +26,18 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Recruitee
 from hrflow import Hrflow
-from hrflow_connectors.connectors.boards.recruitee import PullJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-action = PullJobs(
+
+action = Recruitee.PullJobs(
     subdomain="MY_SUBDOMAIN",
     hrflow_client=client,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/recruitee/README.md b/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
similarity index 53%
rename from src/hrflow_connectors/connectors/destinations/recruitee/README.md
rename to src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
index 2d1295e60..a490eb45f 100644
--- a/src/hrflow_connectors/connectors/destinations/recruitee/README.md
+++ b/src/hrflow_connectors/connectors/recruitee/docs/push_profile.md
@@ -1,12 +1,16 @@
-# Recruitee Connector
-**Recruitee is a recruiting software.**
+# Push profile
 
 `Hrflow.ai` :arrow_right: `Recruitee`
 
-## PushProfile
-`PushProfile` pushes a `Profile` from a ***HrFlow Source*** to a ***Recruitee*** company endpoint and a optionally a Jobs pool..
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***Recruitee*** company endpoint and a optionally a Jobs pool.
 
-### Parameters
+**Links to Recruitee documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Create Candidate](https://docs.recruitee.com/reference/candidates-post) | Create a candidate with provided details,a company id is required however you can also use a company subdomain instead, the request method is `POST`|
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -23,43 +27,25 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Recruitee
 from hrflow import Hrflow
-
-from hrflow_connectors.connectors.destinations.recruitee import PushProfile
-from hrflow_connectors.connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.utils.hrflow import EventParser, Profile, Source
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-def workflow(_request, settings):
-    """
-    CATCH Workflow
-    """    
-    # We add a basic configuration to our logger to see the messages displayed in the standard output
-    # This is not mandatory. It allows you to see what the connector is doing.
-    logger = get_logger_with_basic_config()
-
-    event = EventParser(request=_request)
-    profile = event.get_profile()
-    if profile is not None:
-        logger.info("Profile found !")
-
-        client = Hrflow(api_secret=settings["X-API-KEY"], api_user=settings["X-USER-EMAIL"])
-
-
-        auth = AuthorizationAuth(
-        name = 'Authorization',
-        value= settings['BEARER_TOKEN'],
-    )
-
-        action = PushProfile(
-            company_id = settings['MY_COMPANY_ID'],
-            auth=auth,
-            hrflow_client=client,
-            profile=profile,
-        )
-        response = action.execute()
-        return response
-```
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = AuthorizationAuth(
+    name = 'Authorization',
+    value= settings['BEARER_TOKEN'],
+)
+
+Recruitee.push_profile(
+    company_id = settings['MY_COMPANY_ID'],
+    auth=auth,
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/boards/recruitee/schemas.py b/src/hrflow_connectors/connectors/recruitee/schemas.py
similarity index 61%
rename from src/hrflow_connectors/connectors/boards/recruitee/schemas.py
rename to src/hrflow_connectors/connectors/recruitee/schemas.py
index cfb15a6e6..9b7db1e6c 100644
--- a/src/hrflow_connectors/connectors/boards/recruitee/schemas.py
+++ b/src/hrflow_connectors/connectors/recruitee/schemas.py
@@ -1,4 +1,5 @@
 from pydantic import BaseModel
+from typing import List, Optional
 
 
 class RecruiteJobModel(BaseModel):
@@ -27,3 +28,17 @@ class RecruiteJobModel(BaseModel):
     education_code: str
     city: str
     location: str
+
+
+class RecruiteeProfile(BaseModel):
+    name: str
+    emails: Optional[List[str]]
+    phones: Optional[List[str]]
+    social_links: Optional[List[str]]
+    links: Optional[List[str]]
+    cover_letter: Optional[str]
+
+
+class RecruiteeCandidateModel(BaseModel):
+    candidate: RecruiteeProfile
+    offers: Optional[List[int]]
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/README.md b/src/hrflow_connectors/connectors/sapsuccessfactors/README.md
new file mode 100644
index 000000000..055b3c98c
--- /dev/null
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/README.md
@@ -0,0 +1,10 @@
+# SAP (SuccessFactors) Connector
+> Get software and technology solutions from SAP, the leader in business applications.  Run simple with the best in cloud, analytics, mobile and IT solutions.
+
+🔗 https://www.sap.com/
+
+
+| 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/sapsuccessfactors/__init__.py b/src/hrflow_connectors/connectors/sapsuccessfactors/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py b/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py
new file mode 100644
index 000000000..c335e5e8a
--- /dev/null
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/actions.py
@@ -0,0 +1,322 @@
+from typing import Iterator, Dict, Any, Union, Optional
+from pydantic import Field
+import requests
+import datetime
+
+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
+
+logger = get_logger()
+
+
+class PullJobsAction(PullJobsBaseAction):
+
+    auth: Union[XAPIKeyAuth, OAuth2PasswordCredentialsBody]
+    top: int = Field(
+        20, description="show only the first n items, value by default = `20`"
+    )
+    api_server: str = Field(
+        ...,
+        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]]:
+        """
+        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
+        """
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = f"https://{self.api_server}/odata/v2/JobRequisitionLocale?$top={self.top}&expand=jobRequisition"
+        pull_jobs_request.auth = self.auth
+        pull_jobs_request.headers = {"Accept": "application/json"}
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        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))
+
+        logger.info(f"All jobs have been pulled")
+        response_dict = response.json()
+        job_list = response_dict["d"]["results"]
+        return job_list
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format retrieved jobs into a HrFlow job object
+
+        Args:
+            data (Dict[str, Any]): job to format
+
+        Returns:
+            Dict[str, Any]: job in the HrFlow job object format
+        """
+        job = dict()
+
+        jobRequisition = data.get("jobRequisition")
+
+        # name
+        if data.get("jobTitle") is not None:
+            job["name"] = data.get("jobTitle")
+        else:
+            job["name"] = "Undefined"
+
+        # reference
+        job["reference"] = data.get("jobReqId")
+
+        # location
+        geojson = dict(
+            city=jobRequisition.get("city"),
+            country=jobRequisition.get("country"),
+            facility=jobRequisition.get("facility"),
+            province=jobRequisition.get("stateProvince"),
+        )
+        job["location"] = dict(
+            text=jobRequisition.get("location"),
+            city=jobRequisition.get("city"),
+            geojson=geojson,
+            lat=None,
+            lng=None,
+        )
+
+        # sections
+        job["sections"] = []
+        if data.get("jobDescription") is not None:
+            description_text = data.get("jobDescription")
+            description_text = remove_html_tags(description_text)
+            description_text = description_text.replace("#13;", " ")
+            description_text = description_text.replace("&", "")
+            description_text = description_text.replace("&nbsp;", "")
+            description_text = description_text.replace("quo;s", "")
+
+            description_section = dict(
+                name="sap_description",
+                title="sap_description",
+                description=description_text,
+            )
+            job["sections"].append(description_section)
+
+        # tags
+        annual_salary = jobRequisition.get("annual_SA")
+        department = jobRequisition.get("department")
+        division = jobRequisition.get("division")
+        function = jobRequisition.get("function")
+        industry = jobRequisition.get("industry")
+        monthly_salary = jobRequisition.get("monthly_salary")
+        other_bonus = jobRequisition.get("otherBonus")
+        salary_base = jobRequisition.get("salaryBase")
+        salary_max = jobRequisition.get("salaryMax")
+        salary_min = jobRequisition.get("salaryMin")
+        job_start_date = jobRequisition.get("jobStartDate")
+        job["tags"] = [
+            dict(name="sap_annual_SA", value=annual_salary),
+            dict(name="sap_department", value=department),
+            dict(name="sap_function", value=function),
+            dict(name="sap_division", value=division),
+            dict(name="sap_industry", value=industry),
+            dict(name="sap_monthly_salary", value=monthly_salary),
+            dict(name="sap_otherBonus", value=other_bonus),
+            dict(name="sap_salaryBase", value=salary_base),
+            dict(name="sap_salaryMax", value=salary_max),
+            dict(name="sap_salaryMin", value=salary_min),
+            dict(name="sap_jobStartDate", value=job_start_date),
+        ]
+        job["metadatas"] = [
+            dict(name="sap_recruiterTeam", value=jobRequisition.get("recruiterTeam")),
+            dict(name="sap_sourcerTeam", value=jobRequisition.get("sourcerTeam")),
+            dict(
+                name="sap_hiringManagerTeam",
+                value=jobRequisition.get("hiringManagerTeam"),
+            ),
+        ]
+        return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+
+    profile_already_exists: Any = Field(
+        "COE0019",
+        description="an error code that indicates the candidate is already present and to update we need to update each attribute with a unique request",
+    )
+    auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth]
+    api_server: str = Field(
+        ...,
+        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]:
+        """
+        Formats a hrflow profile into a sap successfactors candidate
+
+        Args:
+            profile (Dict[str, Any]): profile to format
+
+        Returns:
+            Dict[str, Any]: a SAP successfactors profile
+        """
+        sap_profile = dict()
+
+        # Basic information
+        info = profile.get("info")
+        sap_profile["address"] = info.get("location").get("text")
+        sap_profile["cellPhone"] = info.get("phone")
+        fields = info.get("location").get("fields")
+        if fields not in [None, []]:
+            sap_profile["country"] = fields.get("country")[:-1]
+            sap_profile["city"] = fields.get("city")
+            sap_profile["zip"] = fields.get("postcode")
+
+        sap_profile["primaryEmail"] = info.get("email")
+        sap_profile["firstName"] = info.get("first_name")
+        sap_profile["lastName"] = info.get("last_name")
+        sap_profile["currentTitle"] = info.get("summary")
+
+        def format_date(date: datetime.datetime) -> str:
+            """
+            Format `datetime.datetime` to SAP Date
+
+            Args:
+                date (datetime.datetime): Datetime to format
+
+            Returns:
+                str: formatted date
+            """
+            timestamp = int(date.timestamp())
+            sap_date = f"/Date({timestamp})/"
+            return sap_date
+
+        def format_start_date(date_str: str) -> str:
+            """
+            Format ISO start date to SAP Date
+
+            Args:
+                date_str (str): iso date to format
+
+            Returns:
+                str: formatted SAP date
+            """
+            converted_date = from_str_to_datetime(date_str)
+            return format_date(converted_date)
+
+        def format_end_date(date_str: str) -> str:
+            """
+            Format ISO end date to SAP Date
+
+            Args:
+                date_str (str): iso date to format
+
+            Returns:
+                str: formatted SAP date
+            """
+            converted_date = from_str_to_datetime(date_str)
+            # add 10 seconds to avoid that end_date is less or equal than start_date
+            converted_date += datetime.timedelta(seconds=10)
+            return format_date(converted_date)
+
+        # educations
+        if profile.get("educations") is not None:
+
+            def format_education(education: Dict[str, Any]) -> Dict[str, Any]:
+                """
+                Format Hrflow education to SAP Education
+
+                Args:
+                    education (Dict[str, Any]): Hrflow education
+
+                Returns:
+                    Dict[str, Any]: SAP education
+                """
+                sap_education = dict()
+                date_start = education.get("date_start")
+                date_end = education.get("date_end")
+
+                if date_start is not None:
+                    sap_education["startDate"] = format_start_date(date_start)
+
+                if date_end is not None:
+                    sap_education["endDate"] = format_end_date(date_end)
+
+                sap_education["school"] = education.get("school")
+                school_location = education.get("location", {})
+                school_address = school_location.get("text")
+                if school_address is None:
+                    school_address = "Undefined"
+                sap_education["schoolAddress"] = school_address
+                return sap_education
+
+            sap_profile["education"] = dict(results=[])
+            for education in profile.get("educations", []):
+                sap_education = format_education(education)
+                sap_profile["education"]["results"].append(sap_education)
+
+        # experiences
+        if profile.get("experiences") is not None:
+
+            def format_experience(experience: Dict[str, Any]) -> Dict[str, Any]:
+                """
+                Format Hrflow experience to SAP experience
+
+                Args:
+                    experience (Dict[str, Any]): Hrflow experience
+
+                Returns:
+                    Dict[str, Any]:SAP experience
+                """
+                sap_experience = dict()
+                sap_experience["employer"] = experience.get("company")
+
+                experience_location = experience.get("location", {})
+                experience_address = experience_location.get("text")
+                if experience_address is None:
+                    experience_address = "Undefined"
+                sap_experience["employerAddress"] = experience_address
+
+                date_start = experience.get("date_start")
+                date_end = experience.get("date_end")
+                if date_start is not None:
+                    sap_experience["startDate"] = format_start_date(date_start)
+
+                if date_end is not None:
+                    sap_experience["endDate"] = format_end_date(date_end)
+
+                return sap_experience
+
+            sap_profile["outsideWorkExperience"] = dict(results=[])
+            for experience in profile.get("experiences", []):
+                sap_experience = format_experience(experience)
+                sap_profile["outsideWorkExperience"]["results"].append(sap_experience)
+
+        return sap_profile
+
+    def push(self, data):
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        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
+        prepared_request = push_profile_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+        if self.profile_already_exists in response.content.decode("utf-8"):
+            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}`"
+                )
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py b/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py
new file mode 100644
index 000000000..9ea9c4f9f
--- /dev/null
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/connector.py
@@ -0,0 +1,85 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any, Union
+
+from ...core.connector import Connector
+from ...core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class SapSuccessfactors(Connector):
+    @staticmethod
+    def pull_jobs(
+        auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth],
+        hrflow_client: Hrflow,
+        board_key: str,
+        api_server: str,
+        top: int,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs from SAPSuccessFactors via their Job Requisition API. It adds all these jobs to a Hrflow.ai Board.
+
+        `SAP(SuccessFactors)` -> `Hrflow.ai`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            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 `[]`
+            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`
+            top (int, optional): show only the first n items, value by default = `20`. Default value `20`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PullJobsAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            api_server=api_server,
+            top=top,
+            **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: Union[OAuth2PasswordCredentialsBody, XAPIKeyAuth],
+        hrflow_client: Hrflow,
+        profile: Profile,
+        api_server: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a `Profile` from a HrFlow Source to a SAP SuccessFactors Jobs pool.
+
+        `Hrflow.ai` -> `SAP SuccessFactors`
+
+        Args:
+            hrflow_client (Hrflow): Hrflow client instance used to communicate with the Hrflow.ai API
+            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 `[]`
+            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`
+            profile_already_exists (Any, optional): profile already exists !. Default value `None`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            profile=profile,
+            api_server=api_server,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/README.md b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
similarity index 68%
rename from src/hrflow_connectors/connectors/boards/sapsuccessfactors/README.md
rename to src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
index 02458a331..fba1d0130 100644
--- a/src/hrflow_connectors/connectors/boards/sapsuccessfactors/README.md
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/pull_jobs.md
@@ -1,12 +1,16 @@
-# SAP(SuccessFactors) Connector
-**SAP(SuccessFactors) designs and implements innovative solutions for human resources management.**
+# Pull jobs
 
 `SAP(SuccessFactors)` :arrow_right: `Hrflow.ai`
 
-## PullJobs
-`PullJobs` gets all available jobs from SAPSuccessFactors via their ***Job Requisition API***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` gets all available jobs from SAPSuccessFactors via their ***Job Requisition API***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+**Links to SAP documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Get Job Requisitions](https://api.sap.com/api/RCMJobRequisition/overview)| Endpoint to access job requisition data, an api_server is required, the request method is `GET` |
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -24,27 +28,17 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import SAPSuccessfactors
 from hrflow import Hrflow
-
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody, XAPIKeyAuth
-from hrflow_connectors.connectors.boards.sapsuccessfactors import PullJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+from hrflow_connectors import OAuth2PasswordCredentialsBody, XAPIKeyAuth
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+auth = XAPIKeyAuth(name = "APIKey", value = "MY_API_KEY")
 
-auth = XAPIKeyAuth(
-    name = "APIKey",
-    value = "MY_API_KEY",
-)
-
-action = PullJobs(
+SapSuccessfactors.pull_jobs(
     auth=auth,
     top = 30,
     api_server="MY_API_SERVER",
@@ -52,5 +46,4 @@ action = PullJobs(
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md
new file mode 100644
index 000000000..06fab6942
--- /dev/null
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/docs/push_profile.md
@@ -0,0 +1,49 @@
+# Push profile
+
+`Hrflow.ai` :arrow_right: `SAP SuccessFactors`
+
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***SAP SuccessFactors*** Jobs pool.
+
+**Links to SAP documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+|[Post Candidate](https://api.sap.com/api/RCMCandidate/overview) | Endpoint to post a candidate with background information and profile, an api server is required, the request method is `POST`|
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `Union[XAPIKeyAuth, OAuth2PasswordCredentialsBody`] | Auth instance to identify and communicate with the platform        |
+| `api_server` :red_circle: | `str` | api_server: the `api_server` in `https://{api-server}/odata/v2`. For example api_server=`apisalesdemo8.successfactors.com` in `https://apisalesdemo8.successfactors.com/odata/v2`        |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import SapSuccessfactors
+from hrflow import Hrflow
+from hrflow_connectors import OAuth2PasswordCredentialsBody, XAPIKeyAuth
+from hrflow_connectors.connectors.utils.hrflow import Profile, Source
+
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = XAPIKeyAuth(
+    name = "APIKey",
+    value= settings['MY_API_KEY'],
+)
+
+SapSuccessfactors.push_profile(
+    auth=auth,
+    api_server="MY_API_SERVER",
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/schemas.py b/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
similarity index 61%
rename from src/hrflow_connectors/connectors/destinations/sapsuccessfactors/schemas.py
rename to src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
index b3420487c..abd8701f8 100644
--- a/src/hrflow_connectors/connectors/destinations/sapsuccessfactors/schemas.py
+++ b/src/hrflow_connectors/connectors/sapsuccessfactors/schemas.py
@@ -1,21 +1,51 @@
-from typing import Optional, List
-from pydantic import BaseModel, Field
+from typing import Optional, Dict, Any, List
+from pydantic import BaseModel
+
+# Job model
+class SAPSuccessFactorsJobRequistion(BaseModel):
+    annual_SA: str
+    location: Optional[str]
+    city: Optional[str] = None
+    country: Optional[str] = None
+    department: Optional[str] = None
+    division: Optional[str] = None
+    facility: Optional[str] = None
+    function: Optional[str] = None
+    industry: Optional[str] = None
+    monthly_salary: Optional[str] = None
+    salaryBase: Optional[str] = None
+    otherBonus: Optional[str] = None
+    salaryMax: Optional[str] = None
+    salaryMin: Optional[str] = None
+    stateProvince: Optional[str] = None
+    jobStartDate: Optional[str] = None
+    recruiterTeam: Optional[Dict[str, Any]] = None
+    hiringManagerTeam: Optional[Dict[str, Any]] = None
+    sourcerTeam: Optional[Dict[str, Any]] = None
+
+
+class SAPSuccessFactorsJob(BaseModel):
+    jobDescription: str
+    jobTitle: str
+    jobReqId: str
+    jobRequisition: SAPSuccessFactorsJobRequistion
+
+
+# Profile model
+
 
 class Result(BaseModel):
-  
-    
+
     endDate: str
     school: str
     schoolAddress: str
     startDate: str
 
 
-
 class Education(BaseModel):
     results: List[Result]
 
 
-
 class ResultLanguage(BaseModel):
     language: str
     readingProf: str
@@ -37,6 +67,7 @@ class ResultOutsideWorkExperience(BaseModel):
 class OutsideWorkExperience(BaseModel):
     results: List[ResultOutsideWorkExperience]
 
+
 class InsideWorkExperienceResult(BaseModel):
     backgroundElementId: str
     bgOrderPos: str
@@ -52,6 +83,7 @@ class InsideWorkExperienceResult(BaseModel):
 class InsideWorkExperience(BaseModel):
     results: List[InsideWorkExperienceResult]
 
+
 class TalentPoolResults(BaseModel):
     startDate: str
     talentPoolComments: str
@@ -64,7 +96,7 @@ class TalentPool(BaseModel):
 
 
 class SapCandidateModel(BaseModel):
-    address: str    
+    address: str
     cellPhone: Optional[str]
     city: Optional[str]
     contactEmail: Optional[str]
@@ -84,4 +116,4 @@ class SapCandidateModel(BaseModel):
     languages: Languages
     outsideWorkExperience: Optional[OutsideWorkExperience]
     insideWorkExperience: Optional[InsideWorkExperience]
-    talentPool: Optional[TalentPool]
\ No newline at end of file
+    talentPool: Optional[TalentPool]
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/README.md b/src/hrflow_connectors/connectors/smartrecruiters/README.md
new file mode 100644
index 000000000..049ec514c
--- /dev/null
+++ b/src/hrflow_connectors/connectors/smartrecruiters/README.md
@@ -0,0 +1,9 @@
+# Smart Recruiters Connector
+> Move beyond applicant tracking systems (ATS) with an enterprise-grade recruiting platform designed for the modern workforce. SmartRecruiters' Talent Acquisition Suite provides everything needed to attract, select, and hire great talent.
+
+🔗 https://www.smartrecruiters.com/
+
+| 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/smartrecruiters/__init__.py b/src/hrflow_connectors/connectors/smartrecruiters/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/smartrecruiters/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/smartrecruiters/actions.py b/src/hrflow_connectors/connectors/smartrecruiters/actions.py
similarity index 51%
rename from src/hrflow_connectors/connectors/boards/smartrecruiters/actions.py
rename to src/hrflow_connectors/connectors/smartrecruiters/actions.py
index ef71a0730..0f75544e6 100644
--- a/src/hrflow_connectors/connectors/boards/smartrecruiters/actions.py
+++ b/src/hrflow_connectors/connectors/smartrecruiters/actions.py
@@ -3,16 +3,15 @@
 import itertools
 import requests
 
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....core.auth import XSmartTokenAuth
-from ....utils.logger import get_logger
+from ...core.action import PullJobsBaseAction, PushProfileBaseAction
+from ...core.auth import XSmartTokenAuth
+from ...utils.logger import get_logger
 
 
 logger = get_logger()
 
 
-class GetAllJobs(HTTPStream, BoardAction):
+class PullJobsAction(PullJobsBaseAction):
     auth: XSmartTokenAuth
     query: Optional[str] = Field(
         None,
@@ -34,14 +33,6 @@ class GetAllJobs(HTTPStream, BoardAction):
         description="Number of elements to return per page. max value is 100. Default value : 10",
     )
 
-    @property
-    def base_url(self):
-        return "https://api.smartrecruiters.com/jobs"
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[Dict[str, Any]]:
         """
         Pull all jobs from SmartRecruiters
@@ -49,22 +40,33 @@ def pull(self) -> Iterator[Dict[str, Any]]:
         Returns:
             Iterator[Dict[str, Any]]: an iterator of jobs
         """
-        # If param value is `None`, `requests` in `HTTPStream` ignores the param
-        self.params["q"] = self.query
-        self.params["updatedAfter"] = self.updated_after
-        self.params["postingStatus"] = self.posting_status
-        self.params["status"] = self.job_status
-        self.params["limit"] = self.limit
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = "https://api.smartrecruiters.com/jobs"
+        pull_jobs_request.auth = self.auth
+
+        ## Set params
+        # If param value is `None`, `requests` ignores the param
+        pull_jobs_params = dict()
+        pull_jobs_params["q"] = self.query
+        pull_jobs_params["updatedAfter"] = self.updated_after
+        pull_jobs_params["postingStatus"] = self.posting_status
+        pull_jobs_params["status"] = self.job_status
+        pull_jobs_params["limit"] = self.limit
+        pull_jobs_request.params = pull_jobs_params
 
         # Define page generator
         def get_page():
             next_page_id = None
             job_list = None
             while job_list != []:
-                self.params["pageId"] = next_page_id
-                response = self.send_request()
+                pull_jobs_request.params["pageId"] = next_page_id
+                prepared_request = pull_jobs_request.prepare()
+                response = session.send(prepared_request)
 
-                if response.status_code >= 400:
+                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))
@@ -100,12 +102,18 @@ def get_full_job(light_job_dict: Dict[str, Any]) -> Dict[str, Any]:
                 Dict[str, Any]: full job
             """
             job_id = light_job_dict["id"]
-            hearders = dict()
-            self.auth.update(headers=hearders)
-            get_job_url = f"https://api.smartrecruiters.com/jobs/{job_id}"
-            response = requests.get(get_job_url, headers=hearders)
 
-            if response.status_code >= 400:
+            # prepare `get_job` request
+            get_job_request = requests.Request()
+            get_job_request.method = "GET"
+            get_job_request.url = f"https://api.smartrecruiters.com/jobs/{job_id}"
+            get_job_request.auth = self.auth
+
+            # send request
+            prepared_request = pull_jobs_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)
@@ -237,3 +245,161 @@ def add_section(section_name: str):
         ]
 
         return job
+
+
+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` ",
+    )
+
+    def format(self, profile: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format a profile hrflow object to a smartrecruiters profile object
+
+        Args:
+            profile (Dict[str, Any]): [profile object in the hrflow profile format]
+
+        Returns:
+            Dict[str, Any]: [profile in the SmartRecruiters candidate application format]
+        """
+
+        value_or_undefined = lambda s: s or "Undefined"
+
+        def format_project(project):
+            formatted_project_dict = dict()
+            # current
+            formatted_project_dict["current"] = False
+            # start date
+            start_datetime_str = project.get("date_start")
+            if start_datetime_str is None:
+                # datetime is either in format YYYY or YYYY-MM...
+                # if there is none we force this value to avoid conflict with smartrecruiters profile object
+                start_date = "XXXX"
+            else:
+                start_date = start_datetime_str.split("T")[0]
+
+            formatted_project_dict["startDate"] = start_date
+
+            end_datetime_str = project.get("date_end")
+            if end_datetime_str is None:
+                end_date = "XXXX"
+            else:
+                end_date = end_datetime_str.split("T")[0]
+
+            formatted_project_dict["endDate"] = end_date
+            formatted_project_dict["location"] = value_or_undefined(
+                project["location"]["text"]
+            )
+            formatted_project_dict["description"] = project["description"]
+
+            return formatted_project_dict
+
+        def format_educations(educations):
+            formatted_education_list = []
+
+            for education_entity in educations:
+                formatted_education = format_project(education_entity)
+                if education_entity.get("school") is None:
+                    formatted_education["instituion"] = "Undefined"
+                else:
+                    formatted_education["institution"] = education_entity.get("school")
+
+                if education_entity.get("title") is None:
+                    formatted_education["degree"] = "Undefined"
+                else:
+                    formatted_education["degree"] = education_entity.get("title")
+                formatted_education["major"] = "Undefined"
+
+                formatted_education_list.append(formatted_education)
+
+            return formatted_education_list
+
+        def format_experiences(experiences):
+            formatted_experience_list = []
+
+            for exp in experiences:
+                formatted_exp = format_project(exp)
+                if exp["title"] is None:
+                    formatted_exp["title"] = "Undefined"
+                else:
+                    formatted_exp["title"] = exp["title"]
+                if exp["company"] is None:
+                    formatted_exp["company"] = "Undefined"
+                else:
+                    formatted_exp["company"] = exp["company"]
+
+                formatted_experience_list.append(formatted_exp)
+
+            return formatted_experience_list
+
+        info = profile["info"]
+        smart_candidate = dict()
+        smart_candidate["firstName"] = info["first_name"]
+        smart_candidate["lastName"] = info["last_name"]
+        smart_candidate["email"] = info["email"]
+        smart_candidate["phoneNumber"] = info["phone"]
+
+        if info["location"]["fields"] not in [
+            [],
+            None,
+        ]:  # check if fields is not an undefined list
+            smart_candidate["location"] = dict(
+                city=value_or_undefined(info["location"]["fields"].get("city", {})),
+                country=value_or_undefined(
+                    info["location"]["fields"].get("country", {})
+                ),
+                region=value_or_undefined(info["location"]["fields"].get("state", {})),
+                lat=info["location"]["lat"]
+                if info["location"]["lat"] is not None
+                else 0,
+                lng=info["location"]["lng"]
+                if info["location"]["lng"] is not None
+                else 0,
+            )
+        else:
+            smart_candidate["location"] = dict(
+                country="Undefined",
+                region="Undefined",
+                City="Undefined",
+                lat=0,
+                lng=0,
+            )
+
+        smart_candidate["web"] = dict(info["urls"])
+        smart_candidate["tags"] = []
+        smart_candidate["education"] = format_educations(profile.get("educations"))
+        smart_candidate["experience"] = format_experiences(profile.get("experiences"))
+        smart_candidate["attachments"] = profile.get("attachments")
+        smart_candidate["consent"] = True
+
+        return smart_candidate
+
+    def push(self, data: Dict[str, Any]):
+        """
+        Push profile
+
+        Args:
+            data (Dict[str, Any]): Profile
+        """
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        push_profile_request.method = "POST"
+        push_profile_request.url = (
+            f"https://api.smartrecruiters.com/jobs/{self.job_id}/candidates"
+        )
+        push_profile_request.auth = self.auth
+        push_profile_request.json = profile
+        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}`"
+            )
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/connector.py b/src/hrflow_connectors/connectors/smartrecruiters/connector.py
new file mode 100644
index 000000000..ad80bdc3c
--- /dev/null
+++ b/src/hrflow_connectors/connectors/smartrecruiters/connector.py
@@ -0,0 +1,77 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from ...core.auth import XSmartTokenAuth
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class SmartRecruiters(Connector):
+    @staticmethod
+    def pull_jobs(
+        auth: XSmartTokenAuth, hrflow_client: Hrflow, board_key: str, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` retrieves all jobs via the SmartRecruiter API. It adds all these jobs to a Hrflow.ai Board.
+
+        `Smart Recruiters` -> `Hrflow.ai`
+
+        Args:
+            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 `[]`
+            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`
+            query (str, optional): Full-text search query based on a job title; case insensitive; e.g. java developer. Default value `None`
+            updated_after (str, optional): ISO8601-formatted time boundaries for the job update time. Default value `None`
+            posting_status (str, optional): Posting status of a job. Available values  PUBLIC, INTERNAL, NOT_PUBLISHED, PRIVATE. Default value `None`
+            job_status (str, optional): Status of a job. Available values  CREATED, SOURCING, FILLED, INTERVIEW, OFFER, CANCELLED, ON_HOLD. Default value `None`
+            limit (int, optional): Number of elements to return per page. max value is 100. Default value  10. Default value `10`
+
+        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: XSmartTokenAuth,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        job_id: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a HrFlow.ai profile from a Hrflow.ai Source to `SmartRecruiters` via the SmartRecruiter API.
+
+        `Hrflow.ai` -> `Smart Recruiters`
+
+        Args:
+            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
+            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 `[]`
+            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,
+            job_id=job_id,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/smartrecruiters/README.md b/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
similarity index 70%
rename from src/hrflow_connectors/connectors/boards/smartrecruiters/README.md
rename to src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
index 41ee5f3f4..233374d93 100644
--- a/src/hrflow_connectors/connectors/boards/smartrecruiters/README.md
+++ b/src/hrflow_connectors/connectors/smartrecruiters/docs/pull_jobs.md
@@ -1,12 +1,16 @@
-# Smart Recruiters Connector
-**Smart Recruiters is an ATS.**
+# Pull jobs
+`SmartRecruiters` :arrow_right: `Hrflow.ai`
 
-`Smart Recruiters` :arrow_right: `Hrflow.ai`
+`PullJobsAction` retrieves all jobs via the ***SmartRecruiter*** API. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-## GetAllJobs
-`GetAllJobs` retrieves all jobs via the ***SmartRecruiter*** API. It adds all these **jobs** to a ***Hrflow.ai Board***.
+**Links to SmartRecruiters documentation on the endpoints used :**
 
-### Parameters
+| Endpoints | Description |
+| --------- | ----------- |
+|[Get all jobs](https://dev.smartrecruiters.com/customer-api/live-docs/job-api/#/jobs/jobs.all) | Endpoint to search jobs by traditional params (offset, limit...) and get the list of all jobs with their ids, the request method is `GET`|
+|[Get job content](https://dev.smartrecruiters.com/customer-api/live-docs/job-api/#/jobs/jobs.get) | Endpoint to get the content of a job with a given id, a jobId parameter is required, the request method is `GET`|
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -27,26 +31,17 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import SmartRecruiter
 from hrflow import Hrflow
-
-from hrflow_connectors.core.auth import XSmartTokenAuth
-from hrflow_connectors.connectors.boards.smartrecruiters import GetAllJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+from hrflow_connectors import XSmartTokenAuth
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+auth = XSmartTokenAuth(value=smart_token)
 
-auth = XSmartTokenAuth(
-    value="MY_SMART_TOKEN"
-)
-
-action = GetAllJobs(
+SmartRecruiter.pull_jobs(
     auth=auth,
     hrflow_client=client,
     limit=2,
@@ -55,5 +50,4 @@ action = GetAllJobs(
     archive_deleted_jobs_from_stream=False,
     posting_status="PUBLIC",
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md b/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md
new file mode 100644
index 000000000..ed4f9dc05
--- /dev/null
+++ b/src/hrflow_connectors/connectors/smartrecruiters/docs/push_profile.md
@@ -0,0 +1,45 @@
+# Push profile
+`Hrflow.ai` :arrow_right: `SmartRecruiters`
+
+`PushProfileAction` pushes a HrFlow.ai profile from a ***Hrflow.ai Source*** to `SmartRecruiters` via the ***SmartRecruiter*** API.
+
+**Links to SmartRecruiters documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Post Candidate](https://dev.smartrecruiters.com/customer-api/live-docs/candidate-api/) | Endpoint to create a new candidate and assign to a talent pool, the request method is `POST` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `XSmartTokenAuth` | Auth instance to identify and communicate with the platform       |
+| `job_id` :red_circle: | `str` | Id of a Job to which you want to assign a candidate when it’s created. For example job_id=`78d3ef91-8868-4ff3-b35d-9debf9d6f31f` in `https://api.smartrecruiters.com/jobs/78d3ef91-8868-4ff3-b35d-9debf9d6f31f/candidates`        |
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import SmartRecruiters
+from hrflow import Hrflow
+from hrflow_connectors import XSmartTokenAuth
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+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"))
+
+SmartRecruiters.push_profile(
+    auth=auth,
+    job_id="3696cad0-a9b0-4a40-9cd7-4cc5feb1a509",
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/smartrecruiters/schemas.py b/src/hrflow_connectors/connectors/smartrecruiters/schemas.py
new file mode 100644
index 000000000..3f9f28ca9
--- /dev/null
+++ b/src/hrflow_connectors/connectors/smartrecruiters/schemas.py
@@ -0,0 +1,156 @@
+from typing import List, Optional
+
+from pydantic import BaseModel
+
+# Job Model
+class Department(BaseModel):
+    id: 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]
+    remote: Optional[bool]
+    manual: Optional[bool]
+
+
+class Industry(BaseModel):
+    id: str
+
+
+class Function(BaseModel):
+    id: str
+
+
+class TypeOfEmployment(BaseModel):
+    id: str
+
+
+class ExperienceLevel(BaseModel):
+    id: Optional[str]
+
+
+class EeoCategory(BaseModel):
+    id: str
+
+
+class Creator(BaseModel):
+    firstName: str
+    lastName: str
+
+
+class Compensation(BaseModel):
+    min: int
+    max: int
+    currency: str
+
+
+class CompanyDescription(BaseModel):
+    title: str
+    text: str
+
+
+class JobDescription(BaseModel):
+    title: str
+    text: str
+
+
+class Qualifications(BaseModel):
+    title: str
+    text: str
+
+
+class AdditionalInformation(BaseModel):
+    title: str
+    text: str
+
+
+class Sections(BaseModel):
+    companyDescription: CompanyDescription
+    jobDescription: JobDescription
+    qualifications: Qualifications
+    additionalInformation: AdditionalInformation
+
+
+class JobAd(BaseModel):
+    sections: Sections
+
+
+class SmartRecruitersModel(BaseModel):
+    title: str
+    refNumber: str
+    createdOn: str
+    updatedOn: str
+    department: Optional[Department]
+    location: Location
+    status: Optional[str]
+    postingStatus: Optional[str]
+    targetHiringDate: Optional[str]
+    industry: Optional[Industry]
+    function: Optional[Function]
+    typeOfEmployment: Optional[TypeOfEmployment]
+    experienceLevel: Optional[ExperienceLevel]
+    eeoCategory: Optional[EeoCategory]
+    creator: Optional[Creator]
+    compensation: Optional[Compensation]
+    jobAd: JobAd
+
+
+# Profile Model
+
+
+class Location(BaseModel):
+    country: str
+    countryCode: str
+    regionCode: str
+    region: str
+    city: str
+    lat: int
+    lng: int
+
+
+class Web(BaseModel):
+    skype: str
+    linkedin: str
+    facebook: str
+    twitter: str
+    website: str
+
+
+class EducationItem(BaseModel):
+    institution: str
+    degree: str
+    major: str
+    current: bool
+    location: str
+    startDate: str
+    endDate: str
+    description: str
+
+
+class ExperienceItem(BaseModel):
+    title: str
+    company: str
+    current: bool
+    startDate: str
+    endDate: str
+    location: str
+    description: str
+
+
+class SmartrecruitersProfileModel(BaseModel):
+    firstName: str
+    lastName: str
+    email: str
+    phoneNumber: str
+    location: Location
+    web: Web
+    tags: List[str]
+    education: List[EducationItem]
+    experience: List[ExperienceItem]
diff --git a/src/hrflow_connectors/connectors/taleez/README.md b/src/hrflow_connectors/connectors/taleez/README.md
new file mode 100644
index 000000000..b09d44168
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/README.md
@@ -0,0 +1,9 @@
+# Taleez Connector
+> ATS recruitment management software, manage all your processes from A to Z and centralize all your recruitments: centralization, multi-distribution, career site, CV library...
+
+🔗 https://taleez.com/
+
+| 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/taleez/__init__.py b/src/hrflow_connectors/connectors/taleez/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/taleez/actions.py b/src/hrflow_connectors/connectors/taleez/actions.py
new file mode 100644
index 000000000..7ec582356
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/actions.py
@@ -0,0 +1,234 @@
+from typing import Iterator, Dict, Any, Optional
+from pydantic import Field
+import requests
+
+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
+
+logger = get_logger()
+
+
+class PullJobsAction(PullJobsBaseAction):
+    auth: XTaleezAuth
+    page: int = Field(0, description="Page number. Start at '0'")
+    page_size: int = Field(
+        100, description="Page size. Max size of the list returned. Max value : 100"
+    )
+
+    def pull(self) -> Iterator[Dict[str, Any]]:
+        """
+        Pull jobs from a Taleez jobs owner endpoint
+
+        Returns list of all jobs that have been pulled
+        """
+
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = f"https://api.taleez.com/0/jobs?page={self.page}&pageSize={self.page_size}&withDetails=true"
+        pull_jobs_request.auth = self.auth
+        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 this endpoint")
+            error_message = "Unable to pull the data ! Reason : `{}` `{}`"
+            raise RuntimeError(
+                error_message.format(response.status_code, response.content)
+            )
+
+        response_dict = response.json()
+        logger.info(f"Total found: {response_dict['listSize']}")
+        job_list = response_dict["list"]
+
+        return job_list
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format a job into the hrflow job object format
+
+        Args:
+            data (Dict[str, Any]): a taleez job object form
+
+        Returns:
+            Dict[str, Any]: a hrflow job object form
+        """
+
+        job = dict()
+
+        # Job basic information
+        job["name"] = data.get("label")
+        job["reference"] = str(data["id"])
+        job["url"] = data.get("url")
+        job["summary"] = None
+
+        # Job Location
+        city = data.get("city")
+        country = data.get("country")
+        postalcode = data.get("postalCode")
+        lat = data.get("lat")
+        lng = data.get("lng")
+        geojson = dict(country=country, postalcode=postalcode)
+        job["location"] = dict(text=city, lat=lat, lng=lng, geojson=geojson)
+
+        # Job Sections
+        job["sections"] = []
+
+        def create_section(field_name: str):
+            section_name = "taleez_{}".format(field_name)
+            section_tile = section_name
+            description = remove_html_tags(data.get(field_name))
+            if description is not None:
+                section = dict(
+                    name=section_name, title=section_tile, description=description
+                )
+                job["sections"].append(section)
+
+        create_section("jobDescription")
+        create_section("profileDescription")
+        create_section("companyDescription")
+
+        # Job Tags
+        job["tags"] = []
+
+        def create_tag(field_name: str):
+            tag_name = "taleez_{}".format(field_name)
+            tag_value = data.get(field_name)
+            if tag_value is not None:
+                tag = dict(name=tag_name, value=tag_value)
+                job["tags"].append(tag)
+
+        create_tag("contract")
+        create_tag("profile")
+        create_tag("contractLength")
+        create_tag("fullTime")
+        create_tag("workHours")
+        create_tag("qualification")
+        create_tag("remote")
+        create_tag("recruiterId")
+        create_tag("companyLabel")
+        create_tag("urlApplying")
+        create_tag("currentStatus")
+
+        def seconds_to_isoformat(seconds: int) -> str:
+            """
+            Seconds_to_iso8601 converts seconds to datetime ISOFORMAT
+
+            Args:
+                seconds : datetime in seconds since epoch
+
+            returns datetime in isoformat for example for 1642104049 secs returns '2022-01-13T20:00:49'
+            """
+            return datetime.utcfromtimestamp(seconds).isoformat()
+
+        # datetime fields
+        job["created_at"] = seconds_to_isoformat(data.get("dateCreation"))
+        job["updated_at"] = seconds_to_isoformat(data.get("dateLastPublish"))
+
+        return job
+
+
+class PushProfileAction(PushProfileBaseAction):
+    recruiter_id: int = Field(
+        ..., description="ID of the person recruiting the candidate, mandatory"
+    )
+    job_id: Optional[int] = Field(
+        None, description="ID of the job to add a candidate to"
+    )
+
+    def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Format a Hrflow profile object into a Taleez profile object
+
+        Returns: Dict[str, Any] a Taleez profile object form
+        """
+        profile = dict()
+        info = data.get("info")
+        profile["firstName"] = info.get("first_name")
+        profile["lastName"] = info.get("last_name")
+        profile["email"] = info.get("email")
+        profile["number"] = info.get("phone")
+        profile["lang"] = data.get("text_language")
+        profile["recruiterId"] = self.recruiter_id
+
+        profile["socialLinks"] = dict()
+
+        def format_urls() -> None:
+            """
+            format_urls, add links and websites to Taleez profile Social links
+            """
+            urls = info.get("urls")
+            if isinstance(urls, list):
+                for url in urls:
+                    type = url.get("type")
+                    link = url.get("url")
+                    if isinstance(link, str):
+                        profile["socialLinks"][type] = link
+            attachments = info.get("attachments")
+            if isinstance(attachments, list):
+                for attachment in attachments:
+                    file_name = attachment.get("file_name")
+                    public_url = attachment.get("public_url")
+                    if isinstance(public_url, str):
+                        profile["socalLinks"][file_name] = public_url
+
+        format_urls()
+
+        return profile
+
+    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
+        """
+        profile = next(data)
+
+        # Prepare request
+        session = requests.Session()
+        push_profile_request = requests.Request()
+        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
+        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}`"
+            )
+        if self.job_id is not None:
+            push_profile_response_dict = push_profile_response.json()
+            candidate_id = push_profile_response_dict["id"]
+            # Prepare request
+            add_profile_request = requests.Request()
+            add_profile_request.method = "POST"
+            add_profile_request.url = (
+                f"https://api.taleez.com/0/jobs/{self.job_id}/candidates"
+            )
+            add_profile_request.auth = self.auth
+            add_profile_request.json = dict(ids=[candidate_id])
+            prepared_add_profile_request = add_profile_request.prepare()
+
+            # Send request
+            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}`"
+                )
diff --git a/src/hrflow_connectors/connectors/taleez/connector.py b/src/hrflow_connectors/connectors/taleez/connector.py
new file mode 100644
index 000000000..ec95b23ca
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/connector.py
@@ -0,0 +1,75 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any, Union, List
+
+from ...core.connector import Connector
+from ...core.auth import XTaleezAuth
+
+from ...utils.hrflow import Profile
+from .actions import PullJobsAction, PushProfileAction
+
+
+class Taleez(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow, board_key: str, auth: XTaleezAuth, **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` gets all available jobs listed on a Taleez endpoint. It adds all these jobs to a Hrflow.ai Board.
+
+        `Taleez` -> `Hrflow.ai`
+
+        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
+            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 `[]`
+            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`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PullJobsAction(
+            hrflow_client=hrflow_client, board_key=board_key, auth=auth, **kwargs
+        )
+        return action.execute()
+
+    @staticmethod
+    def push_profile(
+        auth: XTaleezAuth,
+        hrflow_client: Hrflow,
+        profile: Profile,
+        recruiter_id: int,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PushProfileAction` pushes a `Profile` from a HrFlow Source to a Taleez Jobs pool.
+
+        `Hrflow.ai` -> `Taleez`
+
+        Args:
+            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 `[]`
+            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`
+            job_id (int, optional): ID of the job to add a candidate to. Default value `None`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+        action = PushProfileAction(
+            auth=auth,
+            hrflow_client=hrflow_client,
+            profile=profile,
+            recruiter_id=recruiter_id,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md b/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md
new file mode 100644
index 000000000..86226e774
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/docs/pull_jobs.md
@@ -0,0 +1,48 @@
+# Pull jobs
+`Taleez` :arrow_right: `Hrflow.ai`
+
+`PullJobsAction` gets all available jobs listed on a ***Taleez endpoint***. It adds all these **jobs** to a ***Hrflow.ai Board***.
+
+**Links to Taleez documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+|[ List all jobs](https://api.taleez.com/swagger-ui/index.html?configUrl=/openapi.json/swagger-config#/jobs/list_2) | Endpoint to list all jobs in your company, the request method is `GET` |
+
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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`        |
+| `Auth` | :red_circle: | `XTaleezAuth` | Required to access Taleez API.
+| `archive_deleted_jobs_from_stream`  | `bool` | Archive Board jobs when they are no longer in the incoming job stream. Default value : `True`        |
+| `page` :red_circle: | `int` | page number, starts at 0, value by default is 0     |
+| `page_size` :red_circle: | `int` | Page size. Max size of the list returned. Max value : 100, default value is 100|
+
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Taleez
+from hrflow import Hrflow
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+auth = XTaleezAuth(value='MY_X_TALEEZ_API_KEY')
+
+Taleez.pull_jobs(
+    page=MY_START_PAGE,
+    page_size=MY_PAGE_SIZE_LIMIT,
+    auth=auth,
+    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/taleez/docs/push_profile.md b/src/hrflow_connectors/connectors/taleez/docs/push_profile.md
new file mode 100644
index 000000000..6462916e5
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/docs/push_profile.md
@@ -0,0 +1,48 @@
+# Push profile
+
+`Hrflow.ai` :arrow_right: `Taleez`
+
+`PushProfileAction` pushes a `Profile` from a ***HrFlow Source*** to a ***Taleez*** Jobs pool.
+
+**Links to Taleez documentation on the endpoints used :**
+
+| Endpoints | Description |
+| --------- | ----------- |
+| [Add Candidate](https://api.taleez.com/swagger-ui/index.html?configUrl=/openapi.json/swagger-config#/candidates/create_1) | Endpoint for sourcing use case, to create a new candidate from his personal information, the request method is `Post` |
+## Parameters
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `logics`  | `List[str]` | Function names to apply as filter before pushing the data. 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: | `XTaleezAuth` | Auth instance to identify and communicate with the platform        |
+| `job_id` :red_circle: | `Optional[int]` | ID of the job to add a candidate to. Default value : `None` |
+| `recruiter_id` :red_circle: | `int` | ID of the person recruiting the candidate, mandatory|
+
+
+:red_circle: : *required* 
+
+## Example
+
+```python
+from hrflow_connectors import Taleez
+from hrflow import Hrflow
+from hrflow_connectors import XTaleezAuth
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
+profile = Profile(key="PROFILE_KEY", source=Source(key="SOURCE_KEY"))
+auth = XTaleezAuth(value='MY_X_TALEEZ_API_KEY')
+
+Taleez.push_profile(
+    auth=auth,
+    add_candidate_to_job=False,
+    recruiter_id="MY_RECRUITER_ID",
+    hrflow_client=client,
+    profile=profile,
+)
+```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/taleez/schemas.py b/src/hrflow_connectors/connectors/taleez/schemas.py
new file mode 100644
index 000000000..0d08c6b3d
--- /dev/null
+++ b/src/hrflow_connectors/connectors/taleez/schemas.py
@@ -0,0 +1,46 @@
+from typing import Any, Optional, Dict
+
+from pydantic import BaseModel, Field
+
+
+class TaleezJobModel(BaseModel):
+    id: int
+    dateCreation: int
+    dateFirstPublish: int
+    dateLastPublish: int
+    label: str = Field(..., description="Job Title")
+    profile: str = Field(
+        ..., description="Job Profile Type, for example administration, HR..."
+    )
+    currentStatus: str = Field(
+        ..., description="status of the JOb whether it is Published, Draft or suspended"
+    )
+    contract: str = Field(..., description="Job Employment Type")
+    contractLength: int
+    fullTime: bool
+    workHours: int
+    qualification: str
+    remote: bool
+    country: str
+    city: str
+    postalCode: Any
+    lat: Any
+    lng: Any
+    recruiterId: int
+    companyLabel: str = Field(..., description="Company Name")
+    url: str
+    urlApplying: str
+    jobDescription: str
+    profileDescription: str
+    companyDescription: str
+
+
+class TaleezCandidateModel(BaseModel):
+    firstName: str
+    lastName: str
+    mail: str
+    phone: str
+    initialReferrer: Optional[str]
+    lang: str
+    recruiterId: int
+    socialLinks: Optional[Dict[str, str]]
diff --git a/src/hrflow_connectors/connectors/workable/README.md b/src/hrflow_connectors/connectors/workable/README.md
new file mode 100644
index 000000000..d5b34f4a2
--- /dev/null
+++ b/src/hrflow_connectors/connectors/workable/README.md
@@ -0,0 +1,8 @@
+# Workable Connector
+> More than an applicant tracking system, Workable's talent acquisition software helps teams find candidates, evaluate applicants and make the right hire, faster.
+
+🔗 https://www.workable.com/
+
+| Actions |
+| ------- |
+| [**Pull jobs**](docs/pull_jobs.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/workable/__init__.py b/src/hrflow_connectors/connectors/workable/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/workable/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/workable/actions.py b/src/hrflow_connectors/connectors/workable/actions.py
similarity index 71%
rename from src/hrflow_connectors/connectors/boards/workable/actions.py
rename to src/hrflow_connectors/connectors/workable/actions.py
index de54bf054..e5160922a 100644
--- a/src/hrflow_connectors/connectors/boards/workable/actions.py
+++ b/src/hrflow_connectors/connectors/workable/actions.py
@@ -1,67 +1,57 @@
 from typing import Iterator, Dict, Any
 from pydantic import Field
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-from ....utils.logger import get_logger
-from ....utils.clean_text import remove_html_tags
+import requests
+
+from ...core.action import PullJobsBaseAction
+from ...utils.logger import get_logger
+from ...utils.clean_text import remove_html_tags
 
 logger = get_logger()
 
 
-class PullJobs(HTTPStream, BoardAction):
+class PullJobsAction(PullJobsBaseAction):
+
     subdomain: str = Field(
         ...,
         description="subdomain of a company endpoint in `https://www.workable.com/api/accounts/{subdomain}` for example subdomain=`eurostar` for eurostar company",
     )
 
-    @property
-    def base_url(self):
-        return "https://www.workable.com/api/accounts/{}?details=true".format(
-            self.subdomain
-        )
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[Dict[str, Any]]:
         """
         pull all jobs from a workable public endpoint jobs stream
-
-        Raises:
-            Exception: if there are no jobs posted
-            ConnectionError: if the request failed, you may want to check your subdomain
-
         Returns:
             Iterator[Dict[str, Any]]: a list of jobs dictionaries
         """
 
-        response = self.send_request()
-        if response.status_code == 200:
-            job_dict_list = response.json()["jobs"]
-            total_found = len(job_dict_list)
-            if total_found == 0:
-                logger.info(f"No jobs found for this request")
-                raise Exception(
-                    f"This company has no jobs available on their workable public endpoint"
-                )
-            else:
-                logger.info(f"Total jobs found for this request: {len(job_dict_list)}")
-                return job_dict_list
-        else:
+        # 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"
+        )
+        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"
             )
             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
+
     def format(self, data: Dict[str, Any]) -> Dict[str, Any]:
         """
         format a job into the hrflow job object format
-
         Args:
             data (Dict[str, Any]): a job object pulled from workable subdomain
-
         Returns:
             Dict[str, Any]: a job into the hrflow job object format
         """
diff --git a/src/hrflow_connectors/connectors/workable/connector.py b/src/hrflow_connectors/connectors/workable/connector.py
new file mode 100644
index 000000000..37f887169
--- /dev/null
+++ b/src/hrflow_connectors/connectors/workable/connector.py
@@ -0,0 +1,41 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from .actions import PullJobsAction
+
+
+class Workable(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow, board_key: str, 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.
+
+        `Workable` -> `Hrflow.ai`
+
+        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
+            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 `[]`
+            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`
+
+        Returns:
+            Optional[Dict[str, Any]]: Workflow response or `None`
+        """
+
+        action = PullJobsAction(
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            subdomain=subdomain,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/workable/README.md b/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
similarity index 72%
rename from src/hrflow_connectors/connectors/boards/workable/README.md
rename to src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
index ae071bf78..73ffdb272 100644
--- a/src/hrflow_connectors/connectors/boards/workable/README.md
+++ b/src/hrflow_connectors/connectors/workable/docs/pull_jobs.md
@@ -1,12 +1,16 @@
-# Workable Connector
-**Workable is a recruiting software.**
+# Pull jobs
 
 `Workable` :arrow_right: `Hrflow.ai`
 
-## PullJobs
-`PullJobs` 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 ***Workable public endpoints***. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+**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` |
+
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -22,21 +26,18 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 
 ```python
+from hrflow_connectors import Workable
 from hrflow import Hrflow
-from hrflow_connectors.connectors.boards.workable import PullJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
-action = PullJobs(
+
+Workable.pull_jobs(
     subdomain="MY_SUBDOMAIN",
     hrflow_client=client,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=True,
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/workable/schemas.py b/src/hrflow_connectors/connectors/workable/schemas.py
new file mode 100644
index 000000000..d464cd4e7
--- /dev/null
+++ b/src/hrflow_connectors/connectors/workable/schemas.py
@@ -0,0 +1,20 @@
+from pydantic import BaseModel
+
+
+class WorkableJobModel(BaseModel):
+    title: str
+    shortcode: str
+    country: str
+    state: str
+    city: str
+    department: str
+    published_on: str
+    url: str
+    application_url: str
+    created_at: str
+    description: str
+    employment_type: str
+    industry: str
+    function: str
+    experience: str
+    education: str
diff --git a/src/hrflow_connectors/connectors/xml/README.md b/src/hrflow_connectors/connectors/xml/README.md
new file mode 100644
index 000000000..1dd34ef27
--- /dev/null
+++ b/src/hrflow_connectors/connectors/xml/README.md
@@ -0,0 +1,6 @@
+# XML Connector
+**Many feeds use the XML format. This connector groups together all the `Actions` that process an XML stream to retrieve jobs.**
+
+| Actions |
+| ------- |
+| [**Pull jobs**](docs/pull_jobs.md) |
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/xml/__init__.py b/src/hrflow_connectors/connectors/xml/__init__.py
new file mode 100644
index 000000000..3f6ed0590
--- /dev/null
+++ b/src/hrflow_connectors/connectors/xml/__init__.py
@@ -0,0 +1,1 @@
+from .connector import *
diff --git a/src/hrflow_connectors/connectors/boards/xml/actions.py b/src/hrflow_connectors/connectors/xml/actions.py
similarity index 56%
rename from src/hrflow_connectors/connectors/boards/xml/actions.py
rename to src/hrflow_connectors/connectors/xml/actions.py
index 5e573cc46..7ff4c36dc 100644
--- a/src/hrflow_connectors/connectors/boards/xml/actions.py
+++ b/src/hrflow_connectors/connectors/xml/actions.py
@@ -1,30 +1,30 @@
-from ....core.action import BoardAction
-from ....core.http import HTTPStream
-
-
 from pydantic import Field
 import xml.etree.ElementTree
 from typing import Iterator
+import requests
+
+from ...core.action import PullJobsBaseAction
 
 
-class XMLBoardAction(BoardAction, HTTPStream):
+class PullJobsAction(PullJobsBaseAction):
     xml_stream_url: str = Field(..., description="URL to XML Stream")
     job_list_xpath: str = Field(
         ..., description="XPath pointing to the job list in the XML stream"
     )
 
-    @property
-    def base_url(self):
-        return self.xml_stream_url
-
-    @property
-    def http_method(self):
-        return "GET"
-
     def pull(self) -> Iterator[xml.etree.ElementTree.Element]:
-        response = self.send_request()
-
-        if response.status_code >= 400:
+        # Prepare request
+        session = requests.Session()
+        pull_jobs_request = requests.Request()
+        pull_jobs_request.method = "GET"
+        pull_jobs_request.url = self.xml_stream_url
+        pull_jobs_request.auth = self.auth
+        prepared_request = pull_jobs_request.prepare()
+
+        # Send request
+        response = session.send(prepared_request)
+
+        if not response.ok:
             error_message = "Unable to pull the data ! Reason : `{}`"
             raise ConnectionError(error_message.format(response.content))
 
@@ -32,4 +32,4 @@ def pull(self) -> Iterator[xml.etree.ElementTree.Element]:
         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
\ No newline at end of file
+        return job_list
diff --git a/src/hrflow_connectors/connectors/xml/connector.py b/src/hrflow_connectors/connectors/xml/connector.py
new file mode 100644
index 000000000..205c06944
--- /dev/null
+++ b/src/hrflow_connectors/connectors/xml/connector.py
@@ -0,0 +1,44 @@
+from hrflow import Hrflow
+from typing import Optional, Dict, Any
+
+from ...core.connector import Connector
+from .actions import PullJobsAction
+
+
+class XML(Connector):
+    @staticmethod
+    def pull_jobs(
+        hrflow_client: Hrflow,
+        board_key: str,
+        xml_stream_url: str,
+        job_list_xpath: str,
+        **kwargs
+    ) -> Optional[Dict[str, Any]]:
+        """
+        `PullJobsAction` retrieves all jobs via an XML stream API. It adds all these jobs to a Hrflow.ai Board.
+
+        `XML Stream` -> `Hrflow.ai`
+
+        Args:
+            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
+            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 `[]`
+            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(
+            hrflow_client=hrflow_client,
+            board_key=board_key,
+            xml_stream_url=xml_stream_url,
+            job_list_xpath=job_list_xpath,
+            **kwargs
+        )
+        return action.execute()
diff --git a/src/hrflow_connectors/connectors/boards/xml/README.md b/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
similarity index 84%
rename from src/hrflow_connectors/connectors/boards/xml/README.md
rename to src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
index d339fea27..60a886f3f 100644
--- a/src/hrflow_connectors/connectors/boards/xml/README.md
+++ b/src/hrflow_connectors/connectors/xml/docs/pull_jobs.md
@@ -1,12 +1,9 @@
-# XML Connector
-**Many feeds use the XML format. This connector groups together all the `Actions` that process an XML stream to retrieve jobs.**
-
+# Pull jobs
 `XML Stream` :arrow_right: `Hrflow.ai`
 
-## SmartJobs
-`XMLBoardAction` retrieves all jobs via an ***XML stream*** API. It adds all these **jobs** to a ***Hrflow.ai Board***.
+`PullJobsAction` retrieves all jobs via an ***XML stream*** API. It adds all these **jobs** to a ***Hrflow.ai Board***.
 
-### Parameters
+## Parameters
 
 | Field | Type | Description |
 | ----- | ---- | ----------- |
@@ -24,18 +21,12 @@
 
 :red_circle: : *required* 
 
-### Example
+## Example
 Let's take the ***Samsic*** job stream as an example of an XML stream.
 ```python
-from hrflow import Hrflow
-
-from hrflow_connectors.core.auth import XSmartTokenAuth
-from hrflow_connectors.connectors.boards.smartrecruiters import SmartJobs
-from hrflow_connectors.utils.logger import get_logger_with_basic_config
-
-# We add a basic configuration to our logger to see the messages displayed in the standard output
-# This is not mandatory. It allows you to see what the connector is doing.
-logger = get_logger_with_basic_config()
+from hrflow_connectors import XML
+from hrflow import hrflow
+from hrflow_connectors import XSmartTokenAuth
 
 client = Hrflow(api_secret="MY_X-API-KEY", api_user="MY_X-USER-EMAIL")
 
@@ -155,14 +146,9 @@ def samsic_format(data):
 
     return job
 
-xml_stream_url = "https://cv.samsic-emploi.fr/media/flux/jobs.xml"
-job_list_xpath = "DataArea"
-
-logger = get_logger_with_basic_config()
-
-action = XMLBoardAction(
-    xml_stream_url=xml_stream_url,
-    job_list_xpath=job_list_xpath,
+XML.pull_jobs(
+    xml_stream_url="https://cv.samsic-emploi.fr/media/flux/jobs.xml",
+    job_list_xpath="DataArea",
     hrflow_client=client,
     board_key="MY_BOARD_KEY",
     hydrate_with_parsing=False,
@@ -171,5 +157,4 @@ action = XMLBoardAction(
     global_scope=globals(),
     local_scope=locals(),
 )
-action.execute()
 ```
\ No newline at end of file
diff --git a/src/hrflow_connectors/core/action.py b/src/hrflow_connectors/core/action.py
index 934c983bc..fa738ff0f 100644
--- a/src/hrflow_connectors/core/action.py
+++ b/src/hrflow_connectors/core/action.py
@@ -1,13 +1,14 @@
-import logging
-from ..utils.clean_text import remove_html_tags
-from ..utils.hrflow import find_element_in_list, Profile, Job
-from ..utils.hrflow import generate_workflow_response
-from ..utils.logger import get_logger
-
 from pydantic import BaseModel, Field
 from typing import List, Dict, Any, Iterator, TypeVar, Optional, Union, Tuple
 import itertools
 import xml.etree.ElementTree
+import html
+
+from ..utils.clean_text import remove_html_tags
+from ..utils.hrflow import find_element_in_list, Profile, Job
+from ..utils.hrflow import generate_workflow_response
+from ..utils.logger import get_logger
+from ..core.auth import Auth, NoAuth
 
 Hrflow = TypeVar("Hrflow")
 TalentDataType = Union[str, xml.etree.ElementTree.Element, Dict[str, Any]]
@@ -15,7 +16,21 @@
 logger = get_logger()
 
 
-class Action(BaseModel):
+class BaseAction(BaseModel):
+    """
+    Abstract class `Action`
+    """
+
+    hrflow_client: Hrflow = Field(
+        ...,
+        description="Hrflow client instance used to communicate with the Hrflow.ai API",
+    )
+
+    auth: Auth = Field(
+        NoAuth(),
+        description="Auth instance to identify and communicate with the platform",
+    )
+
     logics: List[str] = Field(
         [], description="Function names to apply as filter before pushing the data"
     )
@@ -156,7 +171,6 @@ def execute(self) -> Optional[Dict[str, Any]]:
         filtered_data = self.apply_logics(input_data)
         logger.info("Logics have been applied")
 
-        # connect each filtered_data to the format accepted by the pull function (destination, source, board)
         logger.info("Mapping format function...")
         output_data = map(self.format_switcher, filtered_data)
         logger.info("Format function has been mapped")
@@ -168,11 +182,65 @@ def execute(self) -> Optional[Dict[str, Any]]:
         logger.info("All has been done for this connector !")
 
 
-class BoardAction(Action):
-    hrflow_client: Hrflow = Field(
-        ...,
-        description="Hrflow client instance used to communicate with the Hrflow.ai API",
-    )
+class PullBaseAction(BaseAction):
+    """
+    Pull Action
+    """
+
+    def execute(self) -> Optional[Dict[str, Any]]:
+        logger.info("Start execution")
+
+        logger.info("Pulling data...")
+        input_data = self.pull()
+        logger.info("Data has been pulled")
+
+        logger.info("Mapping format function...")
+        formatted_data = map(self.format_switcher, input_data)
+        logger.info("Format function has been mapped")
+
+        logger.info("Applying logics...")
+        filtered_data = self.apply_logics(formatted_data)
+        logger.info("Logics have been applied")
+
+        logger.info("Pushing data...")
+        self.push(filtered_data)
+        logger.info("Data has been pushed")
+
+        logger.info("All has been done for this connector !")
+
+
+class PushBaseAction(BaseAction):
+    """
+    Push Action
+    """
+
+    def execute(self) -> Optional[Dict[str, Any]]:
+        logger.info("Start execution")
+
+        logger.info("Pulling data...")
+        input_data = self.pull()
+        logger.info("Data has been pulled")
+
+        logger.info("Applying logics...")
+        filtered_data = self.apply_logics(input_data)
+        logger.info("Logics have been applied")
+
+        logger.info("Mapping format function...")
+        formatted_data = map(self.format_switcher, filtered_data)
+        logger.info("Format function has been mapped")
+
+        logger.info("Pushing data...")
+        self.push(formatted_data)
+        logger.info("Data has been pushed")
+
+        logger.info("All has been done for this connector !")
+
+
+class PullJobsBaseAction(PullBaseAction):
+    """
+    Pull jobs from an external stream to Hrflow.ai
+    """
+
     board_key: str = Field(
         ..., description="Board key where the jobs to be added will be stored"
     )
@@ -277,7 +345,7 @@ def push(self, data: Iterator[Union[str, Dict[str, Any]]]):
             if response["code"] >= 400:
                 message = response["message"]
                 logger.error("Failed to push a job !")
-                raise ConnectionError("Failed to push ! Reason : `{}`".format(message))
+                raise RuntimeError("Failed to push ! Reason : `{}`".format(message))
 
     def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
         """
@@ -311,6 +379,8 @@ def hydrate_job_with_parsing(self, job: Dict[str, Any]) -> Dict[str, Any]:
         # Clean the `concatenated_str` by removing htlm tags
         logger.debug("Removing HTML tags in concatenated texts...")
         cleaned_str = remove_html_tags(concatenated_str)
+        cleaned_str = html.unescape(cleaned_str)
+        cleaned_str = cleaned_str.strip()
         logger.debug("HTML tags in concatenated texts have been removed")
 
         # If text is empty, the parsing can return an error
@@ -481,17 +551,23 @@ def get_all_references_from_stream(self) -> Iterator[str]:
             Iterator[str]: return all references
         """
         logger.info(f"Getting all references from the stream...")
+
         logger.info(f"Pulling all references from the stream")
         input_data = self.pull()
-        logger.info(f"Applying logics to all references from the stream")
-        filtered_data = self.apply_logics(input_data)
+
         logger.info(f"Formating all references from the stream")
-        formated_data = map(self.format_switcher, filtered_data)
+        formatted_data = map(self.format_switcher, input_data)
+
+        logger.info(f"Applying logics to all references from the stream")
+        filtered_data = self.apply_logics(formatted_data)
+
         logger.info(f"Keeping only reference from the stream")
-        references_iter = map(lambda job: job.get("reference"), formated_data)
+        references_iter = map(lambda job: job.get("reference"), filtered_data)
+
         references_without_none_iter = filter(
             lambda ref: ref is not None, references_iter
         )
+
         logger.info(f"All references from the stream have been got")
         return references_without_none_iter
 
@@ -530,15 +606,14 @@ def execute(self) -> Optional[Dict[str, Any]]:
         input_data = self.pull()
         logger.info("Data has been pulled")
 
-        logger.info("Applying logics...")
-        filtered_data = self.apply_logics(input_data)
-        logger.info("Logics have been applied")
-
-        # connect each filtered_data to the format accepted by the pull function (destination, source, board)
         logger.info("Mapping format function...")
-        formated_data = map(self.format_switcher, filtered_data)
+        formatted_data = map(self.format_switcher, input_data)
         logger.info("Format function has been mapped")
 
+        logger.info("Applying logics...")
+        filtered_data = self.apply_logics(formatted_data)
+        logger.info("Logics have been applied")
+
         logger.info(
             f"Archive the deleted jobs from stream : {self.archive_deleted_jobs_from_stream}"
         )
@@ -546,7 +621,7 @@ def execute(self) -> Optional[Dict[str, Any]]:
             self.check_deletion_references_from_stream()
 
         logger.info("Filtering the job to push")
-        unique_data_to_push = filter(self.check_reference_in_board, formated_data)
+        unique_data_to_push = filter(self.check_reference_in_board, filtered_data)
 
         logger.info(f"Hydrate job with parsing : {self.hydrate_with_parsing}")
         if self.hydrate_with_parsing:
@@ -563,11 +638,8 @@ def execute(self) -> Optional[Dict[str, Any]]:
         logger.info("All has been done for this connector !")
 
 
-class JobDestinationAction(Action):
-    hrflow_client: Hrflow = Field(
-        ...,
-        description="Hrflow client instance used to communicate with the Hrflow.ai API",
-    )
+class PushJobBaseAction(PushBaseAction):
+
     job: Job = Field(..., description="Job to push")
 
     def pull(self) -> Iterator[Dict[str, Any]]:
@@ -592,11 +664,7 @@ def execute(self):
         )
 
 
-class ProfileDestinationAction(Action):
-    hrflow_client: Hrflow = Field(
-        ...,
-        description="Hrflow client instance used to communicate with the Hrflow.ai API",
-    )
+class PushProfileBaseAction(PushBaseAction):
     profile: Profile = Field(..., description="Profile to push")
 
     def pull(self) -> Iterator[TalentDataType]:
@@ -625,23 +693,13 @@ def execute(self):
         )
 
 
-class CatchProfile(Action):
-    hrflow_client: Hrflow = Field(
-        ...,
-        description="Hrflow client instance used to communicate with the Hrflow.ai API",
-    )
+class CatchProfileBaseAction(BaseAction):
 
     source_key: str = Field(
         ..., description="Source key where the profiles to be added will be stored"
     )
 
-    request: Dict[str, Any] = Field(
-        ..., description="Body to format in HrFlow Profile"
-    )
-
-    file_field: str = Field(
-        ..., description="Name of the field containing the file"
-    )
+    request: Dict[str, Any] = Field(..., description="Body to format in HrFlow Profile")
 
     def execute(self):
         """
@@ -649,39 +707,23 @@ def execute(self):
         """
         logger.info("Start execution")
 
-        # connect each filtered_data to the format accepted by the pull function (destination, source, board)
-        logger.info("Mapping format function...")
-        # self.body: json
-        # output_data: [json]
-        output_data = self.format_switcher(self.request)
-        logger.info("Format function has been mapped")
+        logger.info("Format request...")
+        formatted_data = self.format_switcher(self.request)
+        logger.info("Request has been formatted")
 
         logger.info("Pushing data...")
-        self.push(output_data)
+        self.push(formatted_data)
         logger.info("Data has been pushed")
 
         logger.info("All has been done for this connector !")
-
         return generate_workflow_response(
             status_code=201, message="Profile successfully pushed"
         )
 
-    def format(self, request: str) -> Dict[str, Any]:
-        output_data = {
-            "source_key": self.source_key,
-            "profile_file": self.request[self.file_field]
-        }
-        return output_data
-
     def push(self, data: Dict[str, Any]):
-        logger.debug(
-            f"Parsing a profile to Hrflow Source `{self.source_key}`"
-        )
+        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 ConnectionError("Failed to push ! Reason : `{}`".format(message))
-
-    def pull(self) -> Iterator[Dict[str, Any]]:
-        pass
+            raise RuntimeError("Failed to push ! Reason : `{}`".format(message))
diff --git a/src/hrflow_connectors/core/auth.py b/src/hrflow_connectors/core/auth.py
index 0b8f8ed2f..a75551d1b 100644
--- a/src/hrflow_connectors/core/auth.py
+++ b/src/hrflow_connectors/core/auth.py
@@ -1,38 +1,33 @@
 import requests
-from typing import Union, Dict, Optional
-from pydantic import BaseModel, Field
+from typing import Union, Dict, Optional, Any
+from pydantic import BaseModel, Field, SecretStr
 from ..utils.logger import get_logger
 
 
 logger = get_logger()
 
 
-class Auth(BaseModel):
+class Auth(BaseModel, requests.auth.AuthBase):
     """
     Auth
 
     This generic class will propose a common interface to all Auth
     """
 
-    def update(
-        self,
-        params: Union[Dict[str, str], None] = None,
-        headers: Union[Dict[str, str], None] = None,
-        payload: Union[Dict[str, str], None] = None,
-        cookies: Union[Dict[str, str], None] = None,
-    ):
+    def __call__(
+        self, updatable_object: Union[requests.PreparedRequest, Dict[str, Any]]
+    ) -> Union[requests.PreparedRequest, Dict[str, Any]]:
         """
         Updating fields with authentication information
-
-        This class is responsible for placing the credentials in the right places
+        This class is responsible for placing the credentials in the right places.
 
         Args:
-            params (Union[Dict[str, str], None], optional): Params field. Defaults to None.
-            headers (Union[Dict[str, str], None], optional): Headers field. Defaults to None.
-            payload (Union[Dict[str, str], None], optional): Payload field. Defaults to None.
-            cookies (Union[Dict[str, str], None], optional): Cookies field. Defaults to None.
+            params (Union[requests.PreparedRequest, Dict[str, Any]]): updatable object
+
+        Return:
+            Union[requests.PreparedRequest, Dict[str, Any]]: updated object
         """
-        pass
+        return updatable_object
 
 
 class NoAuth(Auth):
@@ -52,9 +47,9 @@ class OAuth2PasswordCredentialsBody(Auth):
 
     access_token_url: str
     client_id: str
-    client_secret: str
+    client_secret: SecretStr
     username: str
-    password: str
+    password: SecretStr
 
     def get_access_token(self) -> str:
         """
@@ -67,35 +62,27 @@ def get_access_token(self) -> str:
         payload = dict()
         payload["grant_type"] = "password"
         payload["client_id"] = self.client_id
-        payload["client_secret"] = self.client_secret
+        payload["client_secret"] = self.client_secret.get_secret_value()
         payload["username"] = self.username
-        payload["password"] = self.password
+        payload["password"] = self.password.get_secret_value()
 
         logger.debug(
             f"Sending request to get access token (url=`{self.access_token_url}`)"
         )
         response = requests.post(self.access_token_url, data=payload)
 
-        if response.status_code != 200:
+        if not response.ok:
             logger.error("OAuth2 failed for getting access token !")
-            raise ConnectionError(
-                "OAuth2 failed ! Reason : `{}`".format(response.content)
-            )
+            raise RuntimeError("OAuth2 failed ! Reason : `{}`".format(response.content))
 
         logger.debug("The access token has been got")
         return response.json()["access_token"]
 
-    def update(
-        self,
-        params: Union[Dict[str, str], None] = None,
-        headers: Union[Dict[str, str], None] = None,
-        payload: Union[Dict[str, str], None] = None,
-        cookies: Union[Dict[str, str], None] = None,
-    ):
-        if headers is not None:
-            headers.update(
-                {"Authorization": "OAuth {}".format(self.get_access_token())}
-            )
+    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
+        access_token = self.get_access_token()
+        auth_header = {"Authorization": f"OAuth {access_token}"}
+        request.headers.update(auth_header)
+        return request
 
 
 class XAPIKeyAuth(Auth):
@@ -109,17 +96,12 @@ class XAPIKeyAuth(Auth):
         "X-API-KEY",
         description="Name of the API key that will be used as a header in the request. Default value : `X-API-KEY`",
     )
-    value: str = Field(..., description="API key value")
+    value: SecretStr = Field(..., description="API key value")
 
-    def update(
-        self,
-        params: Union[Dict[str, str], None] = None,
-        headers: Union[Dict[str, str], None] = None,
-        payload: Union[Dict[str, str], None] = None,
-        cookies: Union[Dict[str, str], None] = None,
-    ):
-        if headers is not None:
-            headers.update({self.name: self.value})
+    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
+        auth_header = {self.name: self.value.get_secret_value()}
+        request.headers.update(auth_header)
+        return request
 
 
 class AuthorizationAuth(XAPIKeyAuth):
@@ -139,4 +121,70 @@ class XSmartTokenAuth(XAPIKeyAuth):
     Auth used to authenticate to SmartRecruiter with a token
     """
 
-    name: str = Field("X-SmartToken", const=True)
\ No newline at end of file
+    name: str = Field("X-SmartToken", const=True)
+
+
+class OAuth2EmailPasswordBody(Auth):
+    """
+    OAuth2 by using a password and email to send to a sginin endpoint used to get the "access token".
+    """
+
+    access_token_url: str
+    email: str
+    password: SecretStr
+
+    def get_access_token(self):
+
+        payload = dict()
+        payload["email"] = self.email
+        payload["password"] = self.password.get_secret_value()
+        logger.debug(
+            f"Sending request to get access token (url=`{self.access_token_url}`)"
+        )
+        response = requests.post(self.access_token_url, data=payload)
+
+        if not response.ok:
+            logger.error("Sign in Failure !")
+            raise RuntimeError("Signin failed ! Reason : `{}`".format(response.content))
+
+        logger.debug("The access token has been got")
+        return response.json()["access_token"]
+
+    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
+
+        access_token = self.get_access_token()
+        auth_header = {"Authorization": f"{access_token}"}
+        request.headers.update(auth_header)
+        return request
+
+
+class XTaleezAuth(XAPIKeyAuth):
+    """
+    XTaleezAuth
+
+    Auth used to authenticate to Taleez with a token
+    """
+
+    name: str = Field("X-taleez-api-secret", const=True)
+
+
+class MonsterBodyAuth(Auth):
+    """
+    MonsterBodyAuth
+
+    Credentials are going to be stored on the body.
+    """
+
+    username: str = Field(description="Monster username")
+    password: str = Field(description="Monster password")
+
+    def __call__(
+        self, updatable_object: requests.PreparedRequest
+    ) -> requests.PreparedRequest:
+        string_body = updatable_object.body.decode()
+        formatted_body = string_body.format(
+            username=self.username, password=self.password
+        )
+        encoded_body = formatted_body.encode("utf-8")
+        updatable_object.body = encoded_body
+        return updatable_object
diff --git a/src/hrflow_connectors/core/connector.py b/src/hrflow_connectors/core/connector.py
new file mode 100644
index 000000000..a3ba0c95c
--- /dev/null
+++ b/src/hrflow_connectors/core/connector.py
@@ -0,0 +1,42 @@
+from typing import Optional, Dict, Any
+
+
+class Connector:
+    """
+    Abstract class `Connector`
+    """
+
+    @staticmethod
+    def pull_jobs(*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]]:
+        """
+        Pull profiles
+        """
+        raise NotImplementedError("`pull_profiles` method is not implemented !")
+
+    @staticmethod
+    def push_job(*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]]:
+        """
+        Push profile
+        """
+        raise NotImplementedError("`push_profile` method is not implemented !")
+
+    @staticmethod
+    def catch_profile(*args, **kwargs) -> Optional[Dict[str, Any]]:
+        """
+        Catch profile
+        """
+        raise NotImplementedError("`catch_profile` method is not implemented !")
diff --git a/src/hrflow_connectors/core/http.py b/src/hrflow_connectors/core/http.py
deleted file mode 100644
index 94cc40351..000000000
--- a/src/hrflow_connectors/core/http.py
+++ /dev/null
@@ -1,129 +0,0 @@
-from logging import log
-import requests
-from pydantic import BaseModel, Field
-from typing import Optional, Dict, Any, Union, TypeVar
-
-
-from .auth import Auth, NoAuth
-from ..utils.logger import get_logger
-
-
-logger = get_logger()
-
-
-class HTTPStream(BaseModel):
-    """
-    HTTPStream
-    Abstract Class
-
-    To send a request with a JSON body, you must set the `content-type` header to `application/json`.
-    It is important to write the header key `content-type` in lower case.
-    """
-
-    auth: Auth = NoAuth()
-
-    params: Dict[str, str] = dict()
-    headers: Dict[str, str] = dict()
-    payload: Union[None, str, Dict[str, Any]] = None
-    cookies: Dict[str, str] = dict()
-
-    @property
-    def base_url(self) -> Optional[str]:
-        return None
-
-    @property
-    def http_method(self):
-        return "GET"
-
-    def path(self):
-        return ""
-
-    def build_request_params(self):
-        pass
-
-    def build_request_headers(self):
-        pass
-
-    def build_request_payload(self):
-        pass
-
-    def build_request_cookies(self):
-        pass
-
-    def get_content_type(self) -> Optional[str]:
-        """
-        Get Content-Type of the body request
-
-        This function find the "Content-Type" in Header
-        and it is NOT case sensitive.
-        So, it can find "content-type" or "CONTENT-TYPE" as well.
-
-        Returns:
-            str: return content-type or None.
-        """
-        for key, value in self.headers.items():
-            if key.lower() == "content-type":
-                return value
-
-    def send_request(self) -> requests.Response:
-        """
-        Send a HTTP request
-
-        Returns:
-            requests.Response: request response
-        """
-        if self.base_url is None:
-            logger.error("Base URL `base_url` (property function) is not defined !")
-            raise ConnectionError(
-                "Base URL `base_url` (property function) is not defined !"
-            )
-
-        # Build the request
-        url = self.base_url + self.path()
-        logger.debug(f"URL use in HTTPStream : `{url}`")
-
-        logger.debug("Preparing request...")
-        logger.debug("Preparing request params")
-        self.build_request_params()
-        logger.debug("Preparing request headers")
-        self.build_request_headers()
-        logger.debug("Preparing request payload")
-        self.build_request_payload()
-        logger.debug("Preparing request cookies")
-        self.build_request_cookies()
-        logger.debug("The request has been prepared")
-
-        # Add the auth property in the different sections
-        logger.debug("Adding auth properties in the different request fields...")
-        self.auth.update(
-            params=self.params,
-            headers=self.headers,
-            cookies=self.cookies,
-            payload=self.payload,
-        )
-        logger.debug("Auth properties have been added in the request fields")
-
-        params = dict()
-        params["method"] = self.http_method
-        params["url"] = url
-        params["params"] = self.params
-        params["headers"] = self.headers
-        params["cookies"] = self.cookies
-
-        logger.debug(f"A `{self.http_method}` request will be sent to this URL `{url}`")
-
-        # Check if request is a JSON application
-        content_type = self.get_content_type()
-        if (
-            content_type is not None
-            and "application/json" in content_type.lower()
-            and isinstance(self.payload, dict)
-        ):
-            logger.debug("The payload has been detected as JSON")
-            params["json"] = self.payload
-        else:
-            logger.debug("The payload has been detected as String or Dataform")
-            params["data"] = self.payload
-
-        logger.debug("Sending the request now")
-        return requests.request(**params)
diff --git a/src/hrflow_connectors/utils/adress_to_lat_long.py b/src/hrflow_connectors/utils/adress_to_lat_long.py
index 16b870c75..8618524b6 100644
--- a/src/hrflow_connectors/utils/adress_to_lat_long.py
+++ b/src/hrflow_connectors/utils/adress_to_lat_long.py
@@ -44,8 +44,8 @@ def get_cities_code_lat_long_mapping():
     ville_code["name"] = "cities_codes"
     data = pkgutil.get_data(__name__, "../data/french_citycode_geo_mapping.csv")
     str_data = data.decode("utf-8")
-    for line in str_data.split('\n'):
-        row = line.split(',')
+    for line in str_data.split("\n"):
+        row = line.split(",")
         code_str = str(row[0])
         # fill the uncompleted zip codes with zeros. (1 400 => 01 400)
         code = code_str.zfill(5)
@@ -69,9 +69,9 @@ def get_cities_names_lat_long_mapping():
     ville_name["name"] = "cities_name"
     data = pkgutil.get_data(__name__, "../data/french_cityname_geo_mapping.csv")
     str_data = data.decode("utf-8")
-    for line in str_data.split('\n'):
+    for line in str_data.split("\n"):
         # .lower() for case sensitivity and accent_replacer(s) for accents.
-        row = line.split(',')
+        row = line.split(",")
         ville_name[accent_replacer(row[0].lower())] = (row[1], row[2])
 
     return ville_name
@@ -92,8 +92,8 @@ def get_departments_codes_lat_long_mapping():
     departement["name"] = "departments_codes"
     data = pkgutil.get_data(__name__, "../data/french_departement_geo_mapping.csv")
     str_data = data.decode("utf-8")
-    for line in str_data.split('\n'):
-        row = line.split(',')
+    for line in str_data.split("\n"):
+        row = line.split(",")
         departement[row[0]] = (row[1], row[2])
 
     return departement
diff --git a/src/hrflow_connectors/utils/datetime_converter.py b/src/hrflow_connectors/utils/datetime_converter.py
index bf1623bdf..65531fad4 100644
--- a/src/hrflow_connectors/utils/datetime_converter.py
+++ b/src/hrflow_connectors/utils/datetime_converter.py
@@ -2,22 +2,28 @@
 import re
 from typing import Union
 
+
 class ParseError(ValueError):
     """
     Parse Error
     """
-    def __init__(self, message):            
+
+    def __init__(self, message):
         super().__init__(message)
 
+
 class DateFormatError(ParseError):
     """
     Date Format Error
     """
-    def __init__(self, value):            
-        super().__init__("Le format de la date ne respecte pas la norme ISO8601 : `{}`".format(value))
+
+    def __init__(self, value):
+        super().__init__(
+            "Le format de la date ne respecte pas la norme ISO8601 : `{}`".format(value)
+        )
 
 
-def any_to_int(any : Union[None, str]) -> int:
+def any_to_int(any: Union[None, str]) -> int:
     """
     Convert int string to integer. if this value is `None`, return `0`
     Args:
@@ -29,12 +35,13 @@ def any_to_int(any : Union[None, str]) -> int:
         return 0
     return int(any)
 
+
 def from_str_to_datetime(datetime_str: str) -> datetime.datetime:
     """
     Convert string to `datetime.datetime`.
     The date must respect ISO8601 format.
     Args:
-        datetime_str (str): date formated with the norme ISO8601
+        datetime_str (str): date formatted with the norme ISO8601
     Raises:
         DateFormatError: "Le format de la date ne respecte pas la norme ISO8601 : ..."
     Returns:
@@ -48,12 +55,16 @@ def from_str_to_datetime(datetime_str: str) -> datetime.datetime:
     # Extract value from string
     ## Datetime
     year, month, day = match.group("year", "month", "day")
-    hour, minute, second, millisecond = match.group("hour", "minute", "second", "millisecond")
+    hour, minute, second, millisecond = match.group(
+        "hour", "minute", "second", "millisecond"
+    )
 
     ## TimeZone
-    tz_field_in_str = match.group("tz")# example : +02:00
+    tz_field_in_str = match.group("tz")  # example : +02:00
     tz_symbol = match.group("tz_symbol")
-    tz_hour, tz_minute, tz_second, tz_millisecond = match.group("tz_hour", "tz_minute", "tz_second", "tz_millisecond")
+    tz_hour, tz_minute, tz_second, tz_millisecond = match.group(
+        "tz_hour", "tz_minute", "tz_second", "tz_millisecond"
+    )
 
     # Pad right with 0 the fields millisecond and tz_millisecond
     # "123" => "123000"
@@ -65,23 +76,44 @@ def from_str_to_datetime(datetime_str: str) -> datetime.datetime:
     # Convert each field to int
     try:
         year, month, day = any_to_int(year), any_to_int(month), any_to_int(day)
-        hour, minute, second, millisecond = any_to_int(hour), any_to_int(minute), any_to_int(second), any_to_int(millisecond)
-        tz_hour, tz_minute, tz_second, tz_millisecond = any_to_int(tz_hour), any_to_int(tz_minute), any_to_int(tz_second), any_to_int(tz_millisecond)
+        hour, minute, second, millisecond = (
+            any_to_int(hour),
+            any_to_int(minute),
+            any_to_int(second),
+            any_to_int(millisecond),
+        )
+        tz_hour, tz_minute, tz_second, tz_millisecond = (
+            any_to_int(tz_hour),
+            any_to_int(tz_minute),
+            any_to_int(tz_second),
+            any_to_int(tz_millisecond),
+        )
     except ValueError:
         raise DateFormatError(datetime_str)
 
     # Convert to Object
-    delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute, seconds=tz_second, microseconds=tz_millisecond)
+    delta = datetime.timedelta(
+        hours=tz_hour, minutes=tz_minute, seconds=tz_second, microseconds=tz_millisecond
+    )
     if tz_symbol == "-":
         # when -HH:MM
         delta = -1 * delta
-    
+
     timezone = datetime.timezone(delta)
 
     if tz_field_in_str is None:
         timezone = None
-    
+
     try:
-        return datetime.datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second, microsecond=millisecond, tzinfo=timezone)
+        return datetime.datetime(
+            year=year,
+            month=month,
+            day=day,
+            hour=hour,
+            minute=minute,
+            second=second,
+            microsecond=millisecond,
+            tzinfo=timezone,
+        )
     except ValueError:
-        raise DateFormatError(datetime_str)
\ No newline at end of file
+        raise DateFormatError(datetime_str)
diff --git a/src/hrflow_connectors/utils/hrflow.py b/src/hrflow_connectors/utils/hrflow.py
index 021d1063e..db1a27385 100644
--- a/src/hrflow_connectors/utils/hrflow.py
+++ b/src/hrflow_connectors/utils/hrflow.py
@@ -86,10 +86,10 @@ def get_job(self, board_to_listen: List[str] = None) -> Optional[Job]:
         """
         Get job
 
-        If the parser does not find `job` object, then it will return `None`
+        If the parser does not find `Job` object, then it will return `None`
 
         Args:
-            source_to_listen (List[str], optional): list of board keys to listen. Defaults to None if you want to listen all available board.
+            board_to_listen (List[str], optional): list of board keys to listen. Defaults to None if you want to listen all available boards.
 
         Returns:
             Optional[Job]: `Job` object or `None` if not found
@@ -118,4 +118,4 @@ def get_profile(self, source_to_listen: List[str] = None) -> Optional[Profile]:
             profile_obj = Profile.parse_obj(profile_dict)
             if source_to_listen is None or profile_obj.source.key in source_to_listen:
                 return profile_obj
-        return None
\ No newline at end of file
+        return None
diff --git a/src/hrflow_connectors/utils/logger.py b/src/hrflow_connectors/utils/logger.py
index e1ae70471..4ed2e946b 100644
--- a/src/hrflow_connectors/utils/logger.py
+++ b/src/hrflow_connectors/utils/logger.py
@@ -46,4 +46,4 @@ def get_logger_with_basic_config(
 
     logger.addHandler(stdout_handler)
 
-    return logger
\ No newline at end of file
+    return logger
diff --git a/src/hrflow_connectors/utils/timedelta_converter.py b/src/hrflow_connectors/utils/timedelta_converter.py
index 3f6d648be..9df0e12c7 100644
--- a/src/hrflow_connectors/utils/timedelta_converter.py
+++ b/src/hrflow_connectors/utils/timedelta_converter.py
@@ -44,7 +44,7 @@ def from_str_to_timedelta(timedelta_str: str) -> datetime.timedelta:
     The date must respect the format `[[-]DD days, ]HH:MM:SS[.mmmmmm]`.
     Examples : `-1804 days, 15:54:03.999990` or `8:02:12`
     Args:
-        datetime_str (str): formated timedelta
+        datetime_str (str): formatted timedelta
     Raises:
         TimeDeltaFormatError: Le format de la durée ne respecte pas le format `[[-]DD days, ]HH:MM:SS[.mmmmmm]`. Example : `-1804 days, 15:54:03.999990` or `8:02:12` : ..."
     Returns:
@@ -87,4 +87,4 @@ def from_str_to_timedelta(timedelta_str: str) -> datetime.timedelta:
             microseconds=millisecond,
         )
     except ValueError:
-        raise TimeDeltaFormatError(timedelta_str)
\ No newline at end of file
+        raise TimeDeltaFormatError(timedelta_str)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index e5164b310..33de0fa59 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,6 @@
 import logging
 from typing import Dict, Any, Callable
 from hrflow import Hrflow
-from webdriver_manager.chrome import ChromeDriverManager
 
 from hrflow_connectors.utils.logger import get_logger_with_basic_config
 
@@ -50,17 +49,6 @@ def hrflow_client_func(portal_name="dev-demo"):
     return hrflow_client_func
 
 
-@pytest.fixture(scope="session")
-def webdriver_path() -> str:
-    """
-    Get the Chrome Web Driver path in using `webdriver_manager`
-
-    Returns:
-        str: Chrome Web Driver path
-    """
-    return ChromeDriverManager().install()
-
-
 @pytest.fixture(scope="session")
 def logger() -> logging.Logger:
     """
diff --git a/tests/connectors/boards/careerbuilder/test_career_jobs.py b/tests/connectors/boards/careerbuilder/test_career_jobs.py
deleted file mode 100644
index 171a06892..000000000
--- a/tests/connectors/boards/careerbuilder/test_career_jobs.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from hrflow_connectors.connectors.boards.careerbuilder import CareerBuilderFeed
-
-
-def test_CareerJobs(logger, hrflow_client, webdriver_path):
-    
-    action = CareerBuilderFeed(
-        executable_path=webdriver_path,
-        domain='fr',
-        hrflow_client=hrflow_client("dev-demo"),
-        maximum_page_num=1,
-        job_search='Data Scientist',
-        job_location='Paris',
-        hydrate_with_parsing=True,
-        board_key="4dda21ae8a3bd3817f0f98ee716dad590c4be87e",
-        )
-    action.execute()
\ No newline at end of file
diff --git a/tests/connectors/boards/craigslist/test_craiglist_jobs.py b/tests/connectors/boards/craigslist/test_craiglist_jobs.py
deleted file mode 100644
index 2f6cd888b..000000000
--- a/tests/connectors/boards/craigslist/test_craiglist_jobs.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from hrflow_connectors.connectors.boards.craigslist.actions import CraigslistFeed
-
-
-def test_CraigslistJobs(logger, hrflow_client, webdriver_path):
-    action = CraigslistFeed(
-        executable_path=webdriver_path,
-        subdomain="Paris",
-        hrflow_client=hrflow_client("dev-demo"),
-        board_key="fc197c369ed9a8d35041961b268076885b2ea6f2",
-        hydrate_with_parsing=True,
-    )
-    action.execute()
diff --git a/tests/connectors/boards/greenhouse/test_get_all_jobs/test_get_all_jobs.py b/tests/connectors/boards/greenhouse/test_get_all_jobs/test_get_all_jobs.py
deleted file mode 100644
index ec7c61570..000000000
--- a/tests/connectors/boards/greenhouse/test_get_all_jobs/test_get_all_jobs.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from hrflow_connectors.connectors.boards.greenhouse import GetAllJobs
-
-
-def test_GetAllJobs(logger, hrflow_client):
-    action = GetAllJobs(
-        board_token='vaulttec',
-        hrflow_client=hrflow_client("dev-demo"),
-        board_key="61abdf5e11a8b75f2f5d1789339ee3638d4c6197",
-        hydrate_with_parsing=True,
-    )
-    action.execute()
\ No newline at end of file
diff --git a/tests/connectors/boards/indeed/test_indeed_feed.py b/tests/connectors/boards/indeed/test_indeed_feed.py
deleted file mode 100644
index 5134bbcef..000000000
--- a/tests/connectors/boards/indeed/test_indeed_feed.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from hrflow_connectors.connectors.boards.indeed import IndeedFeed
-
-
-def test_IndeedFeed(logger, hrflow_client, webdriver_path):
-    action = IndeedFeed(
-        executable_path=webdriver_path,
-        max_page=2,
-        subdomain="fr",
-        hrflow_client=hrflow_client("dev-demo"),
-        job_search="Software Engineer",
-        job_location="Paris",
-        board_key="5865a71e45b94e29f7c1c97d71479ef2757df414",
-        hydrate_with_parsing=True,
-    )
-    action.execute()
diff --git a/tests/connectors/boards/monster/test_push_job.py b/tests/connectors/boards/monster/test_push_job.py
deleted file mode 100644
index c918e5b03..000000000
--- a/tests/connectors/boards/monster/test_push_job.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import pytest
-
-from hrflow_connectors.connectors.boards.monster.actions import PushJob
-from hrflow_connectors.utils.hrflow import Job, Board
-
-
-def test_PushJob(logger, hrflow_client, credentials):
-    job = Job(
-        key="230f841bc57774bde1ad67563d9f092a788364e0",
-        board=Board(key="d31518949ed1f88ac61308670324f93bc0f9374d"),
-    )
-    action = PushJob(
-        username=credentials["monster"]["job"]["username"],
-        password=credentials["monster"]["job"]["password"],
-        subdomain="gateway",
-        board_key=job.board.key,
-        hrflow_client=hrflow_client(),
-        archive_deleted_jobs_from_stream=False,
-        job=job,
-    )
-    response = action.execute()
-    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/recruitee/__init__.py b/tests/connectors/boards/recruitee/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/boards/sapsuccessfactors/__init__.py b/tests/connectors/boards/sapsuccessfactors/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/boards/smartrecruiters/__init__.py b/tests/connectors/boards/smartrecruiters/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/boards/workable/__init__.py b/tests/connectors/boards/workable/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/boards/workable/test_pull_jobs.py b/tests/connectors/boards/workable/test_pull_jobs.py
deleted file mode 100644
index 868a88f9c..000000000
--- a/tests/connectors/boards/workable/test_pull_jobs.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from hrflow_connectors.connectors.boards.workable import PullJobs
-
-
-def test_PullJobs(logger, hrflow_client):
-    action = PullJobs(
-        subdomain='eurostar',
-        hrflow_client=hrflow_client("dev-demo"),
-        board_key="12dc2abb0d952bfcbbc6259b76c0631754459787",
-        hydrate_with_parsing=True,
-    )
-    action.execute()
\ No newline at end of file
diff --git a/tests/connectors/boards/xml/__init__.py b/tests/connectors/boards/xml/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/hrflow_connectors/connectors/boards/__init__.py b/tests/connectors/breezyhr/__init__.py
similarity index 100%
rename from src/hrflow_connectors/connectors/boards/__init__.py
rename to tests/connectors/breezyhr/__init__.py
diff --git a/tests/connectors/breezyhr/test_pull_jobs.py b/tests/connectors/breezyhr/test_pull_jobs.py
new file mode 100644
index 000000000..748d93f60
--- /dev/null
+++ b/tests/connectors/breezyhr/test_pull_jobs.py
@@ -0,0 +1,24 @@
+import pytest
+
+from hrflow_connectors import OAuth2EmailPasswordBody
+from hrflow_connectors import Breezyhr
+
+
+@pytest.fixture
+def auth(credentials):
+    auth = OAuth2EmailPasswordBody(
+        access_token_url = "https://api.breezy.hr/v3/signin",
+        email = credentials["breezyhr"]["email"],
+        password= credentials["breezyhr"]["password"]
+    )
+    return auth
+
+
+def test_PullJobsAction(logger, auth, hrflow_client):
+    Breezyhr.pull_jobs(
+        auth=auth,
+        hrflow_client=hrflow_client("dev-demo"),
+        board_key="fa06643e19d811e8da472858c07f8bbbd954dfd0",
+        hydrate_with_parsing=True,
+        company_name="Hrflow.ai"
+    )
diff --git a/tests/connectors/breezyhr/test_push_profile.py b/tests/connectors/breezyhr/test_push_profile.py
new file mode 100644
index 000000000..b4e7c58a2
--- /dev/null
+++ b/tests/connectors/breezyhr/test_push_profile.py
@@ -0,0 +1,30 @@
+import pytest
+
+from hrflow_connectors import OAuth2EmailPasswordBody
+from hrflow_connectors import Breezyhr
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+
+@pytest.fixture
+def auth(credentials):
+    auth = OAuth2EmailPasswordBody(
+        access_token_url="https://api.breezy.hr/v3/signin",
+        email=credentials["breezyhr"]["email"],
+        password=credentials["breezyhr"]["password"],
+    )
+    return auth
+
+
+def test_PushProfileBaseAction(logger, auth, hrflow_client):
+    profile = Profile(
+        key="a7e7fa4af68e7c450f2b708d14a3bda9b6ade5d9",
+        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+    )
+    response = Breezyhr.push_profile(
+        auth=auth,
+        position_id="edc0330a0f3801",
+        company_name="Hrflow.ai",
+        hrflow_client=hrflow_client("dev-demo"),
+        profile=profile,
+    )
+    assert response.get("status_code") == 201
diff --git a/src/hrflow_connectors/connectors/boards/monster/__init__.py b/tests/connectors/ceridian/__init__.py
similarity index 100%
rename from src/hrflow_connectors/connectors/boards/monster/__init__.py
rename to tests/connectors/ceridian/__init__.py
diff --git a/tests/connectors/boards/ceridian/test_pull_jobs.py b/tests/connectors/ceridian/test_pull_jobs.py
similarity index 71%
rename from tests/connectors/boards/ceridian/test_pull_jobs.py
rename to tests/connectors/ceridian/test_pull_jobs.py
index 6fecc7ec2..a8f5ce9a9 100644
--- a/tests/connectors/boards/ceridian/test_pull_jobs.py
+++ b/tests/connectors/ceridian/test_pull_jobs.py
@@ -1,12 +1,11 @@
-from hrflow_connectors.connectors.boards.ceridian import PullJobs
+from hrflow_connectors import Ceridian
 
 
 def test_PullJobs(logger, hrflow_client):
-    action = PullJobs(
+    Ceridian.pull_jobs(
         subdomain='ustest61-services',
         client_name_space = 'ddn',
         hrflow_client=hrflow_client("dev-demo"),
         board_key="f54e75c62da8f3273789e10a3c89d9877b72c66d",
         hydrate_with_parsing=True,
     )
-    action.execute()
\ No newline at end of file
diff --git a/src/hrflow_connectors/connectors/destinations/__init__.py b/tests/connectors/crosstalent/__init__.py
similarity index 100%
rename from src/hrflow_connectors/connectors/destinations/__init__.py
rename to tests/connectors/crosstalent/__init__.py
diff --git a/tests/connectors/crosstalent/test_auth.py b/tests/connectors/crosstalent/test_auth.py
new file mode 100644
index 000000000..6e0c0fda5
--- /dev/null
+++ b/tests/connectors/crosstalent/test_auth.py
@@ -0,0 +1,23 @@
+import pytest
+
+from hrflow_connectors import OAuth2PasswordCredentialsBody
+
+
+@pytest.fixture
+def auth(credentials):
+    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"],
+    )
+    return auth
+
+
+def test_Auth(auth):
+    access_token = auth.get_access_token()
+    assert isinstance(access_token, str)
+    assert access_token != ""
\ No newline at end of file
diff --git a/tests/connectors/boards/crosstalent/test_get_all_jobs.py b/tests/connectors/crosstalent/test_pull_jobs.py
similarity index 65%
rename from tests/connectors/boards/crosstalent/test_get_all_jobs.py
rename to tests/connectors/crosstalent/test_pull_jobs.py
index fc155d3d2..cd3b88e56 100644
--- a/tests/connectors/boards/crosstalent/test_get_all_jobs.py
+++ b/tests/connectors/crosstalent/test_pull_jobs.py
@@ -1,7 +1,7 @@
 import pytest
 
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.connectors.boards.crosstalent import GetAllJobs
+from hrflow_connectors import OAuth2PasswordCredentialsBody
+from hrflow_connectors import Crosstalent
 
 
 @pytest.fixture
@@ -18,18 +18,11 @@ def auth(credentials):
     return auth
 
 
-def test_Auth(auth):
-    access_token = auth.get_access_token()
-    assert isinstance(access_token, str)
-    assert access_token != ""
-
-
-def test_GetAllJobs(logger, auth, hrflow_client):
-    action = GetAllJobs(
+def test_PullJobsAction(logger, auth, hrflow_client):
+    Crosstalent.pull_jobs(
         auth=auth,
         subdomain="vulcain-eng--recette.my",
         hrflow_client=hrflow_client("dev-demo"),
         board_key="8eba188e1af123a9818d00974ff37b943b7d54f4",
         hydrate_with_parsing=True,
-    )
-    action.execute()
\ No newline at end of file
+    )
\ No newline at end of file
diff --git a/tests/connectors/destinations/crosstalent/test_push_profile.py b/tests/connectors/crosstalent/test_push_profile.py
similarity index 78%
rename from tests/connectors/destinations/crosstalent/test_push_profile.py
rename to tests/connectors/crosstalent/test_push_profile.py
index 6b3d40340..306b8d63d 100644
--- a/tests/connectors/destinations/crosstalent/test_push_profile.py
+++ b/tests/connectors/crosstalent/test_push_profile.py
@@ -1,14 +1,13 @@
 import pytest
 
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.connectors.destinations.crosstalent import PushProfile
+from hrflow_connectors import OAuth2PasswordCredentialsBody
+from hrflow_connectors import Crosstalent
 from hrflow_connectors.utils.hrflow import Profile, Source
 
 
 @pytest.fixture
 def auth(credentials):
     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"],
@@ -19,16 +18,15 @@ def auth(credentials):
     return auth
 
 
-def test_PushProfile(logger, auth, hrflow_client):
+def test_PushProfileBaseAction(logger, auth, hrflow_client):
     profile = Profile(
         key="ea5704b959c5e53aaef65c04ef5018ae1fee1a77",
         source=Source(key="15517d70b0870e4cf431eefd78f8b39cff5607e8"),
     )
-    action = PushProfile(
+    response = Crosstalent.push_profile(
         auth=auth,
         subdomain="vulcain-eng--recette.my",
         hrflow_client=hrflow_client("vulcain"),
         profile=profile,
     )
-    response = action.execute()
     assert response.get("status_code") == 201
diff --git a/tests/connectors/destinations/__init__.py b/tests/connectors/destinations/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/destinations/crosstalent/__init__.py b/tests/connectors/destinations/crosstalent/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/destinations/flatchr/__init__.py b/tests/connectors/destinations/flatchr/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/destinations/flatchr/test_enrichment_profile.py b/tests/connectors/destinations/flatchr/test_enrichment_profile.py
deleted file mode 100644
index 9db5324f5..000000000
--- a/tests/connectors/destinations/flatchr/test_enrichment_profile.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import pytest
-
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.flatchr import EnrichProfile
-from hrflow_connectors.utils.hrflow import Profile, Source
-
-
-@pytest.fixture
-def auth(credentials):
-    auth = AuthorizationAuth(value=credentials["flatchr"]["x-api-key"])
-    return auth
-
-
-def test_EnrichProfile(logger, auth, hrflow_client):
-    profile = Profile(
-        key="5746beca5e941a5a55706efd9adfce31f59e6e2b",
-        source=Source(key="d42eed17626b7ae3dc05efca363788caef91d44b"),
-    )
-    action = EnrichProfile(
-        auth=auth,
-        subdomain="api",
-        hrflow_client=hrflow_client(),
-        profile=profile,
-        vacancy="k0M5O9ylKZnxbQBy",
-        compagny="LEZBvp5b4LdMoVmg",
-    )
-    response = action.execute()
-    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/destinations/greenhouse/__init__.py b/tests/connectors/destinations/greenhouse/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/connectors/destinations/recruitee/__init__.py b/tests/connectors/destinations/recruitee/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/hrflow_connectors/connectors/destinations/monster/__init__.py b/tests/connectors/flatchr/__init__.py
similarity index 100%
rename from src/hrflow_connectors/connectors/destinations/monster/__init__.py
rename to tests/connectors/flatchr/__init__.py
diff --git a/tests/connectors/destinations/flatchr/test_push_profile.py b/tests/connectors/flatchr/test_push_profile.py
similarity index 66%
rename from tests/connectors/destinations/flatchr/test_push_profile.py
rename to tests/connectors/flatchr/test_push_profile.py
index 9a14c9759..92c9b1177 100644
--- a/tests/connectors/destinations/flatchr/test_push_profile.py
+++ b/tests/connectors/flatchr/test_push_profile.py
@@ -1,7 +1,7 @@
 import pytest
 
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.flatchr import PushProfile
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors import Flatchr
 from hrflow_connectors.utils.hrflow import Profile, Source
 
 
@@ -11,17 +11,16 @@ def auth(credentials):
     return auth
 
 
-def test_PushProfile(logger, auth, hrflow_client):
+def test_PushProfileBaseAction(logger, auth, hrflow_client):
     profile = Profile(
         key="5746beca5e941a5a55706efd9adfce31f59e6e2b",
         source=Source(key="d42eed17626b7ae3dc05efca363788caef91d44b"),
     )
-    action = PushProfile(
+    response = Flatchr.push_profile(
         auth=auth,
-        subdomain="careers",
         hrflow_client=hrflow_client(),
         profile=profile,
         vacancy="k0M5O9ylKZnxbQBy",
+        company="LEZBvp5b4LdMoVmg",
     )
-    response = action.execute()
     assert response.get("status_code") == 201
diff --git a/tests/connectors/boards/__init__.py b/tests/connectors/greenhouse/__init__.py
similarity index 100%
rename from tests/connectors/boards/__init__.py
rename to tests/connectors/greenhouse/__init__.py
diff --git a/tests/connectors/greenhouse/test_pull_jobs.py b/tests/connectors/greenhouse/test_pull_jobs.py
new file mode 100644
index 000000000..b77905fd1
--- /dev/null
+++ b/tests/connectors/greenhouse/test_pull_jobs.py
@@ -0,0 +1,10 @@
+from hrflow_connectors import Greenhouse
+
+
+def test_PullJobsAcrion(logger, hrflow_client):
+    Greenhouse.pull_jobs(
+        board_token="vaulttec",
+        hrflow_client=hrflow_client("dev-demo"),
+        board_key="61abdf5e11a8b75f2f5d1789339ee3638d4c6197",
+        hydrate_with_parsing=True,
+    )
\ No newline at end of file
diff --git a/tests/connectors/destinations/greenhouse/test_push_profile.py b/tests/connectors/greenhouse/test_push_profile.py
similarity index 78%
rename from tests/connectors/destinations/greenhouse/test_push_profile.py
rename to tests/connectors/greenhouse/test_push_profile.py
index 7593088cd..8fa621377 100644
--- a/tests/connectors/destinations/greenhouse/test_push_profile.py
+++ b/tests/connectors/greenhouse/test_push_profile.py
@@ -1,7 +1,7 @@
 import pytest
 
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.greenhouse import PushProfile
+from hrflow_connectors import Greenhouse
+from hrflow_connectors.core.auth import AuthorizationAuth, OAuth2PasswordCredentialsBody
 from hrflow_connectors.utils.hrflow import Profile, Source
 
 
@@ -23,12 +23,11 @@ def test_PushProfile(logger, auth, hrflow_client):
         key="65903f6989594e72ccf64a10875127b1293fdce3",
         source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
-    action = PushProfile(
+    response = Greenhouse.push_profile(
         job_id = ["4179714004"] ,
         on_behalf_of = "4249018004",
         auth=auth,
         hrflow_client=hrflow_client(),
         profile=profile,
     )
-    response = action.execute()
     assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/careerbuilder/__init__.py b/tests/connectors/monster/__init__.py
similarity index 100%
rename from tests/connectors/boards/careerbuilder/__init__.py
rename to tests/connectors/monster/__init__.py
diff --git a/tests/connectors/destinations/monster/test_pull_profile.py b/tests/connectors/monster/test_catch_profile.py
similarity index 62%
rename from tests/connectors/destinations/monster/test_pull_profile.py
rename to tests/connectors/monster/test_catch_profile.py
index 9bf51a96c..b3ac3dd7d 100644
--- a/tests/connectors/destinations/monster/test_pull_profile.py
+++ b/tests/connectors/monster/test_catch_profile.py
@@ -1,12 +1,12 @@
-from hrflow_connectors.connectors.destinations.monster.actions import PullProfile
+from hrflow_connectors import Monster
 
 import requests
 import pytest
 from typing import Dict, Any
 
 
-@pytest.fixture(scope="session")
-def monster_request(pytestconfig) -> Dict[str, Any]:
+@pytest.fixture()
+def monster_request() -> Dict[str, Any]:
     """
     Get credentials from a file in the root of the project `credentials.json` (to be defined)
 
@@ -14,11 +14,13 @@ def monster_request(pytestconfig) -> Dict[str, Any]:
         Dict[str, Any]: Credentials
     """
 
-    url = "https://riminder-documents" \
-          "-eu-2019-12.s3-eu-west-1.amazonaws.com/te" \
-          "ams/fc9d40fd60e679119130ea74ae1d34a3e22174f2/sourc" \
-          "es/8df6a1247b1a95e0b84f5226093ff2c58e60cdf1/profiles/64" \
-          "baf80a58f3c6e434d9517311fdfec5cf7c9996/parsing/resume.pdf"
+    url = (
+        "https://riminder-documents"
+        "-eu-2019-12.s3-eu-west-1.amazonaws.com/te"
+        "ams/fc9d40fd60e679119130ea74ae1d34a3e22174f2/sourc"
+        "es/8df6a1247b1a95e0b84f5226093ff2c58e60cdf1/profiles/64"
+        "baf80a58f3c6e434d9517311fdfec5cf7c9996/parsing/resume.pdf"
+    )
 
     r = requests.get(url)
     type(r.content)
@@ -42,12 +44,10 @@ def monster_request(pytestconfig) -> Dict[str, Any]:
     }
 
 
-def test_PullProfile(logger, hrflow_client, monster_request):
-    action = PullProfile(
+def test_CatchProfileBaseAction(logger, hrflow_client, monster_request):
+    response = Monster.catch_profile(
         hrflow_client=hrflow_client(),
         request=monster_request,
         source_key="8df6a1247b1a95e0b84f5226093ff2c58e60cdf1",
-        file_field="FileContents",
     )
-    response = action.execute()
     assert response.get("status_code") == 201
diff --git a/tests/connectors/monster/test_push_job.py b/tests/connectors/monster/test_push_job.py
new file mode 100644
index 000000000..86b0c71b4
--- /dev/null
+++ b/tests/connectors/monster/test_push_job.py
@@ -0,0 +1,28 @@
+import pytest
+
+from hrflow_connectors import Monster
+from hrflow_connectors import MonsterBodyAuth
+from hrflow_connectors.utils.hrflow import Job, Board
+
+
+@pytest.fixture
+def auth(credentials):
+    auth = MonsterBodyAuth(
+        username=credentials["monster"]["username"],
+        password=credentials["monster"]["password"],
+    )
+    return auth
+
+
+def test_PushJobBaseAction(logger, auth, hrflow_client):
+    job = Job(
+        key="230f841bc57774bde1ad67563d9f092a788364e0",
+        board=Board(key="d31518949ed1f88ac61308670324f93bc0f9374d"),
+    )
+    response = Monster.push_job(
+        auth=auth,
+        hrflow_client=hrflow_client(),
+        job=job,
+        subdomain="gateway",
+    )
+    assert response.get("status_code") == 201
diff --git a/tests/connectors/boards/ceridian/__init__.py b/tests/connectors/recruitee/__init__.py
similarity index 100%
rename from tests/connectors/boards/ceridian/__init__.py
rename to tests/connectors/recruitee/__init__.py
diff --git a/tests/connectors/boards/recruitee/test_pull_jobs.py b/tests/connectors/recruitee/test_pull_jobs.py
similarity index 65%
rename from tests/connectors/boards/recruitee/test_pull_jobs.py
rename to tests/connectors/recruitee/test_pull_jobs.py
index 058e201e1..ba91b4886 100644
--- a/tests/connectors/boards/recruitee/test_pull_jobs.py
+++ b/tests/connectors/recruitee/test_pull_jobs.py
@@ -1,11 +1,10 @@
-from hrflow_connectors.connectors.boards.recruitee import PullJobs
+from hrflow_connectors import Recruitee
 
 
 def test_PullJobs(logger, hrflow_client):
-    action = PullJobs(
+    Recruitee.pull_jobs(
         subdomain='testhr',
         hrflow_client=hrflow_client("dev-demo"),
         board_key="c430a1fe111a4076605a8dee85448300bd40f890",
         hydrate_with_parsing=True,
-    )
-    action.execute()
\ No newline at end of file
+    )
\ No newline at end of file
diff --git a/tests/connectors/destinations/recruitee/test_push_profile.py b/tests/connectors/recruitee/test_push_profile.py
similarity index 72%
rename from tests/connectors/destinations/recruitee/test_push_profile.py
rename to tests/connectors/recruitee/test_push_profile.py
index e234fd3cf..a06e26d5f 100644
--- a/tests/connectors/destinations/recruitee/test_push_profile.py
+++ b/tests/connectors/recruitee/test_push_profile.py
@@ -1,12 +1,9 @@
 import pytest
 
-from hrflow_connectors.core.auth import AuthorizationAuth
-from hrflow_connectors.connectors.destinations.recruitee import PushProfile
+from hrflow_connectors import AuthorizationAuth
+from hrflow_connectors import Recruitee
 from hrflow_connectors.utils.hrflow import Profile, Source
 
-
-
-
 @pytest.fixture
 def auth(credentials):
     auth = AuthorizationAuth(
@@ -23,11 +20,11 @@ def test_PushProfile(logger, auth, hrflow_client):
         key="65903f6989594e72ccf64a10875127b1293fdce3",
         source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
     )
-    action = PushProfile(
-        company_id = "testhr",
+    response = Recruitee.push_profile(
+        company_id = "hrflowai",
+        offer_id = None,
         auth=auth,
         hrflow_client=hrflow_client(),
         profile=profile,
     )
-    response = action.execute()
     assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/craigslist/__init__.py b/tests/connectors/sapsuccessfactors/__init__.py
similarity index 100%
rename from tests/connectors/boards/craigslist/__init__.py
rename to tests/connectors/sapsuccessfactors/__init__.py
diff --git a/tests/connectors/boards/sapsuccessfactors/test_pull_jobs.py b/tests/connectors/sapsuccessfactors/test_pull_jobs.py
similarity index 74%
rename from tests/connectors/boards/sapsuccessfactors/test_pull_jobs.py
rename to tests/connectors/sapsuccessfactors/test_pull_jobs.py
index a6f21c128..ca60ce597 100644
--- a/tests/connectors/boards/sapsuccessfactors/test_pull_jobs.py
+++ b/tests/connectors/sapsuccessfactors/test_pull_jobs.py
@@ -1,6 +1,6 @@
 import pytest
-from hrflow_connectors.connectors.boards.sapsuccessfactors import PullJobs
-from hrflow_connectors.core.auth import XAPIKeyAuth
+from hrflow_connectors import SapSuccessfactors
+from hrflow_connectors import XAPIKeyAuth
 
 
 @pytest.fixture
@@ -12,7 +12,7 @@ def auth(credentials):
     return auth
 
 def test_PullJobs(logger,auth, hrflow_client):
-    action = PullJobs(
+    SapSuccessfactors.pull_jobs(
         auth = auth,
         api_server="sandbox.api.sap.com:443/successfactors",
         top=20,
@@ -20,4 +20,3 @@ def test_PullJobs(logger,auth, hrflow_client):
         board_key="193bc9ff5d3076937d93950381783e108c957fb3",
         hydrate_with_parsing=True,
     )
-    action.execute()
\ No newline at end of file
diff --git a/tests/connectors/destinations/sapsuccessfactors/test_push_profile.py b/tests/connectors/sapsuccessfactors/test_push_profile.py
similarity index 73%
rename from tests/connectors/destinations/sapsuccessfactors/test_push_profile.py
rename to tests/connectors/sapsuccessfactors/test_push_profile.py
index ebcae53bf..912a27b70 100644
--- a/tests/connectors/destinations/sapsuccessfactors/test_push_profile.py
+++ b/tests/connectors/sapsuccessfactors/test_push_profile.py
@@ -1,8 +1,10 @@
 import pytest
-from hrflow_connectors.connectors.destinations.sapsuccessfactors import PushProfile
-from hrflow_connectors.core.auth import XAPIKeyAuth
+from hrflow_connectors import SapSuccessfactors
+from hrflow_connectors import XAPIKeyAuth
 from hrflow_connectors.utils.hrflow import Profile, Source
 
+
+
 @pytest.fixture
 def auth(credentials):
     auth = XAPIKeyAuth(
@@ -17,10 +19,11 @@ def test_PushProfile(logger,auth, hrflow_client):
         key = "65903f6989594e72ccf64a10875127b1293fdce3",
         source = Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb")
     )
-    action = PushProfile(
+    response = SapSuccessfactors.push_profile(
+        profile_already_exists='COE0019',
         auth = auth,
         api_server="sandbox.api.sap.com:443/successfactors",
         hrflow_client=hrflow_client("dev-demo"),
         profile=profile,
     )
-    action.execute()
\ No newline at end of file
+    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/crosstalent/__init__.py b/tests/connectors/smartrecruiters/__init__.py
similarity index 100%
rename from tests/connectors/boards/crosstalent/__init__.py
rename to tests/connectors/smartrecruiters/__init__.py
diff --git a/tests/connectors/boards/smartrecruiters/test_smart_jobs.py b/tests/connectors/smartrecruiters/test_pull_jobs.py
similarity index 66%
rename from tests/connectors/boards/smartrecruiters/test_smart_jobs.py
rename to tests/connectors/smartrecruiters/test_pull_jobs.py
index 725a3449f..fe60507d8 100644
--- a/tests/connectors/boards/smartrecruiters/test_smart_jobs.py
+++ b/tests/connectors/smartrecruiters/test_pull_jobs.py
@@ -1,7 +1,7 @@
 import pytest
 
-from hrflow_connectors.core.auth import XSmartTokenAuth
-from hrflow_connectors.connectors.boards.smartrecruiters import GetAllJobs
+from hrflow_connectors import XSmartTokenAuth
+from hrflow_connectors import SmartRecruiters
 
 
 @pytest.fixture
@@ -12,8 +12,8 @@ def auth(credentials):
     return auth
 
 
-def test_GetAllJobs(logger, auth, hrflow_client):
-    action = GetAllJobs(
+def test_PullJobsAction(logger, auth, hrflow_client):
+    SmartRecruiters.pull_jobs(
         auth=auth,
         hrflow_client=hrflow_client("dev-demo"),
         limit=2,
@@ -22,4 +22,3 @@ def test_GetAllJobs(logger, auth, hrflow_client):
         archive_deleted_jobs_from_stream=False,
         posting_status="PUBLIC",
     )
-    action.execute()
diff --git a/tests/connectors/destinations/smartrecruiters/test_smart_candidate.py b/tests/connectors/smartrecruiters/test_push_profile.py
similarity index 70%
rename from tests/connectors/destinations/smartrecruiters/test_smart_candidate.py
rename to tests/connectors/smartrecruiters/test_push_profile.py
index c9bb90184..8ab6d0842 100644
--- a/tests/connectors/destinations/smartrecruiters/test_smart_candidate.py
+++ b/tests/connectors/smartrecruiters/test_push_profile.py
@@ -1,11 +1,10 @@
 import pytest
 
-from hrflow_connectors.core.auth import XSmartTokenAuth
-from hrflow_connectors.connectors.destinations.smartrecruiters import PushProfile
+from hrflow_connectors import XSmartTokenAuth
+from hrflow_connectors import SmartRecruiters
 from hrflow_connectors.utils.hrflow import Profile, Source
 
 
-
 @pytest.fixture
 def auth(credentials):
     auth = XSmartTokenAuth(
@@ -14,17 +13,15 @@ def auth(credentials):
     return auth
 
 
-def test_PushProfile(logger, auth, hrflow_client):
-
+def test_SmartRecruitersPushProfileAction(logger, auth, hrflow_client):
     profile = Profile(
         key="89ddf5f18768747011a06b8921607cb54a4274a5",
         source=Source(key="6d68a20b2dd7c2bfdcb232b9234c38eada0fdcb4"),
     )
-    action = PushProfile(
+    response = SmartRecruiters.push_profile(
         auth=auth,
         job_id="3696cad0-a9b0-4a40-9cd7-4cc5feb1a509",
         hrflow_client=hrflow_client(),
         profile=profile,
     )
-    response = action.execute()
     assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/greenhouse/test_get_all_jobs/__init__.py b/tests/connectors/taleez/__init__.py
similarity index 100%
rename from tests/connectors/boards/greenhouse/test_get_all_jobs/__init__.py
rename to tests/connectors/taleez/__init__.py
diff --git a/tests/connectors/taleez/test_pull_jobs.py b/tests/connectors/taleez/test_pull_jobs.py
new file mode 100644
index 000000000..e552babff
--- /dev/null
+++ b/tests/connectors/taleez/test_pull_jobs.py
@@ -0,0 +1,22 @@
+import pytest
+
+from hrflow_connectors import XTaleezAuth
+from hrflow_connectors import Taleez
+
+
+@pytest.fixture
+def auth(credentials):
+    auth = XTaleezAuth(
+        name = 'X-taleez-api-secret',
+        value=credentials["taleez"]["X-taleez-api-secret"]
+    )
+    return auth
+
+
+def test_PullJobsAction(logger, auth, hrflow_client):
+    Taleez.pull_jobs(
+        auth=auth,
+        hrflow_client=hrflow_client("dev-demo"),
+        board_key="234e96cf22440fa88cae5bca854d674f77fa8f93",
+        hydrate_with_parsing=True,
+    )
diff --git a/tests/connectors/taleez/test_push_profile.py b/tests/connectors/taleez/test_push_profile.py
new file mode 100644
index 000000000..2d90b4f4c
--- /dev/null
+++ b/tests/connectors/taleez/test_push_profile.py
@@ -0,0 +1,30 @@
+import pytest
+
+from hrflow_connectors import XTaleezAuth
+from hrflow_connectors import Taleez
+from hrflow_connectors.utils.hrflow import Profile, Source
+
+@pytest.fixture
+def auth(credentials):
+    auth = XTaleezAuth(
+        name = 'X-taleez-api-secret',
+        value=credentials["taleez"]["X-taleez-api-secret"]
+    )
+    return auth
+
+
+
+def test_PushProfile(logger, auth, hrflow_client):
+
+    profile = Profile(
+        key="65903f6989594e72ccf64a10875127b1293fdce3",
+        source=Source(key="762d2f25b855f7cfd13e5585ef727d8fb6e752cb"),
+    )
+    response = Taleez.push_profile(
+        job_id = None,
+        auth=auth,
+        recruiter_id = 15886,
+        hrflow_client=hrflow_client(),
+        profile=profile,
+    )
+    assert response.get("status_code") == 201
\ No newline at end of file
diff --git a/tests/connectors/boards/indeed/__init__.py b/tests/connectors/workable/__init__.py
similarity index 100%
rename from tests/connectors/boards/indeed/__init__.py
rename to tests/connectors/workable/__init__.py
diff --git a/tests/connectors/workable/test_pull_jobs.py b/tests/connectors/workable/test_pull_jobs.py
new file mode 100644
index 000000000..4d7d3b531
--- /dev/null
+++ b/tests/connectors/workable/test_pull_jobs.py
@@ -0,0 +1,10 @@
+from hrflow_connectors import Workable
+
+
+def test_PullJobsAcrion(logger, hrflow_client):
+    Workable.pull_jobs(
+        subdomain="eurostar",
+        hrflow_client=hrflow_client("dev-demo"),
+        board_key="12dc2abb0d952bfcbbc6259b76c0631754459787",
+        hydrate_with_parsing=True,
+    )
\ No newline at end of file
diff --git a/tests/connectors/boards/monster/__init__.py b/tests/connectors/xml/__init__.py
similarity index 100%
rename from tests/connectors/boards/monster/__init__.py
rename to tests/connectors/xml/__init__.py
diff --git a/tests/connectors/boards/xml/test_xml_board_action.py b/tests/connectors/xml/test_pull_jobs.py
similarity index 95%
rename from tests/connectors/boards/xml/test_xml_board_action.py
rename to tests/connectors/xml/test_pull_jobs.py
index eb2147502..400324207 100644
--- a/tests/connectors/boards/xml/test_xml_board_action.py
+++ b/tests/connectors/xml/test_pull_jobs.py
@@ -1,10 +1,11 @@
 import responses
 
-from hrflow_connectors.connectors.boards.xml import XMLBoardAction
+from hrflow_connectors import XML
+from hrflow_connectors.connectors.xml.actions import PullJobsAction
 from hrflow_connectors.utils.datetime_converter import from_str_to_datetime
 
 
-def test_get_all_jobs_from_samsic_xml_stream(logger, hrflow_client):
+def test_PullJobsAction_from_samsic_xml_stream(logger, hrflow_client):
     def samsic_format(data):
         job = dict()
 
@@ -124,8 +125,7 @@ def create_tag(field_path: str):
 
     xml_stream_url = "https://cv.samsic-emploi.fr/media/flux/jobs.xml"
     job_list_xpath = "DataArea"
-
-    action = XMLBoardAction(
+    XML.pull_jobs(
         xml_stream_url=xml_stream_url,
         job_list_xpath=job_list_xpath,
         hrflow_client=hrflow_client("dev-demo"),
@@ -136,11 +136,10 @@ def create_tag(field_path: str):
         global_scope=globals(),
         local_scope=locals(),
     )
-    action.execute()
 
 
 @responses.activate
-def test_XMLBoardAction_pull_generic_xml_stream(logger, hrflow_client):
+def test_PullJobsAction_pull_generic_xml_stream(logger, hrflow_client):
     xml_stream_url = "https://test.test/job/xml_stream"
 
     xml_stream_str = """<?xml version="1.0" encoding="UTF-8"?>
@@ -170,7 +169,7 @@ def test_XMLBoardAction_pull_generic_xml_stream(logger, hrflow_client):
     responses.add(responses.GET, xml_stream_url, status=200, body=xml_stream_str)
 
     job_list_xpath = "board/jobs"
-    action = XMLBoardAction(
+    action = PullJobsAction(
         xml_stream_url=xml_stream_url,
         job_list_xpath=job_list_xpath,
         hrflow_client=hrflow_client("dev-demo"),
diff --git a/tests/core/test_action.py b/tests/core/test_action.py
index 7a3b70931..9b4b9c0f5 100644
--- a/tests/core/test_action.py
+++ b/tests/core/test_action.py
@@ -1,9 +1,22 @@
-from hrflow_connectors.core.action import Action, BoardAction
+from hrflow_connectors.core.action import (
+    BaseAction,
+    PullBaseAction,
+    PushBaseAction,
+    PullJobsBaseAction,
+    PushProfileBaseAction,
+    PushJobBaseAction,
+    CatchProfileBaseAction,
+)
+from hrflow_connectors.utils.hrflow import Profile, Source, Job, Board
 import pytest
 import requests
 import responses
 from hrflow import Hrflow
 
+##################
+### BaseAction ###
+##################
+
 
 @pytest.fixture
 def generated_data_list():
@@ -15,8 +28,10 @@ def generated_data_list():
     return list_to_filter
 
 
-def test_apply_logics_with_empty_logics_list(generated_data_list):
-    action = Action()
+def test_BaseAction_apply_logics_with_empty_logics_list(
+    hrflow_client, generated_data_list
+):
+    action = BaseAction(hrflow_client=hrflow_client())
     filtered_list = action.apply_logics(generated_data_list)
 
     assert len(filtered_list) == 4
@@ -26,11 +41,12 @@ def test_apply_logics_with_empty_logics_list(generated_data_list):
     assert dict(element1="value2", element2="value2") in filtered_list
 
 
-def test_apply_logics_single_filter(generated_data_list):
+def test_BaseAction_apply_logics_single_filter(hrflow_client, generated_data_list):
     def filter_element1_with_value1(element):
         return element.get("element1") == "value1"
 
-    action = Action(
+    action = BaseAction(
+        hrflow_client=hrflow_client(),
         logics=["filter_element1_with_value1"],
         global_scope=globals(),
         local_scope=locals(),
@@ -42,14 +58,15 @@ def filter_element1_with_value1(element):
     assert dict(element1="value1", element2="value1") in filtered_list
 
 
-def test_apply_logics_two_filter(generated_data_list):
+def test_BaseAction_apply_logics_two_filter(hrflow_client, generated_data_list):
     def filter_element1_with_value1(element):
         return element.get("element1") == "value1"
 
     def filter_element2_with_value1(element):
         return element.get("element2") == "value1"
 
-    action = Action(
+    action = BaseAction(
+        hrflow_client=hrflow_client(),
         logics=["filter_element1_with_value1", "filter_element2_with_value1"],
         global_scope=globals(),
         local_scope=locals(),
@@ -60,12 +77,17 @@ def filter_element2_with_value1(element):
     assert dict(element1="value1", element2="value1") in filtered_list
 
 
-def test_apply_logics_single_filter_without_interaction(generated_data_list):
+def test_BaseAction_apply_logics_single_filter_without_interaction(
+    hrflow_client, generated_data_list
+):
     def filter_nothing(element):
         return True
 
-    action = Action(
-        logics=["filter_nothing"], global_scope=globals(), local_scope=locals()
+    action = BaseAction(
+        hrflow_client=hrflow_client(),
+        logics=["filter_nothing"],
+        global_scope=globals(),
+        local_scope=locals(),
     )
     filtered_list = list(action.apply_logics(generated_data_list))
 
@@ -76,11 +98,12 @@ def filter_nothing(element):
     assert dict(element1="value2", element2="value2") in filtered_list
 
 
-def test_extern_format_function():
+def test_BaseAction_extern_format_function(hrflow_client):
     def extern_format(data):
         return dict(c=data["a"], d=data["a"] + data["b"])
 
-    action = Action(
+    action = BaseAction(
+        hrflow_client=hrflow_client(),
         format_function_name="extern_format",
         global_scope=globals(),
         local_scope=locals(),
@@ -91,8 +114,9 @@ def extern_format(data):
     assert transformed_job == dict(c="aaa", d="aaabbb")
 
 
-def test_default_format_without_extern_format_function():
-    action = Action(
+def test_BaseAction_default_format_without_extern_format_function(hrflow_client):
+    action = BaseAction(
+        hrflow_client=hrflow_client(),
         format_function_name=None,
     )
     job_to_transform = dict(a="aaa", b="bbb", f="fff")
@@ -100,15 +124,16 @@ def test_default_format_without_extern_format_function():
     assert transformed_job == dict(a="aaa", b="bbb", f="fff")
 
 
-def test_overwritten_format_with_extern_format_function():
+def test_BaseAction_overwritten_format_with_extern_format_function(hrflow_client):
     def extern_format(data):
         return dict(c=data["a"], d=data["a"] + data["b"])
 
-    class TestAction(Action):
+    class TestAction(BaseAction):
         def format(self, data):
             return dict(f=data["f"], g=data["a"] + data["b"])
 
     action = TestAction(
+        hrflow_client=hrflow_client(),
         format_function_name="extern_format",
         global_scope=globals(),
         local_scope=locals(),
@@ -120,9 +145,9 @@ def format(self, data):
 
 
 @responses.activate
-def test_Action_connect_and_execute(generated_data_list):
+def test_BaseAction_connect_and_execute(hrflow_client, generated_data_list):
     # Build a connector from `generated_data_list` to `http://test.test/push`
-    class TestConnectorAction(Action):
+    class TestConnectorAction(BaseAction):
         def pull(self):
             return generated_data_list
 
@@ -156,10 +181,75 @@ def push(self, data):
     )
 
     # Exec action
-    action = TestConnectorAction()
+    action = TestConnectorAction(hrflow_client=hrflow_client())
+    action.execute()
+
+
+######################
+### PullBaseAction ###
+######################
+
+
+def test_PullBaseAction_execute(hrflow_client):
+    class MyPullAction(PullBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["logic"]
+
+    def my_logic(data):
+        return not data == "format"
+
+    action = MyPullAction(
+        hrflow_client=hrflow_client,
+        logics=["my_logic"],
+        local_scope=locals(),
+        global_scope=globals(),
+    )
     action.execute()
 
 
+######################
+### PushBaseAction ###
+######################
+
+
+def test_PushBaseAction_execute(hrflow_client):
+    class MyPushAction(PushBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["format"]
+
+    def my_logic(data):
+        return not data == "pulllogic"
+
+    action = MyPushAction(
+        hrflow_client=hrflow_client,
+        logics=["my_logic"],
+        local_scope=locals(),
+        global_scope=globals(),
+    )
+    action.execute()
+
+
+##########################
+### PullJobsBaseAction ###
+##########################
+
+
 @pytest.fixture
 def generated_jobs():
     def generate_jobs_page(page: int, jobs=30):
@@ -178,32 +268,39 @@ def generate_hrflow_search_response(data, max_page=2):
 
 
 @responses.activate
-def test_BoardAction_get_all_references_from_board(generated_jobs):
+def test_PullJobsBaseAction_get_all_references_from_board(
+    hrflow_client, generated_jobs
+):
     # Generate pages of jobs
     page_1 = generated_jobs(page=1, jobs=30)
     page_2 = generated_jobs(page=2, jobs=29)
 
     # Generate responses return by Hrflow
+    expected_page_1_params = dict(
+        board_keys='["abc"]', limit="30", page="1", sort_by="created_at"
+    )
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/jobs/searching?board_keys=%5B%22abc%22%5D&limit=30&page=1&sort_by=created_at",
+        "https://api.hrflow.ai/v1/jobs/searching",
         status=200,
+        match=[responses.matchers.query_param_matcher(expected_page_1_params)],
         json=generate_hrflow_search_response(page_1, max_page=2),
     )
 
+    expected_page_2_params = dict(
+        board_keys='["abc"]', limit="30", page="2", sort_by="created_at"
+    )
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/jobs/searching?board_keys=%5B%22abc%22%5D&limit=30&page=2&sort_by=created_at",
+        "https://api.hrflow.ai/v1/jobs/searching",
         status=200,
+        match=[responses.matchers.query_param_matcher(expected_page_2_params)],
         json=generate_hrflow_search_response(page_2, max_page=2),
     )
 
     # Catch requests sent to Hrflow
-
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     all_reference_iter = action.get_all_references_from_board()
     all_reference_list = list(all_reference_iter)
@@ -220,6 +317,88 @@ def test_BoardAction_get_all_references_from_board(generated_jobs):
     assert all_reference_list == reference_expected
 
 
+@responses.activate
+def test_PullJobsBaseAction_get_all_references_from_board_failure(
+    hrflow_client, generated_jobs
+):
+    # Generate pages of jobs
+    page_1 = generated_jobs(page=1, jobs=30)
+    page_2 = generated_jobs(page=2, jobs=29)
+
+    # Generate responses return by Hrflow
+    expected_page_1_params = dict(
+        board_keys='["abc"]', limit="30", page="1", sort_by="created_at"
+    )
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/jobs/searching",
+        status=400,
+        match=[responses.matchers.query_param_matcher(expected_page_1_params)],
+        json=dict(code=400, message="Test get_job_page failed"),
+    )
+
+    # Catch requests sent to Hrflow
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
+    )
+    try:
+        all_reference_iter = action.get_all_references_from_board()
+        all_reference_list = list(all_reference_iter)
+        assert False
+    except RuntimeError:
+        pass
+
+
+@responses.activate
+def test_PullJobsBaseAction_get_all_references_from_board_and_less_job_returned(
+    hrflow_client, generated_jobs
+):
+    # Generate pages of jobs
+    page_1 = generated_jobs(page=1, jobs=30)
+
+    # Generate responses return by Hrflow
+    expected_page_1_params = dict(
+        board_keys='["abc"]', limit="30", page="1", sort_by="created_at"
+    )
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/jobs/searching",
+        status=200,
+        match=[responses.matchers.query_param_matcher(expected_page_1_params)],
+        json=generate_hrflow_search_response(page_1, max_page=2),
+    )
+
+    # Empty job list
+    expected_page_2_params = dict(
+        board_keys='["abc"]', limit="30", page="2", sort_by="created_at"
+    )
+    page_2_response = dict(code=200, message="Success", data=dict(jobs=[]))
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/jobs/searching",
+        status=200,
+        match=[responses.matchers.query_param_matcher(expected_page_2_params)],
+        json=page_2_response,
+    )
+
+    # Catch requests sent to Hrflow
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
+    )
+    all_reference_iter = action.get_all_references_from_board()
+    all_reference_list = list(all_reference_iter)
+
+    assert len(all_reference_list) == 30
+
+    all_reference_list.sort()
+    page_1_expected = ["{}-{}".format(i, 1) for i in range(30)]
+
+    reference_expected = page_1_expected
+    reference_expected.sort()
+
+    assert all_reference_list == reference_expected
+
+
 @pytest.fixture
 def generated_parsing_text_response():
     text = "I love Python\ni speak english"
@@ -232,7 +411,9 @@ def generated_parsing_text_response():
 
 
 @responses.activate
-def test_BoardAction_hydrate_job_with_parsing(generated_parsing_text_response):
+def test_PullJobsBaseAction_hydrate_job_with_parsing(
+    hrflow_client, generated_parsing_text_response
+):
     # Catch request
     responses.add(
         responses.POST,
@@ -242,10 +423,8 @@ def test_BoardAction_hydrate_job_with_parsing(generated_parsing_text_response):
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    action = PullJobsBaseAction(
+        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])
@@ -262,6 +441,64 @@ def test_BoardAction_hydrate_job_with_parsing(generated_parsing_text_response):
     assert hydrated_job["languages"][0] == dict(name="english", value=None)
 
 
+@responses.activate
+def test_PullJobsBaseAction_hydrate_job_with_parsing_failure(
+    hrflow_client, generated_parsing_text_response
+):
+    # Catch request
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/document/parsing",
+        status=400,
+        json=dict(code=400, message="Test fail to parse"),
+    )
+
+    # Build Action
+    action = PullJobsBaseAction(
+        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])
+
+    assert len(job.get("skills", [])) == 0
+    assert len(job.get("language", [])) == 0
+
+    try:
+        action.hydrate_job_with_parsing(job)
+        assert False
+    except RuntimeError:
+        pass
+
+
+@responses.activate
+def test_PullJobsBaseAction_hydrate_job_with_parsing_with_empty_summary_and_only_html(
+    hrflow_client, generated_parsing_text_response
+):
+    # Catch request
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/document/parsing5",
+        status=200,
+        json=generated_parsing_text_response,
+    )
+
+    # Build Action
+    action = PullJobsBaseAction(
+        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])
+
+    assert "skills" not in job
+    assert "language" not in job
+
+    hydrated_job = action.hydrate_job_with_parsing(job)
+
+    assert "skills" not in hydrated_job
+    assert "language" not in hydrated_job
+    assert hydrated_job == job
+
+
 @pytest.fixture
 def generate_indexing_get_response():
     def indexing_get_response_func(code, message, archived_at):
@@ -273,7 +510,24 @@ def indexing_get_response_func(code, message, archived_at):
 
 
 @responses.activate
-def test_BoardAction_check_reference_in_board_for_job_not_in_board(
+def test_PullJobsBaseAction_check_reference_in_board_for_job_ref_none(
+    hrflow_client,
+    generate_indexing_get_response,
+):
+    # Generate job
+    job = dict(reference=None)
+
+    # Build Action
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
+    )
+    check_response = action.check_reference_in_board(job)
+    assert check_response
+
+
+@responses.activate
+def test_PullJobsBaseAction_check_reference_in_board_for_job_not_in_board(
+    hrflow_client,
     generate_indexing_get_response,
 ):
     # Generate job
@@ -286,25 +540,26 @@ def test_BoardAction_check_reference_in_board_for_job_not_in_board(
     )
 
     # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/job/indexing?board_key=abc&reference=REF1",
+        "https://api.hrflow.ai/v1/job/indexing",
         status=400,
+        match=[responses.matchers.query_param_matcher(expected_params)],
         json=generated_response,
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     check_response = action.check_reference_in_board(job)
     assert check_response
 
 
 @responses.activate
-def test_BoardAction_check_reference_in_board_for_not_archived_job_in_board(
+def test_PullJobsBaseAction_check_reference_in_board_for_not_archived_job_in_board(
+    hrflow_client,
     generate_indexing_get_response,
 ):
     # Generate job
@@ -317,25 +572,27 @@ def test_BoardAction_check_reference_in_board_for_not_archived_job_in_board(
     )
 
     # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/job/indexing?board_key=abc&reference=REF1",
+        "https://api.hrflow.ai/v1/job/indexing",
         status=200,
+        match=[responses.matchers.query_param_matcher(expected_params)],
         json=generated_response,
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     check_response = action.check_reference_in_board(job)
     assert not check_response
 
 
 @responses.activate
-def test_BoardAction_check_reference_in_board_fail(generate_indexing_get_response):
+def test_PullJobsBaseAction_check_reference_in_board_fail(
+    hrflow_client, generate_indexing_get_response
+):
     # Generate job
     job = dict(reference="REF1")
 
@@ -346,18 +603,18 @@ def test_BoardAction_check_reference_in_board_fail(generate_indexing_get_respons
     )
 
     # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/job/indexing?board_key=abc&reference=REF1",
+        "https://api.hrflow.ai/v1/job/indexing",
         status=400,
+        match=[responses.matchers.query_param_matcher(expected_params)],
         json=generated_response,
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     try:
         action.check_reference_in_board(job)
@@ -367,7 +624,8 @@ def test_BoardAction_check_reference_in_board_fail(generate_indexing_get_respons
 
 
 @responses.activate
-def test_BoardAction_check_reference_in_board_for_archived_job_in_board_without_parsing(
+def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_without_parsing(
+    hrflow_client,
     generate_indexing_get_response,
 ):
     # Generate job
@@ -380,10 +638,12 @@ def test_BoardAction_check_reference_in_board_for_archived_job_in_board_without_
     )
 
     # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/job/indexing?board_key=abc&reference=REF1",
+        "https://api.hrflow.ai/v1/job/indexing",
         status=200,
+        match=[responses.matchers.query_param_matcher(expected_params)],
         json=generated_response,
     )
 
@@ -408,18 +668,69 @@ def test_BoardAction_check_reference_in_board_for_archived_job_in_board_without_
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
+    )
+    check_response = action.check_reference_in_board(job)
+    assert not check_response
+
+
+@responses.activate
+def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_without_parsing_and_unarchiving_failed(
+    hrflow_client,
+    generate_indexing_get_response,
+):
+    # Generate job
+    job = dict(reference="REF1")
+
+    # generated response
+    message = "Job details"
+    generated_response = generate_indexing_get_response(
+        code=200, message=message, archived_at="2021-12-25T00:00:00"
+    )
+
+    # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=200,
+        match=[responses.matchers.query_param_matcher(expected_params)],
+        json=generated_response,
+    )
 
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+    ## 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", reference="REF1", is_archive=False)
+    match = [responses.matchers.json_params_matcher(expected_body)]
+    responses.add(
+        responses.PATCH,
+        "https://api.hrflow.ai/v1/job/indexing/archive",
+        status=400,
+        match=match,
+        json=dict(code=400, message="Test Unarchiving failed"),
+    )
+
+    ## 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")
+    match = [responses.matchers.json_params_matcher(expected_body)]
+    responses.add(
+        responses.PUT,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=200,
+        match=match,
+    )
+
+    # Build Action
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
     )
     check_response = action.check_reference_in_board(job)
     assert not check_response
 
 
 @responses.activate
-def test_BoardAction_check_reference_in_board_for_archived_job_in_board_with_parsing(
-    generate_indexing_get_response, generated_parsing_text_response
+def test_PullJobsBaseAction_check_reference_in_board_for_archived_job_in_board_with_parsing(
+    hrflow_client, generate_indexing_get_response, generated_parsing_text_response
 ):
     # Generate job
     section = dict(name="s", title=None, description="i speak english")
@@ -435,10 +746,12 @@ def test_BoardAction_check_reference_in_board_for_archived_job_in_board_with_par
     )
 
     # Catch request
+    expected_params = dict(board_key="abc", reference="REF1")
     responses.add(
         responses.GET,
-        "https://api.hrflow.ai/v1/job/indexing?board_key=abc&reference=REF1",
+        "https://api.hrflow.ai/v1/job/indexing",
         status=200,
+        match=[responses.matchers.query_param_matcher(expected_params)],
         json=generated_response,
     )
 
@@ -478,29 +791,25 @@ def test_BoardAction_check_reference_in_board_for_archived_job_in_board_with_par
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = BoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=True
+    action = PullJobsBaseAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=True
     )
     check_response = action.check_reference_in_board(job)
 
     assert not check_response
 
 
-def test_BoardAction_get_all_references_from_stream():
+def test_PullJobsBaseAction_get_all_references_from_stream(hrflow_client):
     jobs_in_stream = [dict(reference="REF1"), dict(reference="REF2")]
     references_in_stream = ["REF1", "REF2"]
 
-    class TestBoardAction(BoardAction):
+    class TestPullJobsAction(PullJobsBaseAction):
         def pull(self):
             return jobs_in_stream
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
-
-    action = TestBoardAction(
-        hrflow_client=hrflow_client,
+    action = TestPullJobsAction(
+        hrflow_client=hrflow_client(),
         board_key="abc",
         hydrate_with_parsing=False,
         archive_deleted_jobs_from_stream=True,
@@ -510,11 +819,11 @@ def pull(self):
 
 
 @responses.activate
-def test_BoardAction_check_deletion_references_from_stream():
+def test_PullJobsBaseAction_check_deletion_references_from_stream(hrflow_client):
     references_in_stream = ["REF1", "REF2"]
     references_in_board = ["REF1", "REF4"]
 
-    class TestBoardAction(BoardAction):
+    class TestPullJobsAction(PullJobsBaseAction):
         def get_all_references_from_board(self):
             return references_in_board
 
@@ -532,9 +841,433 @@ def get_all_references_from_stream(self):
     )
 
     # Build Action
-    hrflow_client = Hrflow(api_user="", api_secret="")
+    action = TestPullJobsAction(
+        hrflow_client=hrflow_client(), board_key="abc", hydrate_with_parsing=False
+    )
+    action.check_deletion_references_from_stream()
+
+
+def test_PullJobsBaseAction_execute_with_archiving_and_parsing(hrflow_client):
+    class MyPullJobsAction(PullJobsBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic", "pullcheckref"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def check_deletion_references_from_stream(self):
+            assert True
+
+        def check_reference_in_board(self, data):
+            return "checkref" != data
+
+        def hydrate_job_with_parsing(self, data):
+            return data + "_after_parsing"
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["logic_after_parsing"]
+
+    def my_logic(data):
+        return not data == "format"
+
+    action = MyPullJobsAction(
+        hrflow_client=hrflow_client,
+        board_key="abc",
+        hydrate_with_parsing=True,
+        logics=["my_logic"],
+        local_scope=locals(),
+        global_scope=globals(),
+    )
+    action.execute()
+
+
+def test_PullJobsBaseAction_execute_with_archiving_without_parsing(hrflow_client):
+    class MyPullJobsAction(PullJobsBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic", "pullcheckref"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def check_deletion_references_from_stream(self):
+            assert True
+
+        def check_reference_in_board(self, data):
+            return "checkref" != data
+
+        def hydrate_job_with_parsing(self, data):
+            return data + "_after_parsing"
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["logic"]
+
+    def my_logic(data):
+        return not data == "format"
+
+    action = MyPullJobsAction(
+        hrflow_client=hrflow_client,
+        board_key="abc",
+        hydrate_with_parsing=False,
+        logics=["my_logic"],
+        local_scope=locals(),
+        global_scope=globals(),
+    )
+    action.execute()
+
+
+def test_PullJobsBaseAction_execute_without_archiving_and_parsing(hrflow_client):
+    class MyPullJobsAction(PullJobsBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic", "pullcheckref"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def check_deletion_references_from_stream(self):
+            assert False
+
+        def check_reference_in_board(self, data):
+            return "checkref" != data
+
+        def hydrate_job_with_parsing(self, data):
+            return data + "_after_parsing"
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["logic"]
+
+    def my_logic(data):
+        return not data == "format"
+
+    action = MyPullJobsAction(
+        hrflow_client=hrflow_client,
+        board_key="abc",
+        hydrate_with_parsing=False,
+        archive_deleted_jobs_from_stream=False,
+        logics=["my_logic"],
+        local_scope=locals(),
+        global_scope=globals(),
+    )
+    action.execute()
+
+
+@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)
+    returned_value = dict(code=200, data=dict(key="efg"))
+    match = [responses.matchers.json_params_matcher(expected_body)]
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=200,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    action = PullJobsBaseAction(board_key="abc", hrflow_client=hrflow_client())
+    action.push([job])
+
+
+@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)
+    returned_value = dict(code=400, message="Test")
+    match = [responses.matchers.json_params_matcher(expected_body)]
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=400,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    action = PullJobsBaseAction(board_key="abc", hrflow_client=hrflow_client())
+    try:
+        action.push([job])
+        assert False
+    except RuntimeError:
+        pass
+
+
+#############################
+### PushProfileBaseAction ###
+#############################
+
+
+@responses.activate
+def test_PushProfileBaseAction_pull_success(hrflow_client):
+    # Mock requests and check data sent
+    expected_params = dict(source_key="abc", key="efg")
+    returned_value = dict(code=200, data=dict(key="efg"))
+    match = [responses.matchers.query_param_matcher(expected_params)]
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/profile/indexing",
+        status=200,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    profile = Profile(key="efg", source=Source(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]
+    assert profile_got["key"] == "efg"
 
-    action = TestBoardAction(
-        hrflow_client=hrflow_client, board_key="abc", hydrate_with_parsing=False
+
+@responses.activate
+def test_PushProfileBaseAction_pull_failure(hrflow_client):
+    # Mock requests and check data sent
+    expected_params = dict(source_key="abc", key="efg")
+    returned_value = dict(code=400, message="Test", data=dict())
+    match = [responses.matchers.query_param_matcher(expected_params)]
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/profile/indexing",
+        status=400,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    profile = Profile(key="efg", source=Source(key="abc"))
+    action = PushProfileBaseAction(hrflow_client=hrflow_client(), profile=profile)
+
+    try:
+        action.pull()
+        assert False
+    except RuntimeError:
+        pass
+
+
+@responses.activate
+def test_PushProfileBaseAction_execute(hrflow_client):
+    profile = Profile(key="efg", source=Source(key="abc"))
+
+    # Mock the `pull` & `push` method to do nothing
+    class MyPushProfileAction(PushProfileBaseAction):
+        def pull(self):
+            return []
+
+        def push(self, data):
+            return
+
+    action = MyPushProfileAction(hrflow_client=hrflow_client(), profile=profile)
+
+    workflow_response = action.execute()
+
+    assert workflow_response["status_code"] == 201
+    assert workflow_response["message"] == "Profile successfully pushed"
+
+
+##########################
+### PushJobeBaseAction ###
+##########################
+
+
+@responses.activate
+def test_PushJobBaseAction_execute(hrflow_client):
+    job = Job(key="efg", board=Board(key="abc"))
+
+    # Mock the `pull` & `push` method to do nothing
+    class MyPushJobAction(PushJobBaseAction):
+        def pull(self):
+            return ["pullformat", "pulllogic"]
+
+        def format(self, data):
+            assert "pull" in data
+            return data.replace("pull", "")
+
+        def push(self, data):
+            data_list = list(data)
+            assert data_list == ["format", "logic"]
+
+    action = MyPushJobAction(hrflow_client=hrflow_client(), job=job)
+
+    workflow_response = action.execute()
+
+    assert workflow_response["status_code"] == 201
+    assert workflow_response["message"] == "Profile successfully pushed"
+
+
+@responses.activate
+def test_PushJobBaseAction_pull_success(hrflow_client):
+    # Mock requests and check data sent
+    expected_params = dict(board_key="abc", key="efg")
+    returned_value = dict(code=200, data=dict(key="efg"))
+    match = [responses.matchers.query_param_matcher(expected_params)]
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=200,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    job = Job(key="efg", board=Board(key="abc"))
+    action = PushJobBaseAction(hrflow_client=hrflow_client(), job=job)
+    profile_list_got = action.pull()
+
+    # Check returned value
+    assert len(profile_list_got) == 1
+
+    profile_got = profile_list_got[0]
+    assert profile_got["key"] == "efg"
+
+
+@responses.activate
+def test_PushJobBaseAction_pull_failure(hrflow_client):
+    # Mock requests and check data sent
+    expected_params = dict(board_key="abc", key="efg")
+    returned_value = dict(code=400, message="Test", data=dict())
+    match = [responses.matchers.query_param_matcher(expected_params)]
+    responses.add(
+        responses.GET,
+        "https://api.hrflow.ai/v1/job/indexing",
+        status=400,
+        match=match,
+        json=returned_value,
+    )
+
+    # Pull data
+    job = Job(key="efg", board=Board(key="abc"))
+    action = PushJobBaseAction(hrflow_client=hrflow_client(), job=job)
+
+    try:
+        action.pull()
+        assert False
+    except RuntimeError:
+        pass
+
+
+##############################
+### CatchProfileBaseAction ###
+##############################
+
+
+@responses.activate
+def test_CatchProfileBaseAction_execute(hrflow_client):
+    request = {
+        "City": "xxx",
+        "CountryCode": "x",
+        "EmailAddress": "xxx",
+        "FileContents": "xxxx",
+        "FileExt": ".xx",
+        "FirstName": "xxxxx",
+        "JobRefID": "xxxx",
+        "LastName": "xx",
+        "PhoneNumber": "+xxxx",
+        "ResumeValue": "x",
+        "State": "x",
+        "VendorField": "xxx xx xxxxx xx",
+        "WorkAuthorization": 1,
+        "ZIPCode": "xxxxxx",
+    }
+
+    # Mock the `pull` & `push` method to do nothing
+    class MyCatchProfileAction(CatchProfileBaseAction):
+        def format(self, data):
+            assert data.get("City") == "xxx"
+            data["ZIPCode"] = "X"
+            return data
+
+        def push(self, data):
+            assert data["ZIPCode"] == "X"
+
+    action = MyCatchProfileAction(
+        hrflow_client=hrflow_client(),
+        request=request,
+        source_key="d31518949ed1f88ac61308670324f93bc0f9374d",
+    )
+
+    workflow_response = action.execute()
+
+    assert workflow_response["status_code"] == 201
+    assert workflow_response["message"] == "Profile successfully pushed"
+
+
+@responses.activate
+def test_CatchProfileBaseAction_push_success(hrflow_client):
+    # Mock requests and check data sent
+    returned_value = dict(code=200, message="ok", data=[])
+    data_expected = dict(
+        source_key="abc",
+        labels="[]",
+        tags="[]",
+        metadatas="[]",
+        sync_parsing=0,
+        sync_parsing_indexing=1,
+        webhook_parsing_sending=0,
     )
-    action.check_deletion_references_from_stream()
\ No newline at end of file
+
+    files_expected = dict(file=b"base64")
+    match = [
+        responses.matchers.multipart_matcher(files=files_expected, data=data_expected)
+    ]
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/profile/parsing/file",
+        status=200,
+        match=match,
+        json=returned_value,
+    )
+
+    # Push data
+    action = CatchProfileBaseAction(
+        hrflow_client=hrflow_client(), source_key="abc", request=dict()
+    )
+    action.push(dict(source_key="abc", profile_file="base64"))
+
+
+@responses.activate
+def test_CatchProfileBaseAction_push_success(hrflow_client):
+    # Mock requests and check data sent
+    returned_value = dict(code=400, message="Test", data=[])
+    data_expected = dict(
+        source_key="abc",
+        labels="[]",
+        tags="[]",
+        metadatas="[]",
+        sync_parsing=0,
+        sync_parsing_indexing=1,
+        webhook_parsing_sending=0,
+    )
+
+    files_expected = dict(file=b"base64")
+    match = [
+        responses.matchers.multipart_matcher(files=files_expected, data=data_expected)
+    ]
+    responses.add(
+        responses.POST,
+        "https://api.hrflow.ai/v1/profile/parsing/file",
+        status=400,
+        match=match,
+        json=returned_value,
+    )
+
+    # Push data
+    action = CatchProfileBaseAction(
+        hrflow_client=hrflow_client(), source_key="abc", request=dict()
+    )
+    try:
+        action.push(dict(source_key="abc", profile_file="base64"))
+        assert False
+    except RuntimeError:
+        pass
\ No newline at end of file
diff --git a/tests/core/test_auth.py b/tests/core/test_auth.py
index 49320b957..77ce94f67 100644
--- a/tests/core/test_auth.py
+++ b/tests/core/test_auth.py
@@ -1,27 +1,44 @@
 import responses
+import requests
 
-from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody
-from hrflow_connectors.core.auth import XAPIKeyAuth, AuthorizationAuth, XSmartTokenAuth
+from hrflow_connectors.core.auth import OAuth2PasswordCredentialsBody, OAuth2EmailPasswordBody
+from hrflow_connectors.core.auth import (
+    Auth,
+    XAPIKeyAuth,
+    AuthorizationAuth,
+    XSmartTokenAuth,
+    MonsterBodyAuth,
+    XTaleezAuth,
+)
 
-OAuth2PasswordCredentialsBody_JSON_RESPONSE = {
-    "access_token": "ABC.TOKEN.EFD",
-    "instance_url": "https://test.test/services/oauth2/token",
-    "id": "https://test.test/id/00D3N0000002eRJXAY/0055N000006PGpLQAW",
-    "token_type": "Bearer",
-    "issued_at": "1638442809550",
-    "signature": "wp1jFqVjffN2gLP3O9NvK93VBYFdgHD/FtwiYEhPrlQ=",
-}
+
+def test_Auth():
+    auth = Auth()
+    updatable_object = dict(abc="def")
+    updated_object = auth(updatable_object)
+    assert updatable_object == updated_object
 
 
 @responses.activate
 def test_OAuth2PasswordCredentialsBody_get_access_token():
+
+    OAuth2PasswordCredentialsBody_JSON_RESPONSE = {
+        "access_token": "ABC.TOKEN.EFD",
+        "instance_url": "https://test.test/services/oauth2/token",
+        "id": "https://test.test/id/00D3N0000002eRJXAY/0055N000006PGpLQAW",
+        "token_type": "Bearer",
+        "issued_at": "1638442809550",
+        "signature": "wp1jFqVjffN2gLP3O9NvK93VBYFdgHD/FtwiYEhPrlQ=",
+    }
+
     access_token_url = "https://test.test/services/oauth2/token"
+    check_auth_url = "http://test.test/check_auth"
     client_id = "007"
     client_secret = "double0"
     username = "bond"
     password = "jb"
 
-    # build Mock for request
+    # build Mock for get_access_token request
     body = dict(
         grant_type="password",
         client_id=client_id,
@@ -29,17 +46,16 @@ def test_OAuth2PasswordCredentialsBody_get_access_token():
         username=username,
         password=password,
     )
-    match = [responses.matchers.urlencoded_params_matcher(body)]
-
+    get_access_match = [responses.matchers.urlencoded_params_matcher(body)]
     responses.add(
         responses.POST,
         access_token_url,
         status=200,
         json=OAuth2PasswordCredentialsBody_JSON_RESPONSE,
-        match=match,
+        match=get_access_match,
     )
 
-    # Auth to test
+    # Instanciate Auth to test
     auth = OAuth2PasswordCredentialsBody(
         access_token_url=access_token_url,
         client_id=client_id,
@@ -51,11 +67,84 @@ def test_OAuth2PasswordCredentialsBody_get_access_token():
     access_token = auth.get_access_token()
     assert access_token == OAuth2PasswordCredentialsBody_JSON_RESPONSE["access_token"]
 
-    headers = dict(test="abc")
-    auth.update(headers=headers)
+    # build Mock for check_auth request
+    header = dict(Authorization=f"OAuth {access_token}")
+    check_auth_match = [responses.matchers.header_matcher(header)]
+    responses.add(
+        responses.GET,
+        check_auth_url,
+        status=200,
+        match=check_auth_match,
+    )
+
+    # Send authenticated request
+    session = requests.Session()
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", auth=auth
+    )
+    prepared_request = request.prepare()
+    session.send(prepared_request)
+
+    prepared_request
+    prepared_request.headers.update(dict(test="abc"))
+    authenticated_prepared_request = auth(prepared_request)
 
-    assert headers.get("test") == "abc"
-    assert headers.get("Authorization") == "OAuth {}".format(access_token)
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get(
+        "Authorization"
+    ) == "OAuth {}".format(access_token)
+
+
+@responses.activate
+def test_OAuth2PasswordCredentialsBody_get_access_token_failure():
+
+    OAuth2PasswordCredentialsBody_JSON_RESPONSE = {
+        "access_token": "ABC.TOKEN.EFD",
+        "instance_url": "https://test.test/services/oauth2/token",
+        "id": "https://test.test/id/00D3N0000002eRJXAY/0055N000006PGpLQAW",
+        "token_type": "Bearer",
+        "issued_at": "1638442809550",
+        "signature": "wp1jFqVjffN2gLP3O9NvK93VBYFdgHD/FtwiYEhPrlQ=",
+    }
+
+    access_token_url = "https://test.test/services/oauth2/token"
+    check_auth_url = "http://test.test/check_auth"
+    client_id = "007"
+    client_secret = "double0"
+    username = "bond"
+    password = "jb"
+
+    # build Mock for get_access_token request
+    body = dict(
+        grant_type="password",
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+    )
+    get_access_match = [responses.matchers.urlencoded_params_matcher(body)]
+    responses.add(
+        responses.POST,
+        access_token_url,
+        status=400,
+        json=OAuth2PasswordCredentialsBody_JSON_RESPONSE,
+        match=get_access_match,
+    )
+
+    # Instanciate Auth to test
+    auth = OAuth2PasswordCredentialsBody(
+        access_token_url=access_token_url,
+        client_id=client_id,
+        client_secret=client_secret,
+        username=username,
+        password=password,
+    )
+
+    try:
+        access_token = auth.get_access_token()
+        assert False
+    except RuntimeError:
+        pass
 
 
 @responses.activate
@@ -64,32 +153,192 @@ def test_XAPIKeyAuth():
     key = "efg"
     auth = XAPIKeyAuth(name="XSuperKey", value=key)
 
-    headers = dict(test="abc")
-    auth.update(headers=headers)
+    base_headers = dict(test="abc")
 
-    assert headers.get("test") == "abc"
-    assert headers.get("XSuperKey") == key
-    assert len(headers) == 2
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", headers=base_headers
+    )
+    prepared_request = request.prepare()
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get("XSuperKey") == key
+    assert len(authenticated_prepared_request.headers) == 2
 
 
 @responses.activate
 def test_AuthorizationAuth():
-
     key = "im_bond_james_bond"
     auth = AuthorizationAuth(value=key)
 
-    headers = dict(test="abc")
-    auth.update(headers=headers)
+    base_headers = dict(test="abc")
 
-    assert headers.get("test") == "abc"
-    assert headers.get("Authorization") == key
-    assert len(headers) == 2
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", headers=base_headers
+    )
+    prepared_request = request.prepare()
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get("Authorization") == key
+    assert len(authenticated_prepared_request.headers) == 2
 
 
 def test_XSmartTokenAuth():
     key = "abc"
     auth = XSmartTokenAuth(value=key)
 
-    headers = dict(test="smart")
-    auth.update(headers=headers)
-    assert headers == {"test": "smart", "X-SmartToken": "abc"}
+    base_headers = dict(test="abc")
+
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", headers=base_headers
+    )
+    prepared_request = request.prepare()
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get("X-SmartToken") == key
+    assert len(authenticated_prepared_request.headers) == 2
+
+@responses.activate
+def test_OAuth2EmailPasswordBody_get_access_token():
+
+    OAuth2EmailPasswordBody_JSON_RESPONSE = {
+        "access_token": "ABC.TOKEN.EFD",
+        "instance_url": "https://test.test/services/signon/token",
+        "user": {'_id':'fhfh453f','email_address':'lemzo@hotemail.ww','name':'lemzo','usernme':'lemzo30','initial':'L'}
+    }
+
+    access_token_url = "https://test.test/services/signon/token"
+    check_auth_url = "http://test.test/check_auth"
+    email = "lemzo@hotemail.ww"
+    password = "jb"
+
+    # build Mock for get_access_token request
+    body = dict(
+        email=email,
+        password=password,
+    )
+    get_access_match = [responses.matchers.urlencoded_params_matcher(body)]
+    responses.add(
+        responses.POST,
+        access_token_url,
+        status=200,
+        json=OAuth2EmailPasswordBody_JSON_RESPONSE,
+        match=get_access_match,
+    )
+
+    # Instanciate Auth to test
+    auth = OAuth2EmailPasswordBody(
+        access_token_url=access_token_url,
+        email=email,
+        password=password,
+    )
+
+    access_token = auth.get_access_token()
+    assert access_token == OAuth2EmailPasswordBody_JSON_RESPONSE["access_token"]
+
+    # build Mock for check_auth request
+    header = dict(Authorization=f"{access_token}")
+    check_auth_match = [responses.matchers.header_matcher(header)]
+    responses.add(
+        responses.GET,
+        check_auth_url,
+        status=200,
+        match=check_auth_match,
+    )
+
+    # Send authenticated request
+    session = requests.Session()
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", auth=auth
+    )
+    prepared_request = request.prepare()
+    session.send(prepared_request)
+
+    prepared_request
+    prepared_request.headers.update(dict(test="abc"))
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get(
+        "Authorization"
+    ) == "{}".format(access_token)
+
+@responses.activate
+def test_OAuth2EmailPasswordBody_get_access_token_failure():
+
+    OAuth2EmailPasswordBody_JSON_RESPONSE = {
+        "access_token": "ABC.TOKEN.EFD",
+        "instance_url": "https://test.test/services/signon/token",
+        "user": {'_id':'fhfh453f','email_address':'lemzo@hotemail.ww','name':'lemzo','usernme':'lemzo30','initial':'L'}
+    }
+
+    access_token_url = "https://test.test/services/signon/token"
+    check_auth_url = "http://test.test/check_auth"
+    email = "lemzo@hotemail.ww"
+    password = "jb"
+
+    # build Mock for get_access_token request
+    body = dict(
+        email=email,
+        password=password,
+    )
+    get_access_match = [responses.matchers.urlencoded_params_matcher(body)]
+    responses.add(
+        responses.POST,
+        access_token_url,
+        status=400,
+        json=OAuth2EmailPasswordBody_JSON_RESPONSE,
+        match=get_access_match,
+    )
+
+    # Instanciate Auth to test
+    auth = OAuth2EmailPasswordBody(
+        access_token_url=access_token_url,
+        email=email,
+        password=password,
+    )
+    try:
+        access_token = auth.get_access_token()
+        assert False
+    except RuntimeError:
+        pass
+
+
+def test_MonsterBodyAuth():
+
+    username = "efg"
+    password = "hij"
+    url = "http://test.test/check_auth"
+    auth = MonsterBodyAuth(username=username, password=password)
+    job = b"<username>{username}</username>\n<password>{password}</password>"
+
+    request = requests.Request(
+        method="GET", url=url
+    )
+    request.data = job
+
+    prepared_request = request.prepare()
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert isinstance(authenticated_prepared_request.body, bytes)
+    assert authenticated_prepared_request.body == b"<username>efg</username>\n<password>hij</password>"
+
+    
+def test_XTaleezAuth():
+    key = "abc"
+    auth = XTaleezAuth(value=key)
+
+    base_headers = dict(test="abc")
+
+    request = requests.Request(
+        method="GET", url="http://test.test/check_auth", headers=base_headers
+    )
+    prepared_request = request.prepare()
+    authenticated_prepared_request = auth(prepared_request)
+
+    assert authenticated_prepared_request.headers.get("test") == "abc"
+    assert authenticated_prepared_request.headers.get("X-taleez-api-secret") == key
+    assert len(authenticated_prepared_request.headers) == 2
+
diff --git a/tests/core/test_connector.py b/tests/core/test_connector.py
new file mode 100644
index 000000000..23fdbe3bc
--- /dev/null
+++ b/tests/core/test_connector.py
@@ -0,0 +1,33 @@
+from hrflow_connectors.core.connector import Connector
+
+
+def test_Connector_pull_jobs():
+    try:
+        Connector.pull_jobs()
+        assert False
+    except NotImplementedError:
+        assert True
+
+
+def test_Connector_pull_profiles():
+    try:
+        Connector.pull_profiles()
+        assert False
+    except NotImplementedError:
+        assert True
+
+
+def test_Connector_push_job():
+    try:
+        Connector.push_job()
+        assert False
+    except NotImplementedError:
+        assert True
+
+
+def test_Connector_push_profile():
+    try:
+        Connector.push_profile()
+        assert False
+    except NotImplementedError:
+        assert True
\ No newline at end of file
diff --git a/tests/core/test_http.py b/tests/core/test_http.py
deleted file mode 100644
index dfc656941..000000000
--- a/tests/core/test_http.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import requests
-import responses
-from typing import Dict
-
-from hrflow_connectors.core.http import HTTPStream
-from hrflow_connectors.core.auth import Auth, NoAuth
-
-
-@responses.activate
-def test_HTTPStream_get():
-    request_url = "https://test.test/get"
-    body = {"test": "hello"}
-
-    # build Mock for request
-    responses.add(responses.GET, request_url, status=200, json=body)
-
-    # HTTPStream to test
-    class TestHTTPStream(HTTPStream):
-        auth: Auth = NoAuth()
-
-        @property
-        def http_method(self):
-            return "GET"
-
-        @property
-        def base_url(self):
-            return "https://test.test"
-
-        def path(self):
-            return "/get"
-
-    action = TestHTTPStream()
-    response = action.send_request()
-
-    assert isinstance(response, requests.Response)
-    assert response.status_code == 200
-    assert "application/json" in response.headers.get("content-type")
-
-    response_json = response.json()
-    assert response_json.get("test") == "hello"
-
-
-@responses.activate
-def test_HTTPStream_post():
-    request_url = "https://test.test/post"
-    body = {"test": "hello"}
-
-    # build Mock for request
-    responses.add(responses.POST, request_url, status=200, json=body)
-
-    # HTTPStream to test
-    class TestHTTPStream(HTTPStream):
-        auth: Auth = NoAuth()
-
-        @property
-        def http_method(self):
-            return "POST"
-
-        @property
-        def base_url(self):
-            return "https://test.test"
-
-        def path(self):
-            return "/post"
-
-    action = TestHTTPStream()
-    response = action.send_request()
-
-    assert isinstance(response, requests.Response)
-    assert response.status_code == 200
-    assert "application/json" in response.headers.get("content-type")
-
-    response_json = response.json()
-    assert response_json.get("test") == "hello"
-
-
-@responses.activate
-def test_HTTPStream_get_with_header():
-    request_url = "https://test.test/header"
-    body = {"test": "hello"}
-
-    # build Mock for request
-    match = [responses.matchers.header_matcher(dict(d="world !"))]
-    responses.add(responses.GET, request_url, status=200, json=body, match=match)
-
-    # HTTPStream to test
-    class TestHTTPStream(HTTPStream):
-        auth: Auth = NoAuth()
-
-        @property
-        def http_method(self):
-            return "GET"
-
-        @property
-        def base_url(self):
-            return "https://test.test"
-
-        def path(self):
-            return "/header"
-
-        def build_request_headers(self):
-            self.headers["d"] = "world !"
-
-    action = TestHTTPStream()
-    response = action.send_request()
-
-    assert isinstance(response, requests.Response)
-    assert response.status_code == 200
-    assert "application/json" in response.headers.get("content-type")
-
-    response_json = response.json()
-    assert response_json.get("test") == "hello"
diff --git a/tests/utils/test_datetime_converter.py b/tests/utils/test_datetime_converter.py
index 91c15acb0..3d3aa2bec 100644
--- a/tests/utils/test_datetime_converter.py
+++ b/tests/utils/test_datetime_converter.py
@@ -163,6 +163,13 @@ def test_simple_date_with_colons_in_timezone():
     assert got == expected
 
 
+def test_simple_date_with_colons_hours_minutes_in_timezone():
+    got = from_str_to_datetime("2021-10-11T14:57:33+01:23")
+    tz = datetime.timezone(datetime.timedelta(hours=1, minutes=23))
+    expected = datetime.datetime(2021, 10, 11, 14, 57, 33, tzinfo=tz)
+    assert got == expected
+
+
 def test_simple_date_in_HrFlow_tag():
     got = from_str_to_datetime("2021-10-13T10:57:38+0200")
     tz = datetime.timezone(datetime.timedelta(hours=2))
@@ -180,6 +187,7 @@ def test_simple_date_in_HrFlow():
 def test_wrong_date_with_letter():
     try:
         from_str_to_datetime("2021-1H-01T12:59:05+0000")
+        assert False
     except DateFormatError:
         pass
 
@@ -187,6 +195,7 @@ def test_wrong_date_with_letter():
 def test_wrong_date_with_invalid_date():
     try:
         from_str_to_datetime("2021-10-32T12:59:05+0000")
+        assert False
     except DateFormatError:
         pass
 
@@ -194,5 +203,14 @@ def test_wrong_date_with_invalid_date():
 def test_wrong_date_with_invalid_format():
     try:
         from_str_to_datetime("2021-10sssss")
+        assert False
+    except DateFormatError:
+        pass
+
+
+def test_wrong_date_with_invalid_int():
+    try:
+        from_str_to_datetime("2021-ss-ss")
+        assert False
     except DateFormatError:
         pass
\ No newline at end of file
diff --git a/tests/utils/test_event_parser.py b/tests/utils/test_event_parser.py
index 41e2e8bc9..34c7e18fd 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
+from hrflow_connectors.utils.hrflow import EventParser, Profile, Job
+
 
 def test_event_with_profile():
     request = dict(profile=dict(key="abc", source=dict(key="efg")))
@@ -9,12 +10,14 @@ def test_event_with_profile():
     assert profile.key == "abc"
     assert profile.source.key == "efg"
 
+
 def test_event_without_profile():
     request = dict(other=dict(key="abc", source=dict(key="efg")))
     event = EventParser(request=request)
     profile = event.get_profile()
     assert profile is None
 
+
 def test_event_with_profile_in_listened_source():
     request = dict(profile=dict(key="abc", source=dict(key="efg")))
     event = EventParser(request=request)
@@ -25,9 +28,46 @@ def test_event_with_profile_in_listened_source():
     assert profile.key == "abc"
     assert profile.source.key == "efg"
 
+
 def test_event_with_profile_not_in_listened_source():
     request = dict(profile=dict(key="abc", source=dict(key="efg")))
     event = EventParser(request=request)
     source_to_listen = ["xyz", "hij"]
     profile = event.get_profile(source_to_listen=source_to_listen)
-    assert profile is None
\ No newline at end of file
+    assert profile is None
+
+
+def test_event_with_job():
+    request = dict(job=dict(key="abc", board=dict(key="efg")))
+    event = EventParser(request=request)
+    job = event.get_job()
+    assert job is not None
+    assert isinstance(job, Job)
+    assert job.key == "abc"
+    assert job.board.key == "efg"
+
+
+def test_event_without_job():
+    request = dict(other=dict(key="abc", board=dict(key="efg")))
+    event = EventParser(request=request)
+    job = event.get_job()
+    assert job is None
+
+
+def test_event_with_job_in_listened_board():
+    request = dict(job=dict(key="abc", board=dict(key="efg")))
+    event = EventParser(request=request)
+    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 job.key == "abc"
+    assert job.board.key == "efg"
+
+
+def test_event_with_job_not_in_listened_board():
+    request = dict(job=dict(key="abc", board=dict(key="efg")))
+    event = EventParser(request=request)
+    board_to_listen = ["xyz", "hij"]
+    job = event.get_job(board_to_listen=board_to_listen)
+    assert job is None
\ No newline at end of file
diff --git a/tests/utils/test_job_and_board.py b/tests/utils/test_job_and_board.py
new file mode 100644
index 000000000..4b888c508
--- /dev/null
+++ b/tests/utils/test_job_and_board.py
@@ -0,0 +1,19 @@
+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_logger.py b/tests/utils/test_logger.py
new file mode 100644
index 000000000..2eacd3878
--- /dev/null
+++ b/tests/utils/test_logger.py
@@ -0,0 +1,34 @@
+import logging
+from hrflow_connectors.utils.logger import (
+    LOGGER_NAME,
+    get_logger,
+    get_logger_with_basic_config,
+)
+
+
+def test_get_logger():
+    # We need to disable propagation
+    # so that we don't have the properties
+    # like the list of handlers of the parent
+    logger = get_logger()
+    logger.propagate = False
+
+    logger = get_logger()
+    assert logger.hasHandlers()
+    assert len(logger.handlers) == 1
+    assert isinstance(logger.handlers[0], logging.NullHandler)
+
+
+def test_get_logger_with_basic_config():
+    # We need to disable propagation
+    # so that we don't have the properties
+    # like the list of handlers of the parent
+    logger = get_logger()
+    logger.propagate = False
+
+    logger = get_logger_with_basic_config()
+    assert len(logger.handlers) == 2
+
+    handler_class_list = [handler.__class__ for handler in logger.handlers]
+    assert logging.StreamHandler in handler_class_list
+    assert logging.NullHandler in handler_class_list
\ No newline at end of file
diff --git a/tests/utils/test_timedelta_converter.py b/tests/utils/test_timedelta_converter.py
index f96efeef2..7ae4f52b5 100644
--- a/tests/utils/test_timedelta_converter.py
+++ b/tests/utils/test_timedelta_converter.py
@@ -42,6 +42,7 @@ def test_simple_neg_timedelta_with_one_day_hours_millisecond():
 def test_wrong_timedelta_with_days_hours_millisecond_space_missing():
     try:
         assert_datetime("-1803days, 15:54:03.999990")
+        assert False
     except TimeDeltaFormatError:
         pass
 
@@ -49,6 +50,7 @@ def test_wrong_timedelta_with_days_hours_millisecond_space_missing():
 def test_wrong_timedelta_with_days_hours_millisecond_with_letter():
     try:
         assert_datetime("-HH days, 15:54:03.999990")
+        assert False
     except TimeDeltaFormatError:
         pass
 
@@ -56,6 +58,7 @@ def test_wrong_timedelta_with_days_hours_millisecond_with_letter():
 def test_wrong_timedelta_with_days_hours_millisecond_with_plus():
     try:
         assert_datetime("+01 days, 15:54:03.999990")
+        assert False
     except TimeDeltaFormatError:
         pass
 
@@ -63,5 +66,13 @@ def test_wrong_timedelta_with_days_hours_millisecond_with_plus():
 def test_wrong_timedelta_with_days_hours_without_seconds():
     try:
         assert_datetime("1 days, 15:54")
+        assert False
+    except TimeDeltaFormatError:
+        pass
+
+def test_wrong_timedelta_with_invalid_int():
+    try:
+        assert_datetime("1 days, 15:54:ss")
+        assert False
     except TimeDeltaFormatError:
         pass
\ No newline at end of file