Posting Family Trees to Bluesky from the CLI
I’ve added a new command to create and post family tree threads on Bluesky.
It works in a similar way to the timeline command that I already had. To create a timeline, I can run:
cronolm story timeline ROLE_ID
For example, if the ID belongs to Prime Minister of the United Kingdom, the command gets that role from the neo4j database and finds all the related nodes connected with a POSITION_HOLDER edge.
MATCH (n:Entity { id: $personEntityId })-[r]-(y)
WHERE type(r) IN $relationshipTypes
AND (r.start_date IS NOT NULL OR r.end_date IS NOT NULL OR r.point_date IS NOT NULL)
AND r.statement_id IS NOT NULL
RETURN
n.id AS PersonId,
coalesce(nullif(n.name, 'Unknown'), n.name_en, n.name_es) AS PersonName,
n.image_url AS PersonImage,
n.date_of_birth AS DateOfBirth,
n.date_of_death AS DateOfDeath,
type(r) AS RelationshipType,
r.name AS RelationshipName,
r.start_date AS StartDate,
r.end_date AS EndDate,
r.point_date AS PointDate,
y.id AS RelatedEntityId,
labels(y) AS RelatedEntityLabels,
coalesce(nullif(y.name, 'Unknown'), y.name_en, y.name_es) AS RelatedEntityName,
y.description_en AS RelatedEntityDescriptionEn,
y.description_es AS RelatedEntityDescriptionEs,
y.image_url AS RelatedEntityImage
ORDER BY coalesce(
date(left(replace(toString(r.start_date), '""', ''), 10)),
date(left(replace(toString(r.end_date), '""', ''), 10)),
date(left(replace(replace(replace(replace(r.point_date, '""', ''), '+', ''), '-00-', '-01-'), '-00T', '-01T'), 10))
Once it has all the information, it builds the thread and posts each part slowly to avoid hitting Bluesky’s rate limits.
You can see an example here: Roman emperor timeline on Bluesky.
The new family tree command looks like this:
cronolm story family PERSON_ID
This one gets the person from neo4j and finds their direct family relationships: parents, partners, siblings, and children. It then builds the family tree thread and posts it on Bluesky.
MATCH (ego:Human {id: $id})
/* ---------------- Parents ---------------- */
CALL {
WITH ego
CALL {
WITH ego
OPTIONAL MATCH (ego)-[r:FATHER|MOTHER]->(p:Human)
WHERE r.statement_id IS NOT NULL
RETURN p AS person,
{ relType: type(r), relProps: properties(r), direction: 'ego->parent' } AS relDetail
UNION
WITH ego
OPTIONAL MATCH (p:Human)-[r:CHILD]->(ego)
WHERE r.statement_id IS NOT NULL
RETURN p AS person,
{ relType: type(r), relProps: properties(r), direction: 'parent->ego (via CHILD)' } AS relDetail
}
WITH person, relDetail WHERE person IS NOT NULL
WITH person, collect(DISTINCT relDetail) AS relations
RETURN collect({ person: person, relations: relations }) AS parents
}
/* ---------------- Children ---------------- */
CALL {
WITH ego
CALL {
WITH ego
OPTIONAL MATCH (ego)-[r:CHILD]->(c:Human)
WHERE r.statement_id IS NOT NULL
RETURN c AS person,
{ relType: type(r), relProps: properties(r), direction: 'ego->child' } AS relDetail
UNION
WITH ego
OPTIONAL MATCH (c:Human)-[r:FATHER|MOTHER]->(ego)
WHERE r.statement_id IS NOT NULL
RETURN c AS person,
{ relType: type(r), relProps: properties(r), direction: 'child->ego (via FATHER|MOTHER)' } AS relDetail
}
WITH person, relDetail WHERE person IS NOT NULL
WITH person, collect(DISTINCT relDetail) AS relations
RETURN collect({ person: person, relations: relations }) AS children
}
/* ---------------- Partners ---------------- */
CALL {
WITH ego
OPTIONAL MATCH (ego)-[r:SPOUSE|UNMARRIED_PARTNER|PARTNER]-(p:Human)
WHERE r.statement_id IS NOT NULL
WITH ego, p AS person, r
WHERE person IS NOT NULL
WITH ego, person,
collect(DISTINCT {
relType: type(r),
relProps: properties(r),
direction: CASE WHEN startNode(r) = ego THEN 'ego->partner' ELSE 'partner->ego' END
}) AS relations
RETURN collect({ person: person, relations: relations }) AS partners
}
/* ---------------- Siblings ---------------- */
CALL {
WITH ego
OPTIONAL MATCH (ego)-[:FATHER|MOTHER]->(p:Human)
WITH ego, collect(DISTINCT p) AS egoParents
CALL {
WITH ego, egoParents
OPTIONAL MATCH (ego)-[r:SIBLING]-(sib:Human)
WHERE r.statement_id IS NOT NULL
RETURN sib AS person,
{
kind: 'direct',
relType: type(r),
relProps: properties(r),
direction: CASE WHEN startNode(r) = ego THEN 'ego->sib' ELSE 'sib->ego' END
} AS relDetail
UNION
WITH ego, egoParents
OPTIONAL MATCH (ego)-[rE:FATHER|MOTHER]->(par:Human)<-[rS:FATHER|MOTHER]-(sib:Human)
WHERE rE.statement_id IS NOT NULL AND rS.statement_id IS NOT NULL
RETURN sib AS person,
{
kind: 'viaParent',
parent: par,
egoRelType: type(rE),
egoRelProps: properties(rE),
sibRelType: type(rS),
sibRelProps: properties(rS)
} AS relDetail
}
WITH person, relDetail, egoParents
WHERE person IS NOT NULL AND person <> ego AND NOT person IN egoParents
WITH person, collect(DISTINCT relDetail) AS relations
RETURN collect({ person: person, relations: relations }) AS siblings
}
RETURN ego, parents, children, partners, siblings;
You can see an example here: George IV’s family tree thread on Bluesky.
So now creating one of these threads is just a matter of finding the person’s Wikidata ID and running the command.
The next step might be turning this into a Bluesky bot. Someone could mention the Cronologia account with something like:
@cronologia.bsky.social family tree person_name
The bot could search for the person and reply with the generated family tree thread. I haven’t built that part yet, but it feels like the obvious next step.