How to Turn a Gantt Spreadsheet Into a Project Tracker Your Team Can Query [Full Repo]

A Gantt chart shows you the plan. A database answers the question you actually have, which is what slipped and what that stops — and the dependency column is where it is hiding.

The four questions the imported schedule answers as of 9 October 2026: two activities late, two in flight, two milestones ahead, and three activities blocked because they depend on something late

A Gantt chart is very good at showing you the plan and very bad at answering questions about it. The bars are the point, and the bars are also why you end up tracing dependencies with a finger on a screen.

The question you actually have on a Monday is narrower: what slipped, and what is now stuck behind it. A spreadsheet can show you the first with conditional formatting. The second it makes you work out by eye.

This guide moves a twelve-row activity schedule into a database and asks it both. The import took 502 ms; the four queries a tracker needs came back in under half a second each.

Architecture: a Gantt spreadsheet becomes one Activity class whose dates are stored as dates and whose dependencies are an array, which four queries then read to answer what is late, what is running, which milestones remain and what is blocked

What does a schedule look like once it is rows?

Eight columns, and only two of them are interesting. A Gantt row is an activity with a start, an end and a percentage; what makes it a schedule rather than a list is the column that says what it waits for.

id  name                   owner           start       end         progress  dependsOn  milestone
5   Import pipeline        Tomas Vidal     2026-09-22  2026-10-03  80                4  false
6   Permissions and roles  Nadia Okonkwo   2026-09-28  2026-10-07  45                4  false
9   Beta with two teams    Nadia Okonkwo   2026-10-12  2026-10-24   0              6,7  true

dependsOn is the column people leave out when they move a spreadsheet into a tool, because the chart draws the arrows for them. Lose it and you keep the picture and throw away the only thing that answers “so what?”.

Which column types actually matter?

Two: the dates must be dates, and the dependencies must be an array. Everything else can be whatever is convenient.

// Stack: Node 22 | File: import-and-query.mjs
{
  activityId: { type: 'Number' },
  name:       { type: 'String' },
  owner:      { type: 'String' },
  start:      { type: 'Date' },      // not String
  end:        { type: 'Date' },      // not String
  progress:   { type: 'Number' },
  dependsOn:  { type: 'Array' },     // not a comma-separated String
  milestone:  { type: 'Boolean' },
}

Dates as text look fine until you compare them. 2026-10-9 and 2026-10-09 are different strings, sort differently, and neither one compares correctly against a real date — so “what is late” silently returns the wrong rows rather than failing.

Dependencies as text have the same shape of problem. "6,7" cannot be searched for membership; as an array it can, and that is the whole second half of this post.

How we measured: the class was created with one POST /schemas/Activity and returned HTTP 200 in 3.1 seconds, reporting back the eight fields plus the four Parse adds.

How do you get twelve rows in at once?

One batch request, not twelve round trips. The batch endpoint takes a list of operations and runs them server-side:

await fetch(`${PARSE}/batch`, {
  method: 'POST', headers: asServer,
  body: JSON.stringify({
    requests: rows.map((body) => ({ method: 'POST', path: '/classes/Activity', body })),
  }),
});

That imported all twelve in 502 ms. Twelve separate requests from the same machine would have cost twelve round trips; this costs one.

How we measured: one POST /batch carrying twelve create operations, timed around the single call, returning twelve success objects.

What is late, and what does that block?

Two queries, and the second is the one a spreadsheet cannot do for you. Late is a straightforward intersection — the end date has passed and the work is not finished:

{ "end": { "$lt": { "__type": "Date", "iso": "2026-10-09T00:00:00.000Z" } },
  "progress": { "$lt": 100 } }

As of 9 October that returned two activities: the import pipeline at 80% and permissions at 45%. Four other activities had end dates in the past and were correctly left out, because they were finished.

Then the question the chart makes you trace by hand. Take the ids of the late activities and ask which rows depend on any of them:

{ "dependsOn": { "$in": [5, 6] } }

Three activities came back — the reporting dashboard, the beta and the security review. Two slipped tasks are holding up three more, and nobody had to follow an arrow to find out.

The chart shows you the dependency. The array lets you ask it a question.

What else does a tracker need to answer?

Two more queries, both one line, and together with the first two they cover the Monday meeting:

QuestionThe queryAs of 9 Oct
What is late?end < today and progress < 1002 — 466 ms
What is running now?start <= today <= end2 — 278 ms
Which milestones are left?milestone = true and end >= today2 — 168 ms
What is blocked?dependsOn $in [late ids]3 — 170 ms

