This repository contains scripts to automate projects and tasks synchronization between TimeCamp and other systems:
- Synchronizing clients, projects and tasks into TimeCamp
- Exporting time entries from TimeCamp
- Synchronizing meandatory tags and assigned users
-
Clone this repository:
git clone https://github.com/timecamp-org/script-timecamp-projects-sync.git cd script-timecamp-projects-sync -
Install the required dependencies:
pip install -r requirements.txt
-
Set up the environment variables:
- Copy
.env.exampleto.env - Fill in required environment variables and TimeCamp API credentials and other configuration in
.env
- Copy
To set up automatic daily synchronization, you can use a task scheduler like cron (Linux/macOS) or Task Scheduler (Windows).
tasks.json is a JSON array of source tasks. This example contains every key
consumed by sync_projects.py:
[
{
"name": "Acme",
"task_id": "client_acme",
"parent_id": 0
},
{
"name": "Build reporting dashboard",
"task_id": "project_42",
"parent_id": "client_acme",
"restore_if_archived": true,
"original_estimate_seconds": 7200,
"mandatory_tags": {
"Client": ["Acme"],
"Work type": ["Development", "Reporting"]
},
"assigned_users": {
"source_user_id_123": {
"email": "developer@example.com",
"username": "Developer Name"
}
},
"custom_fields": {
"456": "Client",
"789": "W-2"
}
}
]restore_if_archived is optional and defaults to true. When the archive
sync action is enabled, the sync archives TimeCamp tasks that are missing from
the source data and restores archived TimeCamp tasks that are present in it
again. For example, an item that goes to Done is archived, and it comes back
when it moves back to an active status. Set restore_if_archived to false on
a task to keep it archived when someone archived it manually in TimeCamp while
it is still active in the source. If TimeCamp has an active and an archived
task with the same external ID, the sync uses the active task.
custom_fields maps TimeCamp custom field template ids to values. During sync
the engine assigns each value with
POST /v3/custom-fields/{template_id}/assign/{task_id}.
python3 fetch_harvest.py
python3 sync_projects.py
python3 export_time_entries_harvest.py 2026-03-19 2026-03-19python3 fetch_toggl_json.py projects.json
python3 sync_projects.pypython3 fetch_redmine_and_sync.py
python3 export_time_entries_redmine.py 2026-03-19 2026-03-19python3 fetch_azuredevops.py
python3 sync_projects.pyThe fetcher makes each TimeCamp external task ID from the organization URL,
for example sync_org_955312_1234 for work item 1234. The same URL always
gives the same ID. Letter case, spaces at the start or end, and a / at the
end of the URL do not change the ID. Thus the sync finds the same TimeCamp
task on each run. A work item that goes to Done is archived, and the sync
restores the same task when the work item becomes active again.
If you change the organization URL, the IDs change too. An example is a move
from https://acme.visualstudio.com to https://dev.azure.com/acme. The next
sync then archives all tasks of that organization and creates them again.
One-time change after you update. Older versions made a new random ID on
each run. Thus each sync archived all Azure DevOps tasks from the previous run
and created new copies. The first sync after the update does this one last
time: it archives the tasks with the old random IDs and creates tasks with
stable IDs. After that, the sync keeps the same tasks. This also applies if
you set PYTHONHASHSEED to keep the old IDs stable. You can remove that
setting after the update. Archived tasks keep their time entries.
python3 fetch_jira.py
python3 sync_projects.py
# The first run backfills today plus the previous 30 calendar days;
# later runs export entries changed or deleted since the saved cursor
uv run --env-file .env --with-requirements requirements.txt python export_time_entries_jira.py
# Use explicit dates when older history must also be exported
uv run --env-file .env --with-requirements requirements.txt python export_time_entries_jira.py --from 2026-08-01 --to 2026-08-10
# Preview without changing Jira or exporter state
uv run --env-file .env --with-requirements requirements.txt python export_time_entries_jira.py --dry-run
# Preview a two-day export for exactly one TimeCamp user
uv run --env-file .env --with-requirements requirements.txt python export_time_entries_jira.py --from 2026-08-09 --to 2026-08-10 --user-email person@example.com --dry-run
# Export every user configured in JIRA_USER_API_TOKENS
uv run --env-file .env --with-requirements requirements.txt python export_time_entries_jira.py --all-usersBy default, Jira issues in Done, Closed, Resolved, or Completed stay
active in TimeCamp for 14 days after their Jira status category changed. Set
JIRA_ARCHIVING_DELAY_DAYS to a non-negative whole number to change the delay.
Set it to 0 to archive completed issues on the next synchronization. The sync
also restores a Jira task that is present in the fetched data but was archived
in TimeCamp earlier.
Set JIRA_USER_API_TOKENS to JSON keyed by user email and Jira base URL, such as
{"person@example.com":{"https://one.atlassian.net":"token-one","https://two.atlassian.net":"token-two"}},
to authenticate a filtered --user-email export as that Jira user. --all-users
runs the same filtered export sequentially for every configured email and keeps
a separate state file for each user. It continues after individual failures and
returns a non-zero exit code if any user fails. A newly configured user with no
state is automatically backfilled for 31 calendar dates on the next undated
run. A string value remains shorthand for one token used on every instance.
Missing instance mappings and exports with neither --user-email nor
--all-users fall back to the root credentials in JIRA_INSTANCES. The
selected token owner is the Jira worklog author. The exporter records the
original TimeCamp user's display name and email in the comment. It updates,
moves, or deletes Jira worklogs when the source changes and skips Jira writes
when the canonical payload is unchanged. See
docs/jira.md and
docs/time-entry-sync.md for state, recovery,
permissions, and the reusable adapter contract.
python3 fetch_zendesk.py
python3 sync_projects.pySet DATADOG_API_KEY and DATADOG_APP_KEY. For a Datadog site outside US1,
also set DATADOG_SITE to its domain, such as datadoghq.eu.
python3 fetch_datadog.py --output tasks.json
python3 sync_projects.py --input tasks.jsonThe fetcher imports active Datadog Case Management cases under their projects and active or stable incidents under their alphabetically first affected service. Closed, resolved, completed, and archived records are omitted so the TimeCamp sync can archive work that is no longer active.
See docs/datadog.md for the hierarchy and short external ID
contract, required Datadog permissions, and local usage.
uv run --python 3.13 --with-requirements requirements.txt python fetch_timecamp.py --output task_tc.json
uv run --env-file .env --python 3.13 --with-requirements requirements.txt python sync_projects.py --input task_tc.jsonpython3 fetch_mondaycom.py
python3 sync_projects.py
uv run --with-requirements requirements.txt python fetch_mondaycom.py
uv run --with-requirements requirements.txt python sync_projects.py
# Export TimeCamp totals back to Monday.com Time Tracked numbers columns.
# Dry-run by default. Main rows are not updated because Monday should tally subitems.
uv run --with-requirements requirements.txt python fetch_mondaycom.py
uv run --with-requirements requirements.txt python export_monday_time_logged.py --from 2026-06-01 --to 2026-06-18 --column-title "Time Tracked"
uv run --with-requirements requirements.txt python export_monday_time_logged.py --from 2026-06-01 --to 2026-06-18 --column-title "Time Tracked" --apply
# Only use this if main rows should be overwritten by the exporter too.
uv run --with-requirements requirements.txt python export_monday_time_logged.py --from 2026-06-01 --to 2026-06-18 --column-title "Time Tracked" --include-main-rowsBy default, sync_projects.py runs all actions: creating missing tasks, updating changed
names and estimates, archiving stale tasks, creating/restoring mandatory tag lists and
tags, assigning mandatory tags to tasks, and assigning users to tasks.
Set TIMECAMP_SYNC_ACTIONS to a comma-separated list to run only selected actions:
# Only create/restore tags, assign mandatory tags, and assign users.
TIMECAMP_SYNC_ACTIONS=tags,mandatory_tags,users uv run --env-file .env --with-requirements requirements.txt python sync_projects.pyAvailable actions are tasks, names, estimates, archive, tags,
mandatory_tags, and users. The names action updates a TimeCamp task only
when its name differs from the source name. The estimates action copies non-null
original_estimate_seconds values to TimeCamp task hour budgets. Both actions
compare against data already returned by the bulk task request and only send
updates for changed values.
Run only task-name synchronization:
TIMECAMP_SYNC_ACTIONS=names uv run --env-file .env --with-requirements requirements.txt python sync_projects.pyRun only estimate synchronization:
TIMECAMP_SYNC_ACTIONS=estimates uv run --env-file .env --with-requirements requirements.txt python sync_projects.pyMandatory tag assignment checks use a local cache at
data/timecamp_mandatory_tag_cache.json by default. The first run still checks
TimeCamp task tags to seed the cache, using TimeCamp's internal project-list tag
payload in bulk when available instead of reading tags one task at a time. Later
runs skip unchanged tasks when the TimeCamp task id, task modify_time, and
desired mandatory tag ids still match. Set TIMECAMP_MANDATORY_TAG_CACHE_FILE
to use a different cache path.
Enable strict user sync when TimeCamp task assignees should exactly match the
source JSON. This removes direct TimeCamp user assignments from synced tasks
when those users are missing from assigned_users in the JSON file:
TIMECAMP_STRICT_USER_SYNC=true TIMECAMP_SYNC_ACTIONS=users uv run --env-file .env --with-requirements requirements.txt python sync_projects.py
uv run --env-file .env --with-requirements requirements.txt python sync_projects.py --strict-user-syncStrict mode only removes direct assignments on the task being synced. Inherited assignments from parent tasks are left alone.
# Move all root level projects/tasks as a subtask
uv run --env-file .env --with-requirements requirements.txt python helpers/archive.py --subtask-of {task_id} --dry-run
# Assign random colors to root level tasks
python3 helpers/assign_random_apple_colors.py --dry-run
uv run --env-file .env --with-requirements requirements.txt python helpers/assign_random_apple_colors.py --dry-run
# Batch assign users to selected tasks
uv run --env-file .env --with-requirements requirements.txt python helpers/assign_users_to_task.py --task-ids 34523534,34523535 --user-ids 364263,364264
# Batch assign users to all root level tasks
uv run --env-file .env --with-requirements requirements.txt python helpers/assign_users_to_task.py --user-ids 364263,364264
# Fill missing mandatory tags from tasks.json on time entries for all account users
uv run --env-file .env --with-requirements requirements.txt python helpers/assign_mandatory_tags_to_time_entries.py --from 2026-06-01 --to 2026-06-05 --dry-run
uv run --env-file .env --with-requirements requirements.txt python helpers/assign_mandatory_tags_to_time_entries.py --from 2026-06-01 --to 2026-06-05Contributions are welcome! Please feel free to submit a Pull Request.
MIT