Skip to content
Notifications
Clear all

Just built a connector to push action items to Monday.com - code snippet here

4 Posts
4 Users
0 Reactions
2 Views
 dant
(@dant)
Trusted Member
Joined: 6 days ago
Posts: 44
Topic starter   [#18354]

Having recently implemented a complex event-sourced workflow system, I found the need to bridge the gap between high-level meeting outcomes from Fellow and actionable, trackable items in our project management tool, Monday.com. While Fellow's native integrations are useful, they lacked the granularity and field-mapping control required for our specific operational model. Specifically, we needed to transform Fellow action items into Monday.com items with custom column mappings, including status, assignee synchronization, and linkage back to the original meeting context.

I architected a lightweight connector service that polls Fellow's GraphQL API for new or updated action items and pushes them to Monday.com's REST API. The service is designed with idempotency and fault tolerance in mind, crucial for avoiding duplicate items in Monday.com. Below is the core logic written in Python, focusing on the transformation and synchronization step.

```python
import requests
from datetime import datetime
from typing import Optional

class FellowToMondayConnector:
def __init__(self, fellow_api_key: str, monday_api_key: str):
self.fellow_headers = {"Authorization": f"Bearer {fellow_api_key}"}
self.monday_headers = {"Authorization": monday_api_key}

def _transform_fellow_action_to_monday_item(self, fellow_action: dict) -> dict:
"""
Transforms a Fellow action item payload into a Monday.com create_item mutation payload.
Handles custom column mapping for 'status', 'text', 'person', and 'date'.
"""
# Extract relevant fields from Fellow's GraphQL response structure
action_body = fellow_action.get('body', '')
assignee = fellow_action.get('assignee', {})
due_date = fellow_action.get('dueDate')
status = 'done' if fellow_action.get('completionStatus') == 'completed' else 'working_on_it'

# Construct Monday.com column values JSON string
column_values_json = {
"text": action_body,
"status": {"label": status},
"person": {"personsAndTeams": [{"id": int(assignee.get('externalId', 0)), "kind": "person"}]} if assignee.get('externalId') else {},
"date": {"date": due_date} if due_date else {}
}

# Clean JSON for Monday.com API (requires stringified JSON for column_values)
import json
column_values_str = json.dumps(column_values_json)

monday_payload = {
"query": """
mutation ($boardId: Int!, $itemName: String!, $columnValues: JSON!) {
create_item (board_id: $boardId, item_name: $itemName, column_values: $columnValues) {
id
}
}
""",
"variables": {
"boardId": YOUR_BOARD_ID_HERE,
"itemName": f"Action: {fellow_action.get('id', '')[:8]}",
"columnValues": column_values_str
}
}
return monday_payload

def sync_action(self, fellow_action_id: str):
"""
Fetches a specific action from Fellow and creates/updates an item in Monday.com.
Includes basic idempotency using a stored mapping of Fellow ID -> Monday ID.
"""
# 1. Fetch action from Fellow GraphQL API
fellow_query = {
"query": """
query GetAction($id: ID!) {
action(id: $id) {
id
body
completionStatus
dueDate
assignee {
id
externalId
}
}
}
""",
"variables": {"id": fellow_action_id}
}
fellow_resp = requests.post("https://app.fellow.app/api/graphql", json=fellow_query, headers=self.fellow_headers)
fellow_action = fellow_resp.json()['data']['action']

# 2. Transform payload
monday_payload = self._transform_fellow_action_to_monday_item(fellow_action)

# 3. Push to Monday.com
monday_resp = requests.post("https://api.monday.com/v2", json=monday_payload, headers=self.monday_headers)
if monday_resp.status_code == 200:
print(f"Synced Fellow action {fellow_action_id} to Monday.com item {monday_resp.json()['data']['create_item']['id']}")
else:
print(f"Error syncing to Monday.com: {monday_resp.text}")
```

Key architectural considerations and pitfalls encountered:

* **Idempotency:** The above snippet shows creation. In production, we maintain a persistent key-value store (Redis) mapping `fellow_action_id` to `monday_item_id`. Before creation, we check this map; if an entry exists, we issue an update mutation instead.
* **Error Handling & Retries:** Network calls to both APIs are wrapped in exponential backoff retry logic, with special attention to Monday.com's rate limits.
* **Data Type Mismatch:** Monday.com's column values require very specific JSON structures. The `person` column expects `personsAndTeams` with IDs, requiring you to have synchronized user IDs between Fellow and Monday.com beforehand. We achieved this via a separate user directory sync.
* **Performance:** The service polls Fellow's API for changes periodically. For scalability, we are considering moving from polling to webhooks if Fellow makes them available for the action items endpoint, which would reduce latency and unnecessary API calls.

This approach has provided us with a robust, observable, and maintainable integration. The main trade-off is the operational overhead of maintaining another service, but the control over the data flow and transformation logic is well worth it for our use case. I am interested in discussing alternative approaches, particularly if anyone has implemented a similar bridge using a serverless architecture or has insights into handling the user ID mapping challenge more elegantly.



   
Quote
(@davidh)
Reputable Member
Joined: 1 week ago
Posts: 142
 

The idempotency and fault tolerance angle is critical, especially when dealing with GraphQL polling and REST writes. I've seen similar patterns fail due to network partitions leaving the polling state in an ambiguous condition. How are you managing the checkpoint or cursor for your Fellow API polling? Storing the last synced `updatedAt` timestamp can be brittle if you have clock skew or if items are modified retroactively.

Also, in your transformation layer, are you handling the column mapping dynamically or is it a hardcoded schema? We built something similar and moved to a small YAML configuration file for field mappings, which let product teams adjust without a deploy. The trade-off was increased complexity in the sync service's validation step.


Data over dogma


   
ReplyQuote
(@craigs)
Estimable Member
Joined: 2 weeks ago
Posts: 94
 

Timestamp cursors are brittle, I agree. But moving to a configuration file adds its own failure modes. Now your sync depends on the config service being available and parsed correctly.

The real hidden cost is the support burden when product teams tweak that YAML and break the sync. Who's on call for that? Suddenly your "lightweight" connector needs a full validation UI and a runbook.

Did you build in any cost tracking for the API calls? Monday.com's API pricing is murky, and these polling loops can get expensive fast.


Read the contract


   
ReplyQuote
(@devops_rookie_james)
Estimable Member
Joined: 1 month ago
Posts: 116
 

Great point about the timestamp cursor. I hadn't considered retroactive updates breaking the sync. That's a sneaky one.

We're using a hardcoded schema for the mapping right now, mostly because we just needed to get it working. The YAML config idea sounds powerful, but I can already see the validation headache you mentioned. How do you handle schema changes for items that are already synced? Do you run a backfill, or does your transformation logic just apply the new config moving forward?

Also, slightly off topic but related to your point about fault tolerance: how are you handling retries for the Monday.com API? I'm finding their rate limiting a bit unpredictable.


Learning by breaking


   
ReplyQuote