Question about nitpicky detail on dealing with infinite loop issues of Webhooks.

I think I understand that a webhook "Callback" is itself comprised of several "Callback Events", each of which may have its own changeAgent property, a string with a comma-separated list of change agents. Further I understand how I may append my own changeAgent designator in the SmartsheetChangeAgent header of any API I invoke to Create, Update, or Delete something in Smartsheet, so that I can investigate a callback and avoid infinite loops.

I can completely see that if all the events in the callback have my changeAgent designator, I can ignore the callback.

But.... if I am not ignoring the callback, my code may make many changes to the Smartsheet (as part of my syncronization process) that may involve bulk operations that involve some entities in the Callback Events, maybe some entities that are NOT in the Callback Events, and and some that may have some other 3rd party webhook designations that may differ from others..... of course I can place my own agent designation on all API calls, but it doesn't seem deterministically clear to me how to figure out what OTHER agent designations to keep on that 512 character string.

For example, say I receive a callback with three events, each of which lists three different change agents. Now say I am going to do a single bulk update of all three of the entities that led to the Callback Events, but that bulk update will ALSO include other entities that are NOT part of the event. If I am playing nicely, how should I set the SmartsheetChangeAgent? Is it OK to set it JUST to my changeAgent, and strip out all the others (seems unfair). But if I include all the others, that seems also problematic because some other webhook not in my control may get that event about a change that I made, but is now marked with THEIR changeagent designator.

It is hard to find the words to make this question clear.... I hope this is sufficient!

Best Answer

  • Kevin Tao
    Kevin Tao ✭✭
    edited 12/23/22 Answer ✓

    Let me reduce our example into perhaps the simplest one possible:

    • App A receives a webhook callback and updates cell (1, 1)
    • App B receives a webhook callback for that change and in response updates cell (2, 2)

    In other words, App B is not updating the cell that triggered it. I think this is actually probably more common a use case than an app that updates only cells that were just changed. For example, a simple app that when the "Status" column changes to "Complete", sets the "Completed Date" column to today.

    I think in this example, clearly App B should include App A's change agent, because App A triggered App B. App A wants to know that it triggered App B, which is now triggering it, so that it may make the choice to ignore the change in order to prevent a multi-app infinite loop.

    I think where we may not be aligned is that I see change agent as simply providing information to an app, that somewhere along the line, it triggered other automated solutions that made changes that are now triggering it. Whether the app then triggers or not is a decision it has to make--perhaps it needs to keep track of what cells it's updated in its own database to make an informed decision.

    Simply ignoring all cell changes that contain its change agent is probably too simplistic an approach. Also, per the API documentation:

    Else, the change which triggered the callback was not caused by the subscriber itself, and if the subscriber is going to subsequently react to the callback by making a change in Smartsheet (via API request), the API client should append a comma and its own identifier to the original attribute value, and pass that value through using the Smartsheet-Change-Agent header of the API request. Doing so protects against cross-system infinite loops.

    Note that there's no distinction made about which cells you're updating, or only doing this if the cells you're touching were in the callback events.