How we measured: each query timed individually against the imported class, warm, from a laptop to the USA East region. The first is slower because it is the first — the connection is cold.

Are those answers right?

They are, and that is worth checking rather than assuming. The same four questions were recomputed from the original CSV in Python, with no database involved:

                  from the database      from the spreadsheet
late              2  [5, 6]              2  [5, 6]
in flight         2  [7, 8]              2  [7, 8]
milestones ahead  2  [9, 12]             2  [9, 12]
blocked           3  [8, 9, 10]          3  [8, 9, 10]

Identical, including the exact activity ids. The interesting row is the third: an architecture sign-off milestone on 16 September is correctly absent from both, because it is behind us — a query that counted all milestones would have said three, and been useless.

How we measured: verify.py in the companion repository reads activities.csv and applies the same four rules in plain Python; the database numbers come from the run above.

Which errors will you hit importing a schedule?

Three, and the first one is the one that costs an afternoon.

What you seeWhat it means
{"code":107,"error":"cannot route POST /parse/classes/Activity"}the batch path carries a /parse prefix it should not
Dates import, but $lt returns everything or nothingthe column is a String; recreate it as Date and re-import
dependsOn queries return nothing with $inthe column is a String like "6,7", not an array

None of the three is a validation error, which is what makes them slow to find: the import succeeds, the data looks right in the browser, and only the queries behave oddly. If a date comparison returns a number you cannot explain, check the column type before you check the query.

When is a spreadsheet still the better tool?

While the plan is still changing shape. Dragging a bar is faster than a migration, and a schedule that is being argued over does not benefit from a schema — the looseness is the feature.

It is also fine to stay in the sheet when nobody else reads it. The case for moving is multiple people asking different questions of the same plan at once; for one person with one view, the chart is already the answer.

And if what you need is the picture rather than the questions, keep the chart. Nothing here draws a Gantt — it answers queries about one. A tracker that cannot show the bars will lose to a spreadsheet that can, however good its data model is.

What would you add to the schedule tracker next?

A baseline pair of dates beside the live ones. Every row here has one start and one end, which means the schedule can slip without leaving a trace of what it was promised to be — and “are we late?” is a different question from “did this ever have a chance?”.

After that, the dependency chain rather than one hop. The blocked query finds what waits directly on a late task; it does not follow the chain to the launch date three steps downstream. That is a Cloud Code function walking dependsOn recursively, and it is the point where the tracker starts telling you something the spreadsheet never could.

Tagsspreadsheetproject-managementganttparse-serverrest-apidata-modeling

Frequently asked questions

How do you turn a Gantt spreadsheet into a database?

One class, one row per activity, and dates stored as dates rather than text. Create the class with activityId, name, owner, start, end, progress, dependsOn and milestone, then post the rows in a single batch request. On October 9, 2026 twelve activities went in together in 502 ms, after the class itself was created in 3.1 seconds.

How do you query which tasks are late?

Ask for the rows whose end date has passed and whose progress is below 100, in one query: where end is less than today and progress is less than 100. Storing the dates as a Date type rather than a string is what makes the comparison work — on text, 2026-10-9 and 2026-10-09 sort differently and neither compares correctly to a real date.

How do you find out what a late task is blocking?

Keep the dependencies as an array of activity ids and query it with the $in operator. Once you have the list of late ids, one query for rows whose dependsOn array contains any of them returns everything downstream. In the measured run, two late activities blocked three others, which is the number a spreadsheet makes you work out by eye.

What is the batch endpoint path on Back4app?

The paths inside a batch request start at /classes, not /parse/classes. Much of the Parse documentation shows the longer form because a self-hosted Parse Server usually mounts under /parse; on parseapi.back4app.com that prefix returns error 107, cannot route POST /parse/classes/Activity. Drop the prefix and the same batch succeeds.

Should an AI agent build the app or should you model it yourself?

Both, in that order, is the honest answer. An agent is fast at turning a spreadsheet into a working interface, and the shape of the data it produces is what you will live with afterwards. Knowing which columns the schedule actually needs — dates as dates, dependencies as an array — means you can check the agent's work rather than inherit it.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 9, 2026 — Backend: Back4app, free plan, USA East · Source: a 12-row activity spreadsheet with dependencies · Terminal: node and curl on macOS.

More from Back4app Engineering

AI Agent · tutorial

How to Turn a Spreadsheet Into a Web App With a Database and Login

Backend · tutorial

How to Find the Nearest Driver in an Uber-Style App [Full Repo]

AI Agent · tutorial

How to Build an Inventory Management System Without Writing Code