Linking Child and Parent Cards on Trello via the Trello API

One of the things my team at work always seems to have a problem with is tracking our tasks and how they fit into the bigger picture of our team’s goals.  We had these problems when we were using a bulletin board, we have these problems now that we’re using a single Trello board.

We have a subset of the team currently researching new tools due to the perceived limitations of Trello. Being a developer and having never met a problem that couldn’t be solved by throwing code at it, I decided to see what could be done with the Trello API.

I’ve written about using the Trello API a couple times before.  It’s a powerful tool and it’s something I’ve had a lot of fun with.  This builds off of that work.

One thing that was the lynch-pin for figuring all of this out is the difference between a card’s ID, shortID, and shortLink.  Because shortLink isn’t returned by default when you make a request for a card’s information, I thought that it was just another term for shortID, much as how archived and closed are synonyms within the Trello environment.  It turns out that that’s not true, and since shortLink is exposed in every Trello URL, it opens up a lot of options.

Before getting into the code too much, though, a look at the problem we’re trying to solve…

Right now my team has one “Work in Progress” (or “WIP”) board with ten functional columns.  Requests Not Started, Requests In Progress, Slices Not Started, Slices In Progress, Ready for QA on DEV, QA on Dev, Need to Publish to Stage, QA on Stage, Ready for Publish, and Published.

Requests are large-scale projects.  Slices are parts of a request, which are supposed to be individually-shippable.

When you pick up a request that hasn’t started yet, your first task is to break it into slices.  Those slices then march across the board and when they’re all published the request card jumps over to join them (currently a manual process).  The idea is that the slices show the team a little bit more of what’s in progress while the requests are geared towards external stakeholders.

The problem with that is that it makes for a messy board.  If only we could have a WIP board just for requests, and have the slices flowing along some other path, but still tied back to the request card.  So that’s what I did.

Here’s my informal demo video, with code to follow.

Okay, now on to the code.

#!/usr/bin/php -q
<?php
  $trello = new trello_api($GLOBALS['config']['key'], $GLOBALS['config']['secret'], $GLOBALS['config']['token']);

  // LOOP THROUGH EACH CARD ON THE REQUESTS IN PROGRESS LIST
  $card_data = $trello->request('GET', ('/1/lists/' . $GLOBALS['config']['board']['wip_prototype']['rip_list'] . '/cards'));
  foreach ($card_data AS $card) {
    // GET THE CHILD BOARD
    $child_board_name = '';
    foreach ($GLOBALS['config']['label'] AS $board => $color) {
      if ($card->labels[0]->color == $color) {
        $child_board_name = $board;
      }
    }

    // GET THE CARD TAG
    $card_tag = '';
    if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
      $card_tag = '[' . $matches[1] . '] ';
    }    

    // GET THE CHILD TASKS / CARDS
    $children = 0;
    $children_completed = 0;

    foreach ($card->idChecklists AS $checklist_id) {
      $checklist_data = $trello->request('GET', ('/1/checklists/' . $checklist_id));

      if ($checklist_data->name == 'Slices') {
        foreach ($checklist_data->checkItems AS $item) {
          if (preg_match('|https://trello.com/c/(.{8})|is', $item->name, $matches)) {
            // WE HAVE THE CHILD CARD, CHECK IT
            $child_card = $trello->request('GET', ('/1/cards/' . $matches[1]));

            if ($child_card->id) {
              // CARD STILL EXISTS
              $child_card_state = 'incomplete';
              if (($child_card->closed) OR ($child_card->idList == $GLOBALS['config']['board']['qa_prototype']['completed_list'])) {
                $child_card_state = 'complete';
                $children_completed++;
              }

              // UPDATE THE STATUS IF NECESSARY
              if ($child_card_state != $item->state) {
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => $child_card_state));
              }

              $children++;
            } else {
              // CARD HAS BEEN DELETED, REMOVE THE CHECKITEM
              $trello->request('DELETE', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id));
            }
          } else {
            // CARD HASN'T BEEN CREATED YET, DO IT IF WE HAVE A LIST TO POST TO
            if ($GLOBALS['config']['board'][$child_board_name]['ns_list']) {
              $new_card = $trello->request('POST', '/1/cards', array('name' => ($card_tag . $item->name), 'idList' => $GLOBALS['config']['board'][$child_board_name]['ns_list'], 'desc' => ('Parent Card: ' . $card->shortUrl)));

              if ($new_card->id) {
                // UPDATE THE CHECKLIST ITEM WITH THE LINK TO THE NEW CARD
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/name'), array('value' => $new_card->shortUrl));
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => 'incomplete'));
              }

              $children++;
            }
          }
        }
      }
    }

    if (($children > 0) AND ($children == $children_completed)) {
      $trello->request('PUT', ('/1/cards/' . $card->id . '/'), array('pos' => 'top', 'idList' => $GLOBALS['config']['board']['wip_prototype']['completed_list']));
    }
  }

  unset($trello);
