Trigger on doc changes not working

Hmmm… I copied my Task’s (working) “Sync Details” Button (including the script, verbatim) as a “Sync Details” Rule (triggered by the “Details” field changing) and tested it by editing the Details field in a Task entity: the Rule shows as having run successfully (under Activity) but the effect didn’t occur (the script should update “Searchable Details" rich text field). So the same script works fine as a Button, but not as Rule: is there something different about the script API for Rules vs Buttons?

Sounds unusual. Can you post the script here, together with any useful screen grabs from the activity log.

// Developer reference is at api.fibery.io/#action-buttons

const logging = false
const create_searchable_details = true
const update_collection_fields = true
const create_outcome = false

// Lookup table for mapping typeId to entity Name and corresponding
// Referenced Entity field.  Note: this table must be extended
// whenever a new Database is added (if it might be Referenced), or
// modified whenever a Database's Name is updated.

const lookup = {
    'e5f3e990-815a-11ec-adbf-35e3df6ae56f': {
        'name': 'Tag',
        'field': 'Referenced Tags'
    },
    '53c77c70-7cb5-11ec-bdf4-9d8359eb2d89': {
        'name': 'Person',
        'field': 'Referenced People'
    },
    'bdefbc60-7cb6-11ec-bdf4-9d8359eb2d89': {
        'name': 'Product',
        'field': 'Referenced Products'
    },
    'ee89f9a0-7cb4-11ec-bdf4-9d8359eb2d89': {
        'name': 'Organization',
        'field': 'Referenced Organizations'
    },
    '87c49720-7cdc-11ec-bdf4-9d8359eb2d89': {
        'name': 'Trial',
        'field': 'Referenced Trials'
    },
    '54a09420-5dac-11eb-836f-5117371ac1ee': {
        'name': 'Task',
        'field': 'Referenced Tasks'
    },
    '8f95e150-70c7-11ec-b97d-972ab98146c5': {
        'name': 'Epic',
        'field': 'Referenced Epics'
    },
    '848451a0-78e9-11ec-9cbf-ed2f2cff2041': {
        'name': 'Project',
        'field': 'Referenced Projects'
    },
    'ad167630-7014-11ec-9df5-c99d2f84b20b': {
        'name': 'Milestone',
        'field': 'Referenced Milestones'
    },
    'efbd97f0-5dac-11eb-836f-5117371ac1ee': {
        'name': 'Goal',
        'field': 'Referenced Goals'
    },
    '1ad6b940-7cb4-11ec-bdf4-9d8359eb2d89': {
        'name': 'Note',
        'field': 'Referenced Notes'
    }
}

// Create and initialize a field_names list with the names of the
// collection fields listing the corresponding referenced entities.
const field_names = []
for (var key in lookup) {
    field_names.push(lookup[key]['field'])
}

// Recursively traverse the (rich text) document object, storing all
// entity references and then replacing them in-place with the names
// of the referenced entities.
async function traverse1(fibery, parent) {
    let is_entity = false
    for (let child_key in parent) {
        const child = parent[child_key]
        if (child_key == "type" && child == "entity") is_entity = true
        if (typeof child === "object") {
            if (is_entity && child_key == "attrs") {
                const id = child['id']
                const type_id = child['typeId']
                if (logging) console.log("FOUND ENTITY: id: " + id + " type_id: " + type_id)
                const inferred_type = lookup[type_id]['name']
                const reference = await fibery.getEntityById(inferred_type, id, ['Name'])
                found_refs[inferred_type].push(reference)
                return reference['Name']
            } else {
                const name = await traverse1(fibery, child)
                if (name !== null) parent[child_key] = { 'type': 'text', 'text': name }
            }
        }
    }
    return null
}

async function traverse(fibery, found_refs, parent) {
    let is_entity = false
    for (let child_key in parent) {
        const child = parent[child_key]
        if (child_key == "type" && child == "entity") is_entity = true
        if (typeof child === "object") {
            if (is_entity && child_key == "attrs") {
                const id = child['id']
                const type_id = child['typeId']
                if (logging) console.log("FOUND ENTITY: id: " + id + " type_id: " + type_id)
                const inferred_type = lookup[type_id]['name']
                const reference = await fibery.getEntityById(inferred_type, id, ['Name'])
                found_refs[inferred_type].push(reference)
                return reference['Name']
            } else {
                const name = await traverse(fibery, found_refs, child)
                if (name !== null) parent[child_key] = { 'type': 'text', 'text': name }
            }
        }
    }
    return null
}

// Remove all entity references in list from designated collection.
async function emptyReferenceList(fibery, entity, ref_type, refs) {
    for (const ref of refs) {
        if (logging) console.log("REMOVE " + ref_type + " REF: " + ref.Id)
        await fibery.removeCollectionItem(entity.type, entity.id, ref_type, ref.Id)
    }
}

// Add all entity references in list to designated collection.
async function setReferenceList(fibery, entity, ref_type, refs) {
    for (const ref of refs) {
        if (logging) console.log("ADD " + ref_type + " REF: " + ref.Id)
        await fibery.addCollectionItem(entity.type, entity.id, ref_type, ref.Id)
    }
}

// Fibery API is used to retrieve and update entities
const fibery = context.getService('fibery')

// affected entities are stored in args.currentEntities;
// to support batch actions they always come in an array
for (const entity of args.currentEntities) {
    // an entity contains all fields apart from collections;
    // to access a field refer to it by its UI name

    // Note: we could also get a restricted copy of the entity contain only
    // the Details field (+ Id) like this (but not necessary):
    //   const x = await fibery.getEntityById(entity.type, entity.id, ['Details'])
    // That is also the ONLY way to get collection fields, eg:
    //   const entity2 = await fibery.getEntityById(entity.type, entity.id, ['Tags', 'Epic Tags'])
    //   const tags = entity2['Tags']
    //   const epic_tags = entity2['Epic Tags']
    // We could also use that method to get the *incoming* references (entities
    // which have referenced this entity):
    //   const entity2 = await fibery.getEntityById(entity.type, entity.id, ['References']);
    //   const references = entity2['References']
    // although unfortunately there's no way to get a list of entities that this
    // entity references: for that we have to crawl through the rich text fields, as below.

    const details = entity['Details']
    const searchable_details = entity['Searchable Details']

    if (logging) {
        console.log("Entity:")
        console.log(entity)
        console.log("Details:")
        console.log(details)
    }

    const document = await fibery.getDocumentContent(details['secret'], "json")
    const entity2 = await fibery.getEntityById(entity.type, entity.id, field_names)

    if (logging) {
        console.log("Document:")
        console.log(document)
        console.log("Entity + collection fields:")
        console.log(entity2)
    }

    if (update_collection_fields) {
        // Remove existing references (in any case, since if there's
        // no rich text then all collection fields should be empty).
        for (var key in lookup) {
            const field_name = lookup[key]['field']
            await emptyReferenceList(fibery, entity, field_name, entity2[field_name])
        }
    }

    if (document != null) {
        // Create and initialize a found_refs dictionary with the names of
        // the referenced entities.
        const found_refs = {}
        for (var key in lookup) {
            found_refs[lookup[key]['name']] = []
        }

        // Store all entity references found in document, replacing
        // them in-place with the names of the referenced entities.
        await traverse(fibery, found_refs, document)

        if (logging) {
            console.log("Stripped document:")
            console.log(document)
        }

        if (create_searchable_details) {
            // Update the search details field with the stripped text.
            await fibery.setDocumentContent(searchable_details['secret'], document, "json")
        }

        if (create_outcome) {
            // Update the (non-rich text) outcome field with the
            // stripped text (this will strip all remaining structure,
            // but the reference names will survive because they've
            // already been replaced with text).
            await fibery.updateEntity(entity.type, entity.id, { 'Outcome': document })
        }

        if (update_collection_fields) {
            // Update all the collection fields with the stored lists of
            // referenced entities.
            for (var key in lookup) {
                const name = lookup[key]['name']
                const field_name = lookup[key]['field']
                await setReferenceList(fibery, entity, field_name, found_refs[name])
            }
        }
    } else if (create_searchable_details) {
        // There's no rich text, so the search text field must be empty.
        await fibery.setDocumentContent(searchable_details['secret'], "", "md")
    }
}

1 action was executed Apr 13, 2023 09:32 Completed
Script executed Completed

I can see that there are console.log entries in the script. Are you getting these messages in the activity log for the automation (or can you turn on logging to see what you get)?

And maybe a stupid question: is it definitely the same script being run when it works with a button as it is when it runs triggered by a rich text field change?

Actually no I don’t see any output in the console, even when I run the script via the button. Yes it’s the same exact script: copied & pasted from button to rule just now.

I’m not a sw engineer, but these lines seem a bit funny to me…

Given that is_entity is only true when child_key == "type" then I can’t see how is_entity && child_key == "attrs" can ever be true.
Or am I missing something?

Seems fair: this is pretty old code now so I’ll have to revisit it at some point: but it does work, as a button: just not as a rule.

I’d suggest turning on the logging and seeing what you can deduce from the console logging.

I’m not seeing any logging output: it’s as if the script isn’t getting run. BTW this script/automation is a long-standing workaround for the fact that entity names aren’t included in the search index: is a fix for that on the roadmap? (eg. if I include “created a new [EC2] instance” (where [EC2] is a reference to an entity named “EC2”) and then I search for the string “EC2” that string will not appear in the results).

You will see console output only if run as a button. Otherwise, the log output will appear in the activity tab of the automation rule, after it has run.

We have a feature for #mentioned items to be indexed in search, but no ETA I’m afraid.

How odd: I’m not seeing any activity today: even though I’ve tried editing the ‘Details’ field in a task a couple of times.

Do you have access to my account btw? If so do feel free to explore: it’s the “Sync Details” rule (of Task) that’s not working: which is a verbatim copy of the “Sync Details” button (of Task) that works.

We don’t get access to users’ workspaces unless explicitly granted. If you wanted me to dig into it, you can invite me (ideally as an admin): chris@fibery.io

BTW, I have split out into a new topic thread to avoid polluting the original

You’ve already got access actually (admin): please go ahead and thanks!!

OK great. I’ll message you on chat to save this discourse topic becoming a massive thread!

Good grief: looks like it’s working after all! The Searchable Details field just updated after only 1-2 seconds. I swear that wasn’t happening before! Did you change something? Or maybe I just wasn’t waiting long enough? Oh well: if it’s consistently working then that’s great news!!

BTW Is there an easy way for me to run my “Sync Details” script (it’s the same script in both the button and the rule) as a one-off, applied to every Task? (So as to make sure that Searchable Details is up-to-date with Details on every Task, since I’ve certainly not always remembered to press the “Sync Details” button after every change in the past.)

I thought about it and figured maybe I could rewrite the script with an outer loop over Tasks and then attach it as an automation to a “database” that has all Tasks related to it, and run it there via a button? But it there a simpler way?

Or (better): can I create an automation (button) on a database that has a Tasks relation (all Tasks) but then that automation TRIGGERS the Sync Details Button (or Rule) on each Task? (So the script for that (outer) automation could just a loop over the Task relations and the synchronization code could remain in the Task automation(s).)