FXCode
SIGN IN →
Doc. 10 — Features

Import tasks

Migrate your tasks from Notion (or any other tool) with their comments, attachments, people and original dates, without duplicates when you re-import.

Bring your tasks over exactly as they were. An import creates or updates a project’s tasks from a JSON file: with their Markdown description, status, priority, environment and version, who they’re assigned to and who requested them, their links, their comments (with author and date) and their files. Creation, last-edited and completion dates are preserved.

It’s idempotent: each task is identified by its source (e.g. notion) and its externalId. Importing the same file again updates those tasks instead of duplicating them. So you can migrate, review, and import again with a corrected mapping.

In two steps

# 1. Dry run: writes nothing. Reports what would be created or updated and what wasn't mapped
fxc tasks import notion.json --project-id <id> --dry-run

# 2. The real import (running it again updates, never duplicates)
fxc tasks import notion.json --project-id <id>

The CLI validates the file, uploads the local images embedded in the Markdown, imports the tasks and then uploads their attachments. Files already on the task (same name and size) are skipped when you re-import.

From an assistant, the MCP tool import_tasks does the same ({ file, projectId, dryRun }). Over HTTP it’s POST /projects/:id/tasks/import (add ?dryRun=true for a dry run); you need to be on the project’s team. The API doesn’t receive files: only the CLI understands local paths.

The file

{
  "version": 1,
  "source": "notion",
  "statusMap": { "Not started": "No Iniciada", "In progress": "Working", "Completed": "Terminada" },
  "priorityMap": { "High": "Alta", "Medium": "Media", "Low": "Baja" },
  "createMissingPeople": true,
  "tasks": [
    {
      "externalId": "1a2b3c",
      "externalUrl": "https://www.notion.so/acme/Login-roto-1a2b3c",
      "title": "El login falla con Safari",
      "description": "Pasos:\n\n| Paso | Resultado |\n|---|---|\n| Abrir /login | OK |\n| Enviar | Error 500 |\n\n![captura](./notion-import/captura.png)",
      "status": "In progress",
      "priority": "High",
      "environment": "producción",
      "version": "1.4.0",
      "tags": ["auth", "safari"],
      "assignee": "Leonardo",
      "requester": "Alexander Navarro",
      "createdAt": "2025-02-10T09:30:00Z",
      "updatedAt": "2025-02-12T16:00:00Z",
      "links": [{ "url": "https://github.com/acme/web/issues/42", "label": "Issue" }],
      "comments": [
        {
          "author": "Leonardo",
          "createdAt": "2025-02-11T10:00:00Z",
          "content": "Lo reproduzco. Adjunto el informe.",
          "attachments": ["./notion-import/informe.docx"]
        }
      ],
      "attachments": ["./notion-import/datos.xlsx"]
    }
  ]
}

There’s a full five-task example in the repository: docs/examples/notion-import.json.

File level

Field Type Required What it does
version 1 no Format version.
source text (≤ 50) no (import) Default source of the tasks: notion, jira, trello… Part of the idempotency key.
statusMap object no Status name in the source → status name in FXCode.
priorityMap object no Same, for priorities.
createMissingPeople boolean no (false) Creates anyone who isn’t found as a person without an account. Without it, they’re only reported.
tasks list yes 1 to 500 tasks.

Each task

Field Type Required Notes
externalId text (≤ 200) yes The task’s id in the source. Unique per source within the file.
source text no Overrides the file’s source for this task.
externalUrl http(s) URL no Link to the original task.
title text (1–500) yes
description Markdown (≤ 50,000) no Tables, quotes, lists and images.
status text no Translated with statusMap; if it’s not there, it’s looked up as is.
priority text no Translated with priorityMap.
environment text (≤ 100) no E.g. production, staging.
version text (≤ 100) no E.g. 1.4.0.
tags list of texts no Up to 30. On re-import it replaces the tags.
kind TASK | IDEA no Task by default.
assignee email or name no See people.
requester email or name no Who requested it.
createdAt, updatedAt, completedAt ISO 8601 date no 2025-02-10 or 2025-02-10T09:30:00Z.
links list of { url, label? } no Up to 50. Missing ones are added (by URL) and their label is updated.
comments list no Up to 200. See comments.
attachments list of paths no Up to 20 local files (≤ 10 MB each), relative to the JSON.

Whatever the file doesn’t include is left untouched on update: a task re-imported without description keeps the one it had. Fields that aren’t part of the format are ignored and show up in the report.

Comments

Field Type Required Notes
content Markdown (≤ 20,000) yes Can embed local images.
author email or name no Kept even if they don’t have an account.
createdAt ISO 8601 date no The original date.
externalId text no The comment’s key. Without it, one is derived from author + date + content.
attachments list of paths no Up to 10 files, attached to that comment.

Comments are idempotent too: re-importing the same comment updates it. If you change its text in the source and it has no externalId, it comes in as a new one.

Statuses and priorities

statusMap translates the source’s names into yours, ignoring case and accents. A status that doesn’t map leaves the task without a status (or with the one it had, if it already existed) and shows up in the report. If a task arrives in a finished status without completedAt, it’s dated with its updatedAt.