?>

This makes use of my Trello API wrapper class and has several values (all part of the $GLOBALS[‘config’] array) dumped off to an included configuration file.  I think the comments are pretty good, but let’s go through this a little bit of code at a time.

The whole thing is wrapped in a loop through the cards on the Requests in Progress column of the main WIP board.  We get that by doing a GET request to /1/lists/xxxxxx/cards (where xxxxxx us the list’s ID) and I won’t bother detailing it any further.

// GET THE CHILD BOARD
$child_board_name = '';
foreach ($GLOBALS['config']['label'] AS $board => $color) {
  if ($card->labels[0]->color == $color) {
    $child_board_name = $board;
  }
}

// GET THE CARD TAG
$card_tag = '';
if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
  $card_tag = '[' . $matches[1] . '] ';
}

We figure out which board the child cards should go to by looking at the labels and our configured array that matches label colors to child boards.  In this example, the green label belongs to the ecommerce board and the red one to the marketing board.  If the first label on the card matches the color of one of those boards, we’ve got our board.

Then we take a look at the parent card name and look for anything inside square brackets using a regular expression.  That’s the tag that should be applied to child cards, something we use for filtering.

We set the number of children on this card and the number of completed children to zero, values we’ll update as we go.  Then we loop through the checklist IDs we got when we looked up the card data.

$checklist_data = $trello->request('GET', ('/1/checklists/' . $checklist_id));

if ($checklist_data->name == 'Slices') {
  foreach ($checklist_data->checkItems AS $item) {

We use a GET request to /1/checklists/yyyyyy (where yyyyyy is the ID of our checklist) to get more info about that checklist.  If the name of the checklist is “Slices” then we iterate through each item because we know those represent our child cards.

if (preg_match('|https://trello.com/c/(.{8})|is', $item->name, $matches)) {
  // WE HAVE THE CHILD CARD, CHECK IT
  $child_card = $trello->request('GET', ('/1/cards/' . $matches[1]));

  if ($child_card->id) {
    // CARD STILL EXISTS
    $child_card_state = 'incomplete';
    if (($child_card->closed) OR ($child_card->idList == $GLOBALS['config']['board']['qa_prototype']['completed_list'])) {
      $child_card_state = 'complete';
      $children_completed++;
    }

    // UPDATE THE STATUS IF NECESSARY
    if ($child_card_state != $item->state) {
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => $child_card_state));
    }

    $children++;

If the text of the checklist item matches the pattern of a Trello card URL, we know it’s an already-linked child.  We get the shortLink from that URL using our regular expression and then make a GET request to /1/cards/zzzzzz, where zzzzzz is that shortLink.  If the card has an ID it means it still exists, so we can do some work with it.

By default, we set the card’s status to incomplete, which is one of the states of a checkItem in Trello. If the card is closed or is in the Published list on our QA board, we know work has been completed and the state can be set to complete. We can also increment our counter of completed child cards by one.  If the state we’ve determined doesn’t match that of the checkItem, we fire off a PUT request to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/state (wherre cccccc is the card ID, bbbbbb is the checklist ID and aaaaaa is the ID of the specific checklist item), passing in a value set to whatever we determined above.  This updates our checklist item to match the state of the card itself.  Then we increment the counter of children by one.

} else {
  // CARD HAS BEEN DELETED, REMOVE THE CHECKITEM
  $trello->request('DELETE', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id));
}

