Centralized script management for Workspace

When you have 30+ bits of javascript scattered among dozens of Rules and Buttons in a dozen Spaces, managing this codebase becomes a nightmare :scream:.

If a Space or DB gets renamed, or if a DB gets moved from one Space to another, scripts break.

We really need a more robust way to manage a Workspace’s codebase of scripts - a single place where we can:

  • See everywhere scripts are defined: Space > DB > Button/Rule > Action
  • Perform global search-and-replace on all our scripts (or scripts defined in a particular Space/DB)
  • Maintain javascript modules - or at least some way to define global constants that any script in the Workspace can reference.

It’s true that Fibery is not very user-friendly as a scripting IDE but ideally it would operate as a no-code platform, so perhaps it’s worth digging into the reasons why scripting has become so necessary for you.
What are some specific things that you can’t achieve using the existing automation actions?
Perhaps these should be prioritised.

1 Like

100% agree! A lot of my requests here are directly related to this.

Here are some things I can’t currently do with no-code:

  • “Reach through” a Lookup to update the referenced entity.
  • Change the Status of a related entity.
  • Use References to look up a related entity:
// If a Task's "Project" field is uninitialized, and there is a "Meeting Notes"
// that references the Task, set the Task's "Project" field from the MN's "Default Project".

// Script parameters
const ENTITY_LINK_FIELD = 'Project'    // Task field that needs to be set
const PARENT_TYPE       = 'Project/Project'  // Type we want to find a link to (value for)
const REF_ENTITY_TYPES  = ['Clients/Meeting Notes']  // Types that have "Default Project" field
const REF_ENTITY_LINK_FIELD = 'Default Project'  // Name of the field to copy

const fibery = context.getService('fibery')
// const log = console.log