Priorities work the same way. If your priority catalog is empty, the API creates Alta, Media and Baja on startup; you can also seed them yourself (it’s idempotent: if any priority already exists, it does nothing):

fxc settings priorities seed
fxc settings priorities list

People

assignee, requester and each comment’s author accept an email or an exact name (ignoring case and accents; first name or full name both work):

  • By email, the whole people directory is searched.
  • By name, among the project’s team and the people already attached to its tasks (so a person created in a previous import is recognized when you re-import). If two people share a name, nothing is guessed: it shows up as ambiguous.
  • If no one is found and createMissingPeople is true, they’re created as a person without an account. The task shows “Assigned to Leonardo” or “Requested by Alexander Navarro” just as it would with an account.
  • Without that flag, the field is left empty and the person shows up in the report as missing.

If the person has an account, the task is also assigned to their user (it appears in “my tasks”).

Images and attachments

  • Embedded images: the CLI looks for ![text](path) with local paths in descriptions and comments, uploads them as public images (png, jpg, webp, gif or avif, up to 5 MB) and rewrites the URL. Ones that are already https:// are left as they are. Paths with spaces work written as-is inside <…> or URL-encoded the way Notion exports them (image%203.png).

  • Attachments: those in attachments are uploaded to the task (or comment) after it’s imported. They’re private: they’re downloaded from the task through a temporary link. Any file type (docx, xlsx, pdf, zip…), up to 10 MB.

  • Dry-run: uploads nothing. The report says how many images and attachments would be uploaded (wouldUpload).

Re-importing without undoing your cleanup

Re-importing the same file updates existing tasks with everything in the file: title, description, status, priority, tags, assignee… If you edited tasks by hand after importing, limit what gets updated with --fields (or fields in the file). New tasks are still created in full.

# Add comments and original dates without touching titles, statuses or tags
fxc tasks import notion.json --project-id <id> --fields comments,dates --dry-run
fxc tasks import notion.json --project-id <id> --fields comments,dates

Fields: title, description, status, priority, environment, version, tags, kind, externalUrl, assignee, requester, dates (createdAt, updatedAt and completedAt), links, comments and attachments. With fields, the CLI only uploads the images and attachments that will actually be written.

Cleaning up after importing

An import rarely lands clean: duplicates, stale tasks, tags that no longer mean anything. Tidy it up from the CLI (or by selecting several tasks in the web app):

# Archive what's no longer relevant: out of lists, project progress and the MCP (nothing is deleted)
fxc tasks archive <id> <id> …
fxc tasks list --project-id <id> --archived          # review what you archived (unarchive to undo)

# The same changes to many tasks at once
fxc tasks update --ids a,b,c --status "No Iniciada" --priority Media --add-tag migrated --remove-tag notion-old
fxc tasks update --ids a,b --assigned-person-id <personId>   # a person without an account

# Merge duplicates into one: comments, attachments, links, notes and requests move to the target
fxc tasks merge <targetId> <duplicateId> <duplicateId>

# Order the work: relations, subtasks and the queue
fxc tasks link-task <id> --blocked-by <otherId>
fxc tasks update <id> --parent-id <parentId>
fxc tasks queue <id> --first

Re-importing respects your cleanup. An archived task stays archived when you re-import, and so does a merged one: the source task keeps its source + externalId, so the import updates it (archived) instead of creating it again. Combine that with --fields (for example --fields comments,dates) so a re-import doesn’t overwrite the statuses, tags or titles you fixed by hand. More in Kanban, queue and relations.

The report

The dry run and the import return the same thing (as is, with --json):

{
  "dryRun": true,
  "summary": { "total": 5, "create": 4, "update": 1 },
  "tasks": [{ "externalId": "1a2b3c", "source": "notion", "action": "update", "title": "…", "taskId": "cm…" }],
  "unmapped": {
    "statuses": [{ "name": "Blocked", "count": 2 }],
    "priorities": [],
    "people": [{ "ref": "Leonardo", "count": 3, "resolution": "will-create" }],
    "fields": [{ "field": "tasks[].Sprint", "count": 5 }]
  },
  "warnings": []
}

resolution is missing, ambiguous, will-create (dry run with createMissingPeople) or created. The CLI adds how many images and attachments it would upload or uploaded, and which ones don’t exist on disk.

The import sends no emails and no per-task notifications: when it finishes, open lists refresh just once.

Limits

Limit Value
Tasks per file 500
Title 500 characters
Description 50,000 characters (same as creating a task by hand)
Tags per task 30
Links per task 50
Comments per task 200 (20,000 characters each)
Attachments 20 per task, 10 per comment, 10 MB each
statusMap / priorityMap entries 100

More than 500 tasks? Split the file: since the key is source + externalId, you can import it in parts, in any order.

From Notion

Export the database as Markdown & CSV and turn each row into a task in the format above (a short script or your assistant can do it): the page id goes in externalId, its URL in externalUrl, the status column in status and the created-date column in createdAt. The export’s images end up in folders next to the Markdown: reference them with paths relative to the JSON and the CLI uploads them.