If the card’s ID wasn’t found, we know the card has been deleted.  In that case we fire a DELETE request to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa to completely remove the related checklist item.

} else {
  // CARD HASN'T BEEN CREATED YET, DO IT IF WE HAVE A LIST TO POST TO
  if ($GLOBALS['config']['board'][$child_board_name]['ns_list']) {
    $name = $card_tag . $item->name;
    $desc = 'Parent Card: ' . $card->shortUrl;
    $new_card = $trello->request('POST', '/1/cards', array('name' => $name, 'idList' => $GLOBALS['config']['board'][$child_board_name]['ns_list'], 'desc' => $desc));

    if ($new_card->id) {
      // UPDATE THE CHECKLIST ITEM WITH THE LINK TO THE NEW CARD
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/name'), array('value' => $new_card->shortUrl));
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => 'incomplete'));
    }

    $children++;
  }
}

Now we’re back to if we didn’t find a URL in the checklist item’s name.  In that case, we know the child card hasn’t been created yet so we go on to do that, assuming we actually have a list ID to work with (just in case there’s no label on the parent card or something).

We build the child card’s name by combining the text of the list item with the card tag we defined earlier, then we build the description by just linking to the parent card.  Then we fire that data (along with the list ID) off via a POST request to /1/cards.

That request returns data about the newly-created card.  If the card was successfully created (determined by whether or not we got an ID back), we go back to the checklist item.  We update the name to be a link to the new card, which displays in Trello as that card’s name, and we make sure to set the status to incomplete just in case someone did something stupid.  That’s done with a pair of PUT requests, one to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/name with value set to the new card’s shortUrl, the other to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/state with value set to incomplete.

At this point we’ve looped through every item on the “Slices” checklist for a single card.

if (($children > 0) AND ($children == $children_completed)) {
  $trello->request('PUT', ('/1/cards/' . $card->id . '/'), array('pos' => 'top', 'idList' => $GLOBALS['config']['board']['wip_prototype']['completed_list']));
}

The last thing we do before moving on to the next card is compare our counts of children and completed children. To account for the fact that a card may have been moved to Requests in Progress before the “Slices” checklist was completed, we require at least one child to exist.  If there are children and they’ve all been completed, we move the parent card over to the Published column of our WIP board with a PUT request to /1/cards/dddddd, where dddddd is the parent card ID, with the pos defined as top and the idList defined as the ID of the published column.

As I mentioned in the video, I’ve got this running once a minute as a cronjob.  I’m sure there’s a better way to do it but this is my first pass so I’m not worried about it.

2 thoughts on “Linking Child and Parent Cards on Trello via the Trello API”

  1. Hi,

    Great post, really nice concepts!

    I realise this blog post is couple of years old, but we are now also looking to tackle a similar situation. Have you now put this model into production? If so, can you comment on how its going, particular with Trello itself, any noticeable issues, hacky workarounds you’ve had to implement?

    1. This was in production for a little while, then the team it was built for kind of disbanded and started tracking everything differently, then I left the company entirely.

      The big issue we ran into was that, with this running as a cron job, when there were a lot of cards on the board we started hitting our API rate limits. I’d love to rewrite all of this as a webhook (or a series of them), which would eliminate much of the API overhead, I just haven’t gotten around to doing it. I think that’s the better solution, with a lot of the concepts carrying over directly.

Leave a Reply

Your email address will not be published. Required fields are marked *