Answers

  • Hi sgolux,

    This is an interesting scenario you bring up. To paraphrase, if I'm understanding you correctly, you're asking how you should handle receiving a webhook callback that contains multiple events which have different changeAgents.

    Your question also includes the fact that your app will be modifying other entities (aka sheet data) that weren't included in the callback, and how you should handle changeAgent for those changes. I'll answer this part first: you shouldn't worry about sending different changeAgents for different entities--simply use the same changeAgent for all data being updated in response to the webhook callback. For example, if you're updating cell A in response to a callback, even if cell A wasn't an event in the callback, you still want your update to contain the changeAgent which triggered your update.

    As to how you should handle multiple events with different changeAgents: the simplest thing to do would be to concatenate the change agents together, and add yours to the end (all comma-separated, of course). This helps ensure that we retain change agents for all of the apps involved in this triggering chain. When those apps receive webhook callbacks for your change, they will see their own changeAgent in the events and can choose to ignore the change in order to prevent a multi-app infinite loop.

    I hope that helps!

  • sgolux
    sgolux ✭✭

    Hi and thanks @Kevin Tao - but I do want to press into the subtleties a little further.

    Imagine a single callback with two events, one affecting cell in Row 1 column 1 (event A), and one affecting cell in Row 2 column 2 (event B). And imaging that event A has change agent #1 and event B has change agent #2.

    My own change agent is (say) change agent #3.

    Now in response to these events, I change the value of Row 1 Column 1, and the value of Row 2 column 2 AND ADDITIONALLY the value of Row 3 column 3. And I do these all in a single batch "Update Row" request, which of course just has one header.

    Of course I will want to place myself (change agent #3) in the appropriate header, so that when I get the callback about these changes, I will recognize that I myself called them. No worry there.

    But Your suggestion, I think, would say that if I were playing nice, I should ALSO put both Change Agent #1 and Change Agent #2 on the header.

    But this means that when the code for change agent #1 gets the callback, it will think it already knows about the changes to Row 1 Column 1 and Row 2 Column 2 and Row 3 Column 3, because it will have its own change agent in the header. But in fact, the only one it SHOULD know about is Row 1 Column 1..... the other two were not changed by that agent, and hence should (I think?) appropriately be processed by that webhook handler.

    Similarly, when the code for change agent #2 gets the callback, it will think it already knows about ALL the changes, and again it only knows about ONE of them, namely Row 2 Column 2.

    So I wonder if I am actually playing nicer if I simply put my own change agent by itself in all the headers for everything I change, and intentionally do NOT put other change agents in the comma-delimited header string. It seems to me that is safer, and more collaborative.

    Can you comment on that please? I really would like to be a considerate player in the ecosystem!

    Thanks,

    -stephan

  • Appreciate your determination to play nice with other apps. :)

    One thing to keep in mind is that the change agent header is a tool to help prevent infinite loops--it isn't a solution in and of itself. In other words, it might not be as cut and dried as "don't act on any webhook event containing my change agent" (though, depending on the use case, it could be). It also isn't meant as a way to identify which app changed what data in the sheet--it's solely meant to help prevent infinite loops: either an app directly triggering itself repeatedly, or tringgering itself indirectly through a chain of other apps that listen to webhooks and update the sheet.

    An app will likely need to have some smarts to decide whether or not it needs to make updates to a sheet when it receives a callback. If those criteria are specific enough (e.g. only respond to changes in the "Status" column, and then update some other column), then maybe it doesn't even need to worry about the possibility of infinite loops.

    However, if an app needs some help to prevent infinite loops (i.e. it wants to prevent itself from triggering on its own changes, or changes that it indirectly caused by triggering another app), it can use the changeAgent header to help make that decision.

    For example, if the updates the app makes are critical and it never wants to miss an update, perhaps it wants to have a bias for updating the sheet--in that case, perhaps it could always trigger, except when the app is the only one and is present on all of the changeAgents it receives (i.e. it will always trigger except if it was responsible for all of the changes in the callback). This could be "good enough", if the other apps on the sheet aren't responding to changes this app makes. This strategy could also risk "triggering chain" loops with other apps. This is a tradeoff that might need to be considered by an app developer based on the use case.

    This example also highlights the potential complexity of hooking up multiple apps to a single sheet that both listen for sheet changes and update the sheet.

    The "nicest" possible approach could be to break up your updates into one per change agent. So in your example, you'd make 3 updates to the sheet, one for Cell (1, 1), Cell (2, 2,), and Cell (3, 3), and include the appropriate change agent for each cell. However, I think expecting or requiring this of apps is probably unrealistic, and potentially unscalable since in theory, it might require one to make dozens of updates to the sheet instead of one bulk update.

  • sgolux
    sgolux ✭✭

    Thanks @Kevin Tao ! So given that the scale issues for me make "unbatching" an impossibility, I think I will make myself the changeAgent and NOT pass through any other change agents. I can't think of any better solution. If that causes feelings of nausea for you, please let me know!

  • It does, just a little. :) Can you elaborate on why you'd rather not concatenate the change agents and include them all?

    One issue with not passing through the other change agents is that it removes the option for other apps to ignore the changes made by your app, i.e. since their change agent isn't present for the changes your app makes, they now have no choice but to look at your app's changes and potentially act on them.

    If we include them, then they at least have the option to decide whether or not to ignore the callback events.

    Imagine a case where 3 apps all listen to webhooks and all update the sheet, and all strip out the other change agents when updating the sheet. None of the apps would have a way of knowing that their change triggered another app, which is now triggering them.

    Given the choice between including potentially extra change agents and omitting all change agents, the safer of the two options is to include the extra change agents if the primary concern is to avoid infinite loops.

  • sgolux
    sgolux ✭✭

    Ok @Kevin Tao - I'm having a hard time expressing this. I'm thinking that if a change agent parses the string and finds themselves on it, what that should mean to them semantically is "You've already handled this change so you probably don't need to do anything about it". But take my previous example. In my hypothetical integration, the fact that Row 1 column 1 and Row 2 column 2 changed (which may have happened due to the other change agents), causes MY integration to (say) add those values together and put the result in Row 3 Column 3. Now if that change to Row 3 Column 3 gets marked as "belonging" also to the OTHER change agents, then they will erroneously get told by my having concatenated their tokens into the header "You've already handled this change so you probably don't need to do anything about it". But in fact, I'm kind of lying to them, yes? I'm telling them that they don't need to do anything about a change when in fact they may very well NEED to do something about that change, and address it. So that is why I'm thinking I shouldn't fake them out. Is my logic here wrong?

  • Kevin Tao
    Kevin Tao ✭✭
    edited 12/23/22 Answer ✓

    Let me reduce our example into perhaps the simplest one possible:

    • App A receives a webhook callback and updates cell (1, 1)
    • App B receives a webhook callback for that change and in response updates cell (2, 2)

    In other words, App B is not updating the cell that triggered it. I think this is actually probably more common a use case than an app that updates only cells that were just changed. For example, a simple app that when the "Status" column changes to "Complete", sets the "Completed Date" column to today.

    I think in this example, clearly App B should include App A's change agent, because App A triggered App B. App A wants to know that it triggered App B, which is now triggering it, so that it may make the choice to ignore the change in order to prevent a multi-app infinite loop.

    I think where we may not be aligned is that I see change agent as simply providing information to an app, that somewhere along the line, it triggered other automated solutions that made changes that are now triggering it. Whether the app then triggers or not is a decision it has to make--perhaps it needs to keep track of what cells it's updated in its own database to make an informed decision.

    Simply ignoring all cell changes that contain its change agent is probably too simplistic an approach. Also, per the API documentation:

    Else, the change which triggered the callback was not caused by the subscriber itself, and if the subscriber is going to subsequently react to the callback by making a change in Smartsheet (via API request), the API client should append a comma and its own identifier to the original attribute value, and pass that value through using the Smartsheet-Change-Agent header of the API request. Doing so protects against cross-system infinite loops.

    Note that there's no distinction made about which cells you're updating, or only doing this if the cells you're touching were in the callback events.