// Iterate through the entities
for (const entity of args.currentEntities) {
    // Set the Project field if empty
    if (entity[ENTITY_LINK_FIELD].id == null) {
        // Get all references to the Task
        const entityWithRefs = await fibery.getEntityById(entity.type, entity.id, ['References'])
        // Look for a reference to to a type with "Default Project" field (i.e. Meeting Notes)
        for (const ref of entityWithRefs['References']) {
            // Get referencing doc
            const refDoc  = await fibery.getEntityById('Collaboration~Documents/Reference', ref['Id'], ['FromEntityId', 'FromEntityType'])
            const refType = refDoc['FromEntityType'].name
            if (refType == PARENT_TYPE) {
                // The referring entity is the actual PARENT_TYPE (so its ID can be used directly)
                // I.e. a Task was created/referenced in a Project's Rich Text field
                const refEntity = await fibery.getEntityById(refType, refDoc['FromEntityId'], ['Name'])
                // log(` refType == PARENT_TYPE: ${refType}, ${refDoc['FromEntityId']} ${refEntity.id}`)
                await fibery.updateEntity(entity.type, entity.id, { [ENTITY_LINK_FIELD]: refEntity.id })
            } else if (REF_ENTITY_TYPES.includes(refType)) {
                // Found a suitable referring entity (i.e. that has a "Default Project" field)
                const refEntity = await fibery.getEntityById(refType, refDoc['FromEntityId'], ['Name', REF_ENTITY_LINK_FIELD])
                if (refEntity != null && refEntity[REF_ENTITY_LINK_FIELD].id != null) {
                    const refId = refEntity[REF_ENTITY_LINK_FIELD].id
                    await fibery.updateEntity(entity.type, entity.id, { [ENTITY_LINK_FIELD]: refId })
  • Call an external webhook
  • Do something to every entity in a Collection, conditional upon the entity’s State, etc.
  • Trim blanks from a field (I tried a Formula, didn’t work :man_shrugging:)
  • Access an entity’s UUID
  • Treat a Formula field like a Lookup/Reference (if it returns an entity).
  • Do anything with Documents (like create & link).
  • Work with Rich Text:
// Create and link a new Meeting Notes entity to a Client

const log = console.log
const dump = (obj) => JSON.stringify(obj, null, 2)
const fibery = context.getService('fibery')
const MEETING_NOTES_TYPE = 'Clients/Meeting Notes'
const TEMPLATE_DOC_ID = "b62eaad0-e02b-11ec-b6d4-31b05e8bc8ae"
const template_DocName = 'Meeting Notes TEMPLATE'
const fmt = 'html'
function assert(b, msg) { if (!b) throw new Error(msg) }

// Retrieve the contents of the Meeting Notes TEMPLATE Document

const template_Secret = await getDocumentSecret(TEMPLATE_DOC_ID)
assert(template_Secret, `"${template_DocName}" Document not found - ID "${TEMPLATE_DOC_ID}"`)
const template = await fibery.getDocumentContent(template_Secret, fmt)
assert(template, `No content retrieved for "${template_DocName}" Document - ID "${TEMPLATE_DOC_ID}"`)

for (const client of args.currentEntities) {
    // Create a new Meeting Notes entity and link it to the Client
    const meetNotes = await fibery.createEntity(MEETING_NOTES_TYPE, { 'Client ID': client['Public ID'] })

    // Build the new Meeting Notes Description content via mustache macro replacement of the Template's contents
    const macros = {}
    const newDescription = template.replace(/\{\{\s*(\w+)\s*\}\}/g,
        (match, name) => (typeof macros[name] === 'string' ? macros[name] : ''))
    //log('new Description: ' + text)

    // Update the new Meeting Notes Description content
    await fibery.setDocumentContent(meetNotes.Description.Secret, newDescription, fmt)

async function getRichTextContent(entity, fieldName, fmt) {
    const secret = entity[fieldName].Secret
    return await fibery.getDocumentContent(secret, fmt)

async function getDocumentSecret(docId) {
    const view = await fibery.getEntityById('fibery/view', docId, ['fibery/meta']);
    return view['fibery/meta']['documentSecret'];
  • De-duplication (looping, comparing, deleting, etc) – i.e. enforcing the uniqueness of entities by a key value (post-hoc is better than nothing!)
// Search for identical Roles (by Name) and delete all but the oldest one
const fibery = context.getService('fibery')
const entity_type = args.currentEntities[0].type
const SPACE_NAME = 'Users'

for (const entity of args.currentEntities) {
    // Find all identical Roles
    const query = `{findRoles( name:{is: "${entity.name}"} orderBy:{creationDate: ASC}) {id}}`
    const result = await fibery.graphql(SPACE_NAME, query)
    // Delete any duplicates
    if (result && result.data.findRoles.length > 1) {
        const roles = result.data.findRoles
        for (let i = 1; i < roles.length; i++) {
            // console.log(`Deleting: ${roles[i].id}`)
            await fibery.deleteEntity(entity_type, roles[i].id)
  • Because no-code Rule conditions can only test an entity’s own fields (i.e. they can’t test the fields of related entities or use arbitrary Formulas), I am forced to bring in more fields from related entities as Lookups/Formulas, and I resist doing this because it litters up my DB’s with all sorts of unwanted fields.

  • The lack of a “Description” or “Comments” area for each DB’s Automations page (and for individual Rules/Buttons) is frustrating. I would really hate to inherit a complex system like this with such little documentation. Scripts at least allow me to do that.

:bulb: Ideally it should be just as easy to “synthesize” a join anywhere (instead of needing to manually construct a new DB to represent it). E.g. in a Table view, have the ability show any arbitrarily related fields (as columns) AND allow the user to edit them; or in automations, allow any related fields (and formulas) to be tested in the Rule conditions (we need formulas there!). I think this ability alone would relieve most of the architecture/code limitation issues I struggle with – big things like lack of polymorphism.

That, plus the ability to hide fields (I know it’s coming), but that’s really more of a band-aid to paper over the need to drag in so many fields with Lookups/Formulas.