<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://daniels-notes.de/feed.xml" rel="self" type="application/atom+xml" /><link href="https://daniels-notes.de/" rel="alternate" type="text/html" /><updated>2026-07-18T13:27:33+00:00</updated><id>https://daniels-notes.de/feed.xml</id><title type="html">Daniel’s notes</title><subtitle>Blog about WEBCON BPS and GitHub projects.</subtitle><author><name>Daniel Krüger</name></author><entry><title type="html">User Defined API - Get data from data sources (Part 2)</title><link href="https://daniels-notes.de/posts/2026/user-defined-api-part-2-get-data-from-data-sources" rel="alternate" type="text/html" title="User Defined API - Get data from data sources (Part 2)" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/user-defined-API-part-2-get-data-from-data-sources</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/user-defined-api-part-2-get-data-from-data-sources"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is the second part of my blog post about User Defined APIs. This one is dedicated to running mode <code class="language-plaintext highlighter-rouge">Get data from data source</code>. While they are easy to use in general, I want to highlight a few issues I’ve run into, so that you are aware of them.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-12-00-29.png" alt="Get data from data source returns a JSON array of the configured data via a GET request." /><figcaption>
      Get data from data source returns a JSON array of the configured data via a GET request.

    </figcaption></figure>

<h1 id="series">Series</h1>

<p>If you have no idea what User Defined APIs are and why you should change this, you should start with the first part of this series:</p>
<ol>
  <li><a href="/posts/2026/user-defined-api-overview-part-1">Overview</a></li>
  <li><a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources">User Defined API - Get data from data sources</a></li>
  <li><a href="/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance">User Defined API - Actions on a workflow instance</a></li>
  <li><a href="/posts/2026/user-defined-api-part-4-execute-automation">User Defined API - Execute automation</a></li>
</ol>

<h1 id="get-data-from-data-source">Get data from data source</h1>
<h2 id="definition">Definition</h2>
<p>The <code class="language-plaintext highlighter-rouge">Get data from data source</code> running mode is like a data table, which can be accessed via a GET request.</p>
<ul>
  <li>You select a data source</li>
  <li>Define public names for the columns</li>
  <li>Add optional filters via parameters, which you also know from automations</li>
</ul>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-08-57-54.png" alt="Running mode - Getting data from a data source" /><figcaption>
      Running mode - Getting data from a data source

    </figcaption></figure>

<p>Afterwards you can access the data via a simple GET. If <a href="/posts/2026/user-defined-api-overview-part-1#authentication">cookie authentication</a>` is enabled you can even test it from the browser.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-12-04-12.png" alt="The result is filtered using a query parameter" /><figcaption>
      The result is filtered using a query parameter

    </figcaption></figure>

<p>If you want to have more detailed explanation you can take a look at the <a href="https://docs.webcon.com/docs/2025R2/Studio/Process/APIDefinition/UDADataSource">official documentation</a>.</p>

<h2 id="benefits">Benefits</h2>
<h3 id="automatic-conversion-to-json">Automatic conversion to JSON</h3>
<p>In the past I’ve used a lot of business rules to return data from WEBCON inside HTML fields. For example for the <a href="/posts/2023/breadcrumb">breadcrumb</a> and <a href="/posts/2023/path-button-styling-revisited">automated path coloring</a>. The issue here was that I needed to use SQL to convert the values to JSON and store the result in a HTML fields. This led to issues if special characters had been used. This is something which will be handled by WEBCON automatically.</p>

<h3 id="reuse-existing-data-sources">Reuse existing data sources</h3>
<p>You can simply reuse any existing data sources, which makes it easy to have a single source of truth.</p>

<h3 id="privileges">Privileges</h3>
<p>According to the documentation the privileges of the executing user will be applied, if you use <code class="language-plaintext highlighter-rouge">BPS internal views</code>.</p>

<h1 id="inconveniences-pitfalls-and-workarounds">Inconveniences, pitfalls and workarounds</h1>
<h2 id="common-to-all-running-modes">Common to all running modes</h2>
<p>There are pitfalls which are common to all running modes which I’ve described in part 1:</p>

<ul>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#moving-api-definitions">Moving API definitions</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#time-and-timezones-are-a-never-ending-issue-in-it">Time and time zones are a never-ending issue in IT</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#logging-uda-vs-public-api">Logging UDA vs public API</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#errors">Errors</a></li>
</ul>

<h2 id="bps-internal-views-ignore-seconds">BPS internal views ignore seconds</h2>
<p>In most cases you won’t need to know the seconds, but if you do need to know them, you cannot use BPS internal views.
Even if you have a date field and not a date column of an item list the BPS internal view will just omit the seconds.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-03-32.png" alt="The BPS interal view returns 11:19 while the value in the database is 11:19:56" /><figcaption>
      The BPS interal view returns 11:19 while the value in the database is 11:19:56

    </figcaption></figure>

<p>If you need the seconds, you will have to switch to a SQL data source and format the value correctly</p>
<pre><code class="language-SQL">, Convert(varchar(25), migrationData.{WFCONCOL:4180}, 127)+'Z'
</code></pre>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-12-12.png" alt="Formatting the date time correctly in a SQL data source" /><figcaption>
      Formatting the date time correctly in a SQL data source

    </figcaption></figure>

<h2 id="time-zone-issue">Time zone issue</h2>
<p>I’ve worked around the issue that the current version completely ignores the time zones, by adding the ‘Z’ to the value. This will tell the client that the value is in UTC+0 time zone.</p>

<pre><code class="language-SQL">, Convert(varchar(25), migrationData.{WFCONCOL:4180}, 127)+'Z'
</code></pre>

<p class="notice--warning"><strong>Remark:</strong> You may need to verify this for your environment as this may have different time zone settings.</p>

<p>While this works fine for date fields this is something else for item list date time columns. They are stored in a text field, and this can be a little bit more complicated / should be checked for each case, if the values are passed from an external system.</p>

<h2 id="optional-filters">Optional filters</h2>
<p>If you sometimes want to pass a filter and sometimes you want to retrieve all information, you can use this approach:</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-19-11.png" alt="The condition resolves to true for all records if no filter value is passed." /><figcaption>
      The condition resolves to true for all records if no filter value is passed.

    </figcaption></figure>

<h2 id="1000-record-limit">1000 record limit</h2>
<h3 id="limitation">Limitation</h3>
<p>I’ve no idea how the people writing the documentation think. I would have highlighted the fixed limit of 1000 rows. At least I haven’t found an option to change this limit even if the wording suggested, that it can be changed.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-21-34.png" alt="A maximum of 1000 records can be returned" /><figcaption>
      A maximum of 1000 records can be returned

    </figcaption></figure>

<p>It would be nice if the UDA would support a paging option by default. This would save us configuration effort as we need to configure it for each UDA.</p>
<h3 id="simple-paging-for-99-of-cases">Simple paging for 99% of cases</h3>
<p>My first approach was simple:</p>
<ul>
  <li>At an order by to the SQL data source</li>
  <li>Add a parameter <code class="language-plaintext highlighter-rouge">lastReturnedInstanceId</code></li>
  <li>Add an optional filter using this filter</li>
</ul>

<p>This allowed us to get the first 1000 records, get the instance id of the last record, execute a new call and pass the last instance id. The next call would get the next page and so on.</p>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-27-10.png" alt="If `lastReturnedInstanceId` is empty the first 1000 records will be returned, otherwise those after the passed instance id" /><figcaption>
      If <code class="language-plaintext highlighter-rouge">lastReturnedInstanceId</code> is empty the first 1000 records will be returned, otherwise those after the passed instance id

    </figcaption></figure>

<h3 id="complex-paging-for-100">Complex paging for 100%</h3>
<h4 id="issue">Issue</h4>
<p>This was working fine for 99% of the cases. For one specific case it just silently failed with a 502 error. There wasn’t any other information:</p>
<ul>
  <li>UDA Logs table was empty</li>
  <li>Event log</li>
</ul>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-34-25.png" alt="I couldn&#39;t find any further information for the 502.3 error" /><figcaption>
      I couldn’t find any further information for the 502.3 error

    </figcaption></figure>

<p>In the end, I identified that a specific record caused this, if it was part of the result. Everything was fine, if I only returned this record or a limited number of elements. I have no idea why this record caused the issue. All records had similar information and if the record would have invalid characters in a value, then it shouldn’t be possible to retrieve it at all. I used a different filter where I switched from the greater than to equal operator to verify it.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-37-25.png" alt="While all records have a similar structure one of 35k records caused the GET request to fail." /><figcaption>
      While all records have a similar structure one of 35k records caused the GET request to fail.

    </figcaption></figure>

<h4 id="resolution">Resolution</h4>
<p>The first step to identifying the problematic record was to change the filtering. Instead of getting all records after the passed instance id, I changed the filter to pass an upper /lower range.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?listGuid=a09ceaeb-7fa8-4cb7-a178-c92e76c2b02e&amp;instanceIdLowerRange=26972&amp;&amp;instanceIdUpperRange=27504
</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-44-32.png" alt="" /><figcaption>
      

    </figcaption></figure>

<p>This allowed me to define the page/batch size returned by WEBCON and to narrow down on the culprit:</p>
<ol>
  <li>Get instances between 26972 and 27504 (failed)</li>
  <li>Get instances between 27250 and 27504 (worked)</li>
  <li>Get instances between 27225 and 27504 (worked)</li>
  <li>Get instances between 27200 and 27504 (failed)</li>
  <li>etc</li>
</ol>

<p>I verified that, if the batch was small enough, I could retrieve the data with the problematic record.</p>

<p>The next challenge was that I no longer could use the “start with last retrieved instance id”. After all, this will fail. I needed to know in advance which records there are in total.</p>

<p>The workaround was to create another UDA. This should return all instance ids. In addition, I needed to filter this by ListGuid, so I had to add the filtering to the UDA</p>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-10-52-52.png" alt="The UDA to return all available instance ids is applying the same filters as the actual one. " /><figcaption>
      The UDA to return all available instance ids is applying the same filters as the actual one.

    </figcaption></figure>

<p>The underlying SQL data source returns a comma separated string of all instance ids grouped by ListGuid.</p>
<pre><code class="language-SQL">        string_agg(cast(migrationData.WFD_Id as nvarchar(max)),',') as InstanceId

</code></pre>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-11-02-40.png" alt="The data source returns all instance ids as a comma separated string for a list." /><figcaption>
      The data source returns all instance ids as a comma separated string for a list.

    </figcaption></figure>

<p>I’ve opted for the comma separated string due to these reasons:</p>
<ul>
  <li>I didn’t want to add a paging mechanism again.</li>
  <li>Getting all instance ids in one request allows to create the batches on my own.</li>
  <li>A comma separated string is smaller than a JSON array.</li>
  <li>I didn’t want to use a fixed batch sizes.</li>
</ul>

<p>Let me elaborate on the last one. I could have taken the approach that I always return 200 instances and this may have worked out, but as a technician I didn’t like this.</p>
<ul>
  <li>In most cases the 1000 records had been returned fine. Why should I multiply the GET requests in all cases?</li>
  <li>Who ensures that there won’t be another case in which it fails with the defined batch size.</li>
</ul>

<p>Therefore, I started each request with 1000 instances. If this failed with a 502.3 error, the batch would be split by half, and it starts again. In the worst case, this will repeat until a single instance is returned, which is unlikely. :)</p>

<h2 id="only-data-sources-and-no-connections">Only data sources and no connections</h2>
<h3 id="bloating-the-global-data-sources">Bloating the global data sources</h3>
<p>In the UDA you have to select a data source and cannot use a connection like in a data table.</p>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-11-15-01.png" alt="You can only select data sources in a UDA" /><figcaption>
      You can only select data sources in a UDA

    </figcaption></figure>

<p>So we need to create a global data source, even if it’s used only for one use case. That’s something I don’t like.</p>

<h3 id="filtering">Filtering</h3>
<h4 id="assumption">Assumption</h4>
<p>The issue here is that the SQL data source is used as an inner select and the filter is added to the outer one (1).</p>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-2-get-data-from-data-sources/2026-06-14-11-20-43.png" alt="The generated SQL statement of a filtered data source" /><figcaption>
      The generated SQL statement of a filtered data source

    </figcaption></figure>

<p>This may be fine for simple SQL statements but for complex ones this causes an unnecessary load for the SQL server. At least this is my assumption.</p>

<h4 id="ai-explanation">AI explanation</h4>

<p>I’m copying here the final answer from AI which I got after discussing my assumption:</p>

<h5 id="sql-server-predicate-pushdown">SQL Server Predicate Pushdown</h5>

<h6 id="what-is-predicate-pushdown">What is Predicate Pushdown?</h6>

<p>When you submit a query to SQL Server, the query optimizer doesn’t always execute it exactly as written. One of its most important optimisations is <strong>predicate pushdown</strong> — moving a <code class="language-plaintext highlighter-rouge">WHERE</code> filter closer to the base table (the data source), rather than evaluating it after a larger intermediate result has been produced.</p>

<p>Consider this example:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="n">Col1</span><span class="p">,</span> <span class="n">Col2</span> <span class="k">FROM</span> <span class="n">MyTable</span>
<span class="p">)</span> <span class="k">AS</span> <span class="n">D</span>
<span class="k">WHERE</span> <span class="n">Col2</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>
</code></pre></div></div>

<p>Logically, the subquery runs first and produces a full result set, which the outer <code class="language-plaintext highlighter-rouge">WHERE</code> then filters. In practice, SQL Server rewrites this internally to:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">Col1</span><span class="p">,</span> <span class="n">Col2</span> <span class="k">FROM</span> <span class="n">MyTable</span> <span class="k">WHERE</span> <span class="n">Col2</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>
</code></pre></div></div>

<p>The filter is <em>pushed down</em> into the inner query, so fewer rows are scanned from the start. This is especially beneficial when the filtered column has an index — pushdown turns a full scan into an efficient index seek.</p>

<hr />

<h6 id="when-pushdown-fails">When Pushdown Fails</h6>

<p>There are several cases where SQL Server cannot push a predicate into an inner query. These are called <strong>unnesting inhibitors</strong>.</p>

<p>####### 1. <code class="language-plaintext highlighter-rouge">TOP</code> or <code class="language-plaintext highlighter-rouge">OFFSET-FETCH</code> in the inner query</p>

<p>SQL Server cannot safely push a filter past <code class="language-plaintext highlighter-rouge">TOP</code> or <code class="language-plaintext highlighter-rouge">OFFSET-FETCH</code> because doing so would change which rows are returned — producing incorrect results.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- The outer WHERE is STUCK — it cannot be pushed past TOP</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="n">TOP</span> <span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="n">Col1</span><span class="p">,</span> <span class="n">Col2</span> <span class="k">FROM</span> <span class="n">MyTable</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">Col1</span>
<span class="p">)</span> <span class="k">AS</span> <span class="n">D</span>
<span class="k">WHERE</span> <span class="n">Col2</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>
</code></pre></div></div>

<p>SQL Server must first materialise the top 100 rows, <em>then</em> apply the filter.</p>

<p>####### 2. Non-SARGable Predicates</p>

<p>A predicate is <strong>SARGable</strong> (Search ARGument Able) when SQL Server can use an index to satisfy it. Wrapping a column in a function makes it non-SARGable — the optimizer cannot seek on a transformed value.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Non-SARGable: full scan required, filter applied row-by-row</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">MyTable</span> <span class="k">WHERE</span> <span class="nb">YEAR</span><span class="p">(</span><span class="n">OrderDate</span><span class="p">)</span> <span class="o">=</span> <span class="mi">2023</span><span class="p">;</span>

<span class="c1">-- SARGable equivalent: index seek possible</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">MyTable</span> <span class="k">WHERE</span> <span class="n">OrderDate</span> <span class="o">&gt;=</span> <span class="s1">'2023-01-01'</span> <span class="k">AND</span> <span class="n">OrderDate</span> <span class="o">&lt;</span> <span class="s1">'2024-01-01'</span><span class="p">;</span>
</code></pre></div></div>

<p>####### 3. Scalar UDFs on the Filtered Column</p>

<p>A scalar user-defined function (UDF) blocks pushdown when the outer filter references its output, because the value doesn’t exist in the base table — it must be computed per row first.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Pushdown BLOCKED: ComputedVal is derived, not a base column</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="n">dbo</span><span class="p">.</span><span class="n">MyScalarFn</span><span class="p">(</span><span class="n">Col1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ComputedVal</span> <span class="k">FROM</span> <span class="n">MyTable</span>
<span class="p">)</span> <span class="k">AS</span> <span class="n">D</span>
<span class="k">WHERE</span> <span class="n">ComputedVal</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>
</code></pre></div></div>

<p>Additionally, scalar UDFs execute <strong>once per row</strong>, prevent parallelism, and are opaque to the optimizer. SQL Server 2019 introduced <strong>Scalar UDF Inlining</strong> (compatibility level 150), which can automatically inline simple UDFs and recover these optimisations. Check eligibility with:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="n">is_inlineable</span> <span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">sql_modules</span> <span class="k">WHERE</span> <span class="n">object_id</span> <span class="o">=</span> <span class="n">OBJECT_ID</span><span class="p">(</span><span class="s1">'dbo.MyScalarFn'</span><span class="p">);</span>
</code></pre></div></div>

<p>####### 4. Local Variables in the <code class="language-plaintext highlighter-rouge">WHERE</code> Clause</p>

<p>By default, SQL Server does not push down filters that reference local variables.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">DECLARE</span> <span class="o">@</span><span class="n">val</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>

<span class="c1">-- Pushdown may NOT occur by default</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">Col1</span> <span class="k">FROM</span> <span class="n">MyTable</span><span class="p">)</span> <span class="k">AS</span> <span class="n">D</span> <span class="k">WHERE</span> <span class="n">Col1</span> <span class="o">=</span> <span class="o">@</span><span class="n">val</span><span class="p">;</span>
</code></pre></div></div>

<p>To enable this, turn on query optimizer hotfixes at the database level:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="n">SCOPED</span> <span class="n">CONFIGURATION</span> <span class="k">SET</span> <span class="n">QUERY_OPTIMIZER_HOTFIXES</span> <span class="o">=</span> <span class="k">ON</span><span class="p">;</span>
</code></pre></div></div>

<p>####### 5. Aggregates on Non-Grouped Columns</p>

<p>When a derived table uses <code class="language-plaintext highlighter-rouge">GROUP BY</code>, an outer filter on a <em>computed aggregate</em> (like <code class="language-plaintext highlighter-rouge">SUM</code> or <code class="language-plaintext highlighter-rouge">COUNT</code>) cannot be pushed down — the aggregate must be fully evaluated first.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 'Total' is computed — pushdown blocked, full aggregation runs first</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="n">Category</span><span class="p">,</span> <span class="k">SUM</span><span class="p">(</span><span class="n">Amount</span><span class="p">)</span> <span class="k">AS</span> <span class="n">Total</span> <span class="k">FROM</span> <span class="n">Sales</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">Category</span>
<span class="p">)</span> <span class="k">AS</span> <span class="n">D</span>
<span class="k">WHERE</span> <span class="n">Total</span> <span class="o">&gt;</span> <span class="mi">1000</span><span class="p">;</span>
</code></pre></div></div>

<p>Filters on <em>grouping key columns</em> (<code class="language-plaintext highlighter-rouge">Category</code> in this example) can still be pushed down.</p>

<p>####### 6. Multiple Nested Derived Tables</p>

<p>Pushdown can work across multiple layers, but <strong>any single inhibitor in any layer blocks pushdown for everything above it</strong>. The deeper the nesting, the greater the risk.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Pushdown stops at the TOP layer — the outer WHERE cannot reach the base table</span>
<span class="k">SELECT</span> <span class="o">*</span>
<span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="o">*</span>
    <span class="k">FROM</span> <span class="p">(</span>
        <span class="k">SELECT</span> <span class="n">TOP</span> <span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="n">Col1</span><span class="p">,</span> <span class="n">Col2</span> <span class="k">FROM</span> <span class="n">MyTable</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">Col1</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="k">Inner</span>
<span class="p">)</span> <span class="k">AS</span> <span class="k">Outer</span>
<span class="k">WHERE</span> <span class="n">Col2</span> <span class="o">=</span> <span class="s1">'ABC'</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h6 id="best-practices">Best Practices</h6>

<ul>
  <li><strong>Filter early</strong>: Don’t rely on the optimizer — place <code class="language-plaintext highlighter-rouge">WHERE</code> clauses as close to the base table as possible, especially inside deeply nested queries.</li>
  <li><strong>Keep predicates SARGable</strong>: Avoid functions on columns in <code class="language-plaintext highlighter-rouge">WHERE</code> clauses; rewrite range conditions instead.</li>
  <li><strong>Avoid scalar UDFs in hot paths</strong>: Use inline table-valued functions or rewrite logic directly in the query where possible.</li>
  <li><strong>Check the execution plan</strong>: Use <code class="language-plaintext highlighter-rouge">SET STATISTICS IO ON</code> or view the actual execution plan. A <code class="language-plaintext highlighter-rouge">Filter</code> operator sitting above a scan (rather than a seek predicate) is a clear sign pushdown has not occurred.</li>
</ul>

<hr />

<h6 id="references">References</h6>

<ul>
  <li><a href="https://techcommunity.microsoft.com/blog/sqlserver/predicate-pushdown-and-why-should-i-care/385946">Predicate Pushdown and why should I care? — Microsoft Community Hub</a></li>
  <li><a href="https://sqlperformance.com/2020/07/t-sql-queries/table-expressions-part-4">Fundamentals of table expressions, Part 4 — SQLPerformance.com</a></li>
  <li><a href="https://sqlperformance.com/2013/03/t-sql-queries/the-problem-with-window-functions-and-views">The Problem with Window Functions and Views — SQLPerformance.com</a></li>
  <li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/user-defined-functions/scalar-udf-inlining">Scalar UDF Inlining — Microsoft Learn</a></li>
  <li><a href="https://learn.microsoft.com/en-us/sql/relational-databases/polybase/polybase-pushdown-computation">Pushdown Computations in PolyBase — Microsoft Learn</a></li>
</ul>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Series" /><category term="User Defined API" /><category term="API Definition" /><category term="UDA" /><summary type="html"><![CDATA[Benefits, inconveniences, pitfalls and workarounds of the UDA running mode: Get data from data source]]></summary></entry><entry><title type="html">User Defined API - Actions on a workflow instance (Part 3)</title><link href="https://daniels-notes.de/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance" rel="alternate" type="text/html" title="User Defined API - Actions on a workflow instance (Part 3)" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/user-defined-API-part-3-actions-on-a-workflow-instance</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is the third part of my blog post about User Defined APIs. This one is dedicated to running mode <code class="language-plaintext highlighter-rouge">Actions on a workflow instance</code>. I think this will be my primary option for creating workflow instances, if I don’t need a generic solution.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-06-11.png" alt="Features of the `Actions on a workflow instance` running mode" /><figcaption>
      Features of the <code class="language-plaintext highlighter-rouge">Actions on a workflow instance</code> running mode

    </figcaption></figure>

<p class="notice--info"><strong>Info:</strong>
In the first version I stated, that it’s not possible to work with attachments. This is wrong.</p>

<h1 id="series">Series</h1>

<p>If you have no idea what User Defined APIs are and why you should change this, you should start with the first part of this series:</p>
<ol>
  <li><a href="/posts/2026/user-defined-api-overview-part-1">Overview</a></li>
  <li><a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources">User Defined API - Get data from data sources</a></li>
  <li><a href="/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance">User Defined API - Actions on a workflow instance</a></li>
  <li><a href="/posts/2026/user-defined-api-part-4-execute-automation">User Defined API - Execute automation</a></li>
</ol>

<h1 id="actions-on-a-workflow-instance">Actions on a workflow instance</h1>
<h2 id="when-to-use">When to use</h2>
<p>I mentioned that I would prefer a User defined API whenever I don’t need to use a generic solution. Let me provide an example.</p>

<p>If I want to migrate the elements of a SharePoint list to workflow instance, I would use the user defined API:</p>
<ul>
  <li>Create the endpoint with meaningful names.<br />
The times where you had/wanted to use 8 characters for file names / variables are long gone. While you can infer what a path with the label ‘Reject’ means from the current state of a workflow the label ‘Reject purchase request’ is way easier to grasp, especially if you have multiple approvals in the same workflow and each has the label ‘Reject’.</li>
  <li>Use the documentation fields</li>
  <li>Create the documentation</li>
  <li>Feed AI the documentation and you are mostly done.</li>
</ul>

<p>In contrast I used the public API when I had to migrate 20 similar SharePoint list from two site collections to four different workflows. In this case it would be a pain to create all these endpoints, especially if the structures of the SharePoint lists differ from each other. Here a mapping file and the public API is way more useful. With similar I mean, that one SharePoint list uses the currency data type and the next one a free text field for the same information.</p>

<h2 id="benefits">Benefits</h2>
<h3 id="read--write-of-item-lists">Read / write of item lists</h3>
<p>When reading / writing data you can not only retrieve and update fields but also item lists. This is convenient. :)</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-18-37.png" alt="Item lists are supported." /><figcaption>
      Item lists are supported.

    </figcaption></figure>

<p>Fun fact: Seconds are returned, too. In case you don’t know why I’m mentioning this: <a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources#bps-internal-views-ignore-seconds">BPS internal views ignore seconds</a></p>

<h3 id="support-for-attachments">Support for attachments</h3>
<p>You can also access / upload attachment. All you need to do is define a collection and you will be able to select the attachments as a source.</p>

<p><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-27-08-31-32.png" alt="You can return/send attachments." /></p>

<h3 id="using-business-rules">Using business rules</h3>
<p>The best is, you are not even limited to actual fields, but you can use business rules which opens a whole bunch of possibilities.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-26-14.png" alt="You can use a business rule to return additional information." /><figcaption>
      You can use a business rule to return additional information.

    </figcaption></figure>

<p>You can use these for reading data and updating data. The latter one is like the usage of business rules to process REST action responses.</p>

<h3 id="predefined-paths">Predefined paths</h3>
<p>In contrast to the public API, you can limit the available paths. This adds an additional security level.</p>

<p><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-17-09-26.png" alt="You need to define each available path and can use a meaningful name." /></p>

<h1 id="inconveniences-pitfalls-and-workarounds">Inconveniences, pitfalls and workarounds</h1>

<h2 id="common-to-all-running-modes">Common to all running modes</h2>
<p>There are pitfalls which are common to all running modes which I’ve described in part 1:</p>

<ul>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#moving-api-definitions">Moving API definitions</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#time-and-timezones-are-a-never-ending-issue-in-it">Time and time zones are a never-ending issue in IT</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#logging-uda-vs-public-api">Logging UDA vs public API</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#errors">Errors</a></li>
</ul>

<h2 id="403-forbidden">403 Forbidden</h2>
<p>This is the one running mode which causes the most headaches in regard to setting up the privileges for OAuth authentication. This is my latest checklist:</p>
<ul>
  <li>App.UserDefAPI.ReadWrite.All permission scope</li>
  <li>Access to the application</li>
  <li>Process, workflow, form type level privileges</li>
  <li>Business entity privileges</li>
  <li>Business entity is enabled</li>
</ul>

<p>The most annoying thing here is not the 403 forbidden error, but I didn’t find a more helpful error message on the server itself. It’s state of the art, that the underlying reason is not send to the client, but some more information would be helpful on the server.</p>

<h3 id="fixed-start-a-workflow-instance-parameter">Fixed start a workflow instance parameter</h3>
<p>While I can understand that the workflow and form type are fixed. I really don’t get it that the business entity is also hard coded. Ok, I can it serves as an additional security layer and the client application doesn’t need to be aware of the id/GUID, but this also means, that you must create a separate endpoint for each business entity. I hope you don’t need to configure this for a two-digit number of entities.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-49-43.png" alt="Business entity is also hard coded." /><figcaption>
      Business entity is also hard coded.

    </figcaption></figure>

<h2 id="retrieve-data-of-multiple-instances">Retrieve data of multiple instances</h2>
<p>It’s great that this running mode also offers a GET option, but I don’t understand the current implementation. You can return a single instance, but if you want to return multiple instances, you will need to define another endpoint using the <code class="language-plaintext highlighter-rouge">Get data from data source</code> running mode. It would help a lot if the User Defined API supports OData filters.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-39-28.png" alt="If you omit the instance id, you get an error" /><figcaption>
      If you omit the instance id, you get an error

    </figcaption></figure>

<h2 id="return-data-of-business-rules">Return data of business rules</h2>
<p>This is really just an inconvenience, but if you want to return data which are not mapped to fields, you still need to select a field. At least, the system doesn’t enforce that each field can only be selected ones.</p>
<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-3-actions-on-a-workflow-instance/2026-06-14-16-41-52.png" alt="You have to select a field, even if you return data via a business rule." /><figcaption>
      You have to select a field, even if you return data via a business rule.

    </figcaption></figure>

<h2 id="no-support-for-data-rows--data-tables">No support for data rows / data tables</h2>
<p>It would be great if you could select data rows like fields and data table like item list, but this isn’t supported. At least you can work around this for data rows, by using a business rule.</p>

<h3 id="no-force-checkout-and-admin-mode">No ‘force checkout’ and ‘admin’ mode</h3>
<p>This can be an annoying limitation. Especially if you are testing. I don’t know how often I got a 409 Conflict error, because I had checked out the workflow instance in the browser. It’s the same as with those annoying timers.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Series" /><category term="User Defined API" /><category term="API Definition" /><category term="UDA" /><summary type="html"><![CDATA[Benefits, inconveniences, pitfalls and workarounds of the UDA running mode: Actions on a workflow instance]]></summary></entry><entry><title type="html">User Defined API - Execute automation (Part 4)</title><link href="https://daniels-notes.de/posts/2026/user-defined-api-part-4-execute-automation" rel="alternate" type="text/html" title="User Defined API - Execute automation (Part 4)" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/user-defined-API-part-4-execute-automation</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/user-defined-api-part-4-execute-automation"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is the last part of my blog post about User Defined APIs. This one is dedicated to running mode <code class="language-plaintext highlighter-rouge">Execute automations</code>. While the other running modes have a fixed use case, this one is way more flexible.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-56-41.png" alt="A `Execute Automation` automation should start with validation." /><figcaption>
      A <code class="language-plaintext highlighter-rouge">Execute Automation</code> automation should start with validation.

    </figcaption></figure>

<h1 id="series">Series</h1>

<p>If you have no idea what User Defined APIs are and why you should change this, you should start with the first part of this series:</p>
<ol>
  <li><a href="/posts/2026/user-defined-api-overview-part-1">Overview</a></li>
  <li><a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources">User Defined API - Get data from data sources</a></li>
  <li><a href="/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance">User Defined API - Actions on a workflow instance</a></li>
  <li><a href="/posts/2026/user-defined-api-part-4-execute-automation">User Defined API - Execute automation</a></li>
</ol>

<h1 id="execute-automation">Execute automation</h1>
<h2 id="similar-to-a-cyclical-action">Similar to a <code class="language-plaintext highlighter-rouge">Cyclical action</code></h2>
<p>This running mode is similar to a <code class="language-plaintext highlighter-rouge">Cyclical action</code>. You define an automation and have the same actions. The automation is also executed without the context of a workflow instance. The biggest difference is that it can be triggered via a REST request instead by a the workflow service. Of course, you can provide input parameters and get a response. The documentation offers an overview of the <a href="https://docs.webcon.com/docs/2026R1/Studio/Action/">available actions</a>.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-17-52-33.png" alt="A subset of the available actions." /><figcaption>
      A subset of the available actions.

    </figcaption></figure>

<h2 id="operators-break-with-error-and-end">Operators <code class="language-plaintext highlighter-rouge">Break with error</code> and <code class="language-plaintext highlighter-rouge">End</code></h2>
<p>While I have seen the operators <code class="language-plaintext highlighter-rouge">Break with error</code> and <code class="language-plaintext highlighter-rouge">End</code> in the past, I never had an idea when to use them.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-05-30.png" alt="I&#39;ve finally a use case for the `Break with error` and `End` operators." /><figcaption>
      I’ve finally a use case for the <code class="language-plaintext highlighter-rouge">Break with error</code> and <code class="language-plaintext highlighter-rouge">End</code> operators.

    </figcaption></figure>

<p>This changes when you start to get a feeling for the <code class="language-plaintext highlighter-rouge">Execute automation</code> running mode. You can compare it to a function in a programming language:</p>
<ul>
  <li>The endpoint name is similar to the function name <code class="language-plaintext highlighter-rouge">CreateAuthenticatedClientAsync</code>.</li>
  <li>You have input parameters <code class="language-plaintext highlighter-rouge">customAuthenticationParams</code>.</li>
  <li>You need to validate them and throw errors, if the validation fails <code class="language-plaintext highlighter-rouge">ArgumentNullException</code>.</li>
  <li>Depending on the context you don’t need to execute the whole function <code class="language-plaintext highlighter-rouge">return client;</code>.</li>
</ul>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">public</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">HttpClient</span><span class="p">&gt;</span> <span class="nf">CreateAuthenticatedClientAsync</span><span class="p">(</span><span class="n">CustomRestAuthenticationParams</span> <span class="n">customAuthenticationParams</span><span class="p">)</span>
  <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">customAuthenticationParams</span><span class="p">.</span><span class="n">BaseUrl</span><span class="p">))</span> 
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="n">customAuthenticationParams</span><span class="p">.</span><span class="n">BaseUrl</span><span class="p">);</span>

      <span class="k">try</span>
      <span class="p">{</span>
          <span class="k">if</span> <span class="p">(</span><span class="nf">IsTokenValid</span><span class="p">())</span>
          <span class="p">{</span>
              <span class="n">_logger</span><span class="p">.</span><span class="nf">Log</span><span class="p">(</span><span class="s">$"Reusing existing access token. It is valid for at least one minute"</span><span class="p">);</span>
              <span class="n">HttpClient</span> <span class="n">client</span> <span class="p">=</span> <span class="n">Helper</span><span class="p">.</span><span class="nf">GetClient</span><span class="p">(</span><span class="n">_logger</span><span class="p">,</span><span class="n">Context</span><span class="p">,</span><span class="n">TokenEndpoint</span><span class="p">);</span>
              <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">accessToken</span><span class="p">);</span>
              <span class="k">return</span> <span class="n">client</span><span class="p">;</span>
          <span class="p">}</span>
          <span class="p">....</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Break with error</code> and <code class="language-plaintext highlighter-rouge">End</code> operators can be used in the same way in an automation.</p>
<ol>
  <li>In case of an invalid input parameter value an error is thrown.</li>
  <li>The input parameter “return unset values” is true which enforces the early end of the automation execution.</li>
</ol>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-17-44-18.png" alt="(1) Throws an validation error while (2) prevents the further processing of the automation." /><figcaption>
      (1) Throws an validation error while (2) prevents the further processing of the automation.

    </figcaption></figure>

<h2 id="uses-cases">Uses cases</h2>
<p>Due to the flexibility this is hard to define, and I can’t offer a guideline for this. If I provide ideas here, you may subconsciously disregard other options. Maybe the best guide would be your “gut feeling”. If an implementation idea feels off, then this would be one option to think about. For example, your need to get a new document from WEBCON.</p>
<ul>
  <li>You would need to start a workflow instance,</li>
  <li>Provide information</li>
  <li>Move it to the next step, which generates the document</li>
  <li>Then you can get the attachments</li>
</ul>

<p>While it works, this may also be doable via an <code class="language-plaintext highlighter-rouge">Execute automation</code> endpoint in a single call.</p>

<h1 id="inconveniences-pitfalls-and-workarounds">Inconveniences, pitfalls and workarounds</h1>

<h2 id="common-to-all-running-modes">Common to all running modes</h2>
<p>There are pitfalls which are common to all running modes which I’ve described in part 1:</p>

<ul>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#moving-api-definitions">Moving API definitions</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#time-and-timezones-are-a-never-ending-issue-in-it">Time and time zones are a never-ending issue in IT</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#logging-uda-vs-public-api">Logging UDA vs public API</a></li>
  <li><a href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1#errors">Errors</a></li>
</ul>

<h2 id="only-post-is-supported">Only POST is supported</h2>
<p>When you start playing around with UDA you will like first test the <code class="language-plaintext highlighter-rouge">Get data from data sources</code> and <code class="language-plaintext highlighter-rouge">Actions on a workflow instance</code> running modes. These allow you to use the browser to view the data.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-02-26.png" alt="The browser can display the  `Get data from data sources` and `Actions on a workflow instance` endpoint" /><figcaption>
      The browser can display the  <code class="language-plaintext highlighter-rouge">Get data from data sources</code> and <code class="language-plaintext highlighter-rouge">Actions on a workflow instance</code> endpoint

    </figcaption></figure>

<p>After reading the documentation about the <code class="language-plaintext highlighter-rouge">Execute automation</code> I did the same, after all I could pass input parameters as query parameters here too.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-03-32.png" alt="You can pass input parameters as query parameters to an `Execute automation` endpoint." /><figcaption>
      You can pass input parameters as query parameters to an <code class="language-plaintext highlighter-rouge">Execute automation</code> endpoint.

    </figcaption></figure>

<p>Unfortunately, I just got a 400 Bad request error. The reason is that you have to use the POST method, even if you use query parameters.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-04-58.png" alt="`Execute automation` only supports POST method." /><figcaption>
      <code class="language-plaintext highlighter-rouge">Execute automation</code> only supports POST method.

    </figcaption></figure>

<h2 id="time-zones-are-lost">Time zones are lost</h2>
<p>Stumbling into the time zone issues during the migration with the UDA endpoints I wanted to test how the UDA handled those.</p>

<p>For this I created a very simple test case. I would pass a date time value to WEBCON, and it should return the value as is. Unfortunately, my concerns have been confirmed.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-13-32.png" alt="The input parameter is directly copied the output parameter." /><figcaption>
      The input parameter is directly copied the output parameter.

    </figcaption></figure>

<p>The issue is not only that the milliseconds are removed, but the time zone information is lost. In my case this resulted in a two-hour shift. Now let’s imagine that I’m not UTC+2 but UTC-2 and that this information has been posted by an external travel agency with the departure time of flight. With UTC+ I will just have to wait 2 more hours at the airport, but with UTC- the plane will have already departed.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-16-22.png" alt="The loss of the time zone information results in an incorrect time." /><figcaption>
      The loss of the time zone information results in an incorrect time.

    </figcaption></figure>

<h2 id="no-null-values">No null values</h2>
<p>I tested various options to set an output parameter to null, but neither one was working for any data type.</p>
<ol>
  <li>Return values as they are<br />
  The initial output parameters are returned.
  <img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-24-29.png" alt="" />
  The response when the initial values are returned:
  <img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-26-16.png" alt="" /></li>
  <li>Set values to empty / null <br /><br />
  <img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automation/2026-06-14-19-07-27.png" alt="" />
  The response also contains parameters which other test values. I wanted to verify whether there are any other issues.
  <img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-29-03.png" alt="" /></li>
</ol>

<p>Besides the time zone issue there’s ‘only’ the issue with returning null/unset values. Ok, in case of date time, we could workaround it, as we get <code class="language-plaintext highlighter-rouge">0001-01-01T00:00:00</code> as a value which can be interpreted as null, but there’s no option for text, decimal or boolean value.</p>

<p>Let me explain this with an example why this is an issue. If you get a provision for selling it and it hasn’t been defined yet, it makes a big difference whether there is no information or 0. In the first case the underlying program can return an error in the latter one, the 0 could be correct and you won’t get a provision.</p>

<figure class=""><img src="/assets/images/posts/2026-06-14-user-defined-API-part-4-execute-automations/2026-06-14-18-37-20.png" alt="It makes a big difference, whether your provision has been not been defined or 0 is the default value." /><figcaption>
      It makes a big difference, whether your provision has been not been defined or 0 is the default value.

    </figcaption></figure>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Series" /><category term="User Defined API" /><category term="API Definition" /><category term="UDA" /><summary type="html"><![CDATA[Benefits, inconveniences, pitfalls and workarounds of the UDA running mode: Actions on a workflow instance]]></summary></entry><entry><title type="html">User Defined API - Overview (Part 1)</title><link href="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1" rel="alternate" type="text/html" title="User Defined API - Overview (Part 1)" /><published>2026-05-30T00:00:00+00:00</published><updated>2026-05-30T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/user-defined-API-overview-part-1</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/user-defined-api-overview-part-1"><![CDATA[<h1 id="overview">Overview</h1>
<p>While User Defined APIs have been available since 2025 R2, which had been released on 2025-05-05, I have ignored them. When reading about them I thought about them as an alternative to the Public API (https://hostname/api). I have to admit, this conclusion is plainly wrong and was a mistake on my side for two reasons:</p>

<ul>
  <li>They can be executed by the user.<br />
There’s no need to make the client credentials publicly available, which would be used for the public API.</li>
  <li>AI eats API Definitions for breakfast.</li>
</ul>

<figure class=""><img src="/assets/images/posts/2026-05-30-user-defined-API-overview-part-1/2026-05-30-12-35-16.png" alt="AI eats API Definitions for breakfast." /><figcaption>
      AI eats API Definitions for breakfast.

    </figcaption></figure>

<p>There is already great documentation available:</p>

<ul>
  <li><a href="https://community.webcon.com/posts/post/user-defined-api-api-definition/545/3">Knowledge Base - User Defined API (API Definition)</a></li>
  <li><a href="https://docs.webcon.com/docs/2025R2/Studio/Process/APIDefinition/">Documentation - API Definition (General)</a>
    <ul>
      <li><a href="https://docs.webcon.com/docs/2025R2/Studio/Process/APIDefinition/UDAAutomation">Running mode - Starting automation</a></li>
      <li><a href="https://docs.webcon.com/docs/2025R2/Studio/Process/APIDefinition/UDADataSource">Running mode - Reading the data source</a></li>
      <li><a href="https://docs.webcon.com/docs/2025R2/Studio/Process/APIDefinition/UDAElement">Running mode - Actions on a workflow instance</a></li>
    </ul>
  </li>
</ul>

<p>In this post I will provide a quick overview and will on topics which are common to every running mode.</p>

<p class="notice--info"><strong>Info:</strong> The documentation and Designer Stuido use the name <code class="language-plaintext highlighter-rouge">API Definitions</code> in the community the term <code class="language-plaintext highlighter-rouge">User Defined API</code> was used. I will stick with the latter one and may also use the abbreviation UDA, which is used in the database.</p>

<h1 id="series">Series</h1>
<p>Here are the other parts of this series:</p>
<ol>
  <li><a href="/posts/2026/user-defined-api-overview-part-1">Overview</a></li>
  <li><a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources">User Defined API - Get data from data sources</a></li>
  <li><a href="/posts/2026/user-defined-api-part-3-actions-on-a-workflow-instance">User Defined API - Actions on a workflow instance</a></li>
  <li><a href="/posts/2026/user-defined-api-part-4-execute-automation">User Defined API - Execute automation</a></li>
</ol>

<h1 id="differences-between-public-api-and-user-defined-apis">Differences between Public API and User Defined APIs</h1>
<p>There are three big differences between the Public API and User Defined API. These are:</p>
<ul>
  <li>Different authentication options</li>
  <li>UDAs have a specialized use case</li>
  <li>You can create custom documentation for UDAs</li>
</ul>

<h2 id="authentication">Authentication</h2>
<p>This is the one thing I’ve overlooked when I read the knowledge base post:</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-10-53.png" alt="It&#39;s just a new authentication method which makes a big impact, but let us highlight (bold font) the other aspects which are explained in detail later anyway." /><figcaption>
      It’s just a new authentication method which makes a big impact, but let us highlight (bold font) the other aspects which are explained in detail later anyway.

    </figcaption></figure>

<p>Ok, if I would have taken a close look at the Designer Studio, I would have seen it again, but I didn’t.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-14-32.png" alt="Authentication methods for a UDA endpoint" /><figcaption>
      Authentication methods for a UDA endpoint

    </figcaption></figure>

<p>I want to make clear why this makes a difference. You can use the UDA endpoint via JavaScript <em>without any additional authentication</em>. In the past I didn’t use the Public API in forms as it uses OAuth authentication. This would require disclosing the client secret. This would be the same for the <code class="language-plaintext highlighter-rouge">API application authentication (OAuth2)</code> access option. But the other option will be executed in the context of the signed-in user. This will allow us to use a UDA endpoint from anywhere in the Portal where we can inject JavaScript:</p>
<ul>
  <li>Forms</li>
  <li>Dashboards</li>
  <li>Everywhere, if you want to go down <a href="https://daniels-notes.de/posts/2023/global-js">this road</a>.</li>
</ul>

<p>Cookie in this context means, that your user is used, even if you currently using the <code class="language-plaintext highlighter-rouge">Working on behalf</code> mode.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-10-48-19.png" alt="" /><figcaption>
      

    </figcaption></figure>

<h2 id="specialized-usage">Specialized usage</h2>
<p>The Public API is a matured all-purpose API for interacting with WEBCON from a third-party client. It will offer features which are not available in a UDA. The all-purpose approach is also the downside of it. You will need to understand how WEBCON works to use it. This is not the case with the UDA.</p>

<p>The UDA provides an abstraction layer and will make it easier for others to communicate with WEBCON. The below picture shows two bodies which update a workflow instance, and I doubt, that anyone would choose the Public API (1) option, if it’s not necessary.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-08-42-11.png" alt="Two options for updating a workflow instance (1) is the body of a Public API call (2) is the body of a UDA call." /><figcaption>
      Two options for updating a workflow instance (1) is the body of a Public API call (2) is the body of a UDA call.

    </figcaption></figure>

<p>As you have seen, the UDA will provide you with an understandable body. While this is the same for all <code class="language-plaintext highlighter-rouge">Running modes</code>, the uses cases of them are different.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-08-47-31.png" alt="UDAs offer three different running modes" /><figcaption>
      UDAs offer three different running modes

    </figcaption></figure>

<h3 id="get-data-from-the-data-source">Get data from the data source</h3>
<p>Think of this as a data table which can be accessed via REST. This data table can then be filtered using input parameters.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-08-57-54.png" alt="Running mode - Getting data from a data source" /><figcaption>
      Running mode - Getting data from a data source

    </figcaption></figure>

<p>Even so it’s simple as that, I will dedicate a separate blog post to it, because I have run into some issues and want to prevent you from running into them yourself. One important fact is that it returns a maximum of 1000 elements. While my first workaround worked for 95% of the cases, I encountered another edge case, but this and other information will be covered in the dedicated post.</p>

<h3 id="actions-on-a-workflow-instance">Actions on a workflow instance</h3>
<p>This mode allows you to:</p>
<ul>
  <li>Start a workflow instance</li>
  <li>Save it</li>
  <li>Execute a path</li>
  <li>Get a single instance</li>
</ul>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-08-58-52.png" alt="Available options executing actions on a workflow instance" /><figcaption>
      Available options executing actions on a workflow instance

    </figcaption></figure>

<p>Here the most important information I want to provide is that there’s no <code class="language-plaintext highlighter-rouge">admin mode</code> or <code class="language-plaintext highlighter-rouge">force checkout</code> option. You can only modify the editable fields. I will cover other topics in the dedicated post.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-10-50-41.png" alt="Field editing is restricted" /><figcaption>
      Field editing is restricted

    </figcaption></figure>

<h3 id="execute-automation">Execute automation</h3>
<p>This is like a <code class="language-plaintext highlighter-rouge">Cyclical action</code> which can be triggered via a REST request. The big difference here is that you can provide input parameters and get a response. Otherwise, it’s limited to the same actions, as this one is also executed without the context of a workflow instance. This will offer a lot of options and improve the user experience and therefore it will get its own post. :)</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-05-30.png" alt="I&#39;ve finally a use case for the `Break with error` and `End` operators." /><figcaption>
      I’ve finally a use case for the <code class="language-plaintext highlighter-rouge">Break with error</code> and <code class="language-plaintext highlighter-rouge">End</code> operators.

    </figcaption></figure>

<h2 id="custom-documentation">Custom documentation</h2>
<p>You can download an OpenAPI definition file for all UDAs endpoints in the process.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-50-24.png" alt="You can download the OpenAPI defintion." /><figcaption>
      You can download the OpenAPI defintion.

    </figcaption></figure>

<p>This is a technical documentation of all endpoints saved in a JSON file. There may be WEBCON power users who open it and close it after a few seconds. After all it gets technical really fast and isn’t easy to read.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-09-53-30.png" alt="A saved Open API defintion" /><figcaption>
      A saved Open API defintion

    </figcaption></figure>

<p>The mistake here is that you don’t need to understand it, but AI will. OpenAPI (swagger) is an old documentation option which is known by all AI models. If you follow these basic guidelines, then the AI can derive a context from the provided information and will be able to help you a lot.</p>
<ul>
  <li>Use speaking names<br />
<code class="language-plaintext highlighter-rouge">workflowInstanceId</code> instead of <code class="language-plaintext highlighter-rouge">id</code></li>
  <li>Provide documentation<br />
Is it clear what something does from it’s name/ will you know what something does in three months? If not, document it.</li>
</ul>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-10-01-32.png" alt="The documentation of an input parameter of an UDA endpoint" /><figcaption>
      The documentation of an input parameter of an UDA endpoint

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-10-04-20.png" alt="Well document endpoints are a treat for everyone but especially for AIs" /><figcaption>
      Well document endpoints are a treat for everyone but especially for AIs

    </figcaption></figure>

<h1 id="common-inconveniences-and-pitfalls">Common inconveniences and pitfalls</h1>
<p>With all the good options the UDA offer, there are of course inconveniences and pitfalls. It’s no wonder:</p>
<ul>
  <li>This was released one year ago</li>
  <li>I don’t remember any user voice or question in the community.</li>
  <li>There are three distinct mentions in the change logs.</li>
</ul>

<p>Based on this I would guess that the adoption of this feature probably hasn’t been high.</p>

<h2 id="moving-api-definitions">Moving API definitions</h2>
<p>API definitions with the running mode <code class="language-plaintext highlighter-rouge">Get data from the data source</code> or <code class="language-plaintext highlighter-rouge">Starting automation</code> don’t run in context of a process / workflow instance. At the same time, they adhere to the privileges of the application which is necessary when calling the API using the current user. So, there’s no option to define these on a global level, which would be better, but it would at least be helpful if we could move an endpoint to another process.</p>

<p>But there’s a workaround, but it requires fiddling with the database. Thankfully WEBCON is really forgiving if you change something in the database. :)</p>

<p>If you create the SQL statement for generating the <code class="language-plaintext highlighter-rouge">UdaDefinitions</code> table, you will notice which other tables are referenced as foreign keys. In this version the notably tables are <code class="language-plaintext highlighter-rouge">Automations</code> and <code class="language-plaintext highlighter-rouge">WFBusinessRuleDefinitions</code>.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-01-19.png" alt="Foreign key tables which also have DEFID column" /><figcaption>
      Foreign key tables which also have DEFID column

    </figcaption></figure>

<p>When you want to move an UDA from one process you will need to update the DEFID value in the relevant records.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-07-33.png" alt="You may need to update multiple record, if you want to move a UDA to another process" /><figcaption>
      You may need to update multiple record, if you want to move a UDA to another process

    </figcaption></figure>

<p>Ensure that the updated automation / business rules themselves aren’t a parent element. Otherwise, you should update the dependent rows too.</p>

<h2 id="time-and-time-zones-are-a-never-ending-issue-in-it">Time and time zones are a never-ending issue in IT</h2>

<p class="notice--warning"><strong>Remark:</strong> This applies to version 20261.1.6.198 and before. It may have been fixed in later versions.</p>

<p>If you are working with time values then make sure that you apply workaround for your cases if necessary. It will depend on the settings of the client, WEBCON and database server. In the example below I’m sending data from a client with UTC+2 to WEBCON running in UTC+2 and database is also set to UTC+2 (W.Europe Standard Time)</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-20-57.png" alt="Databse time zone of the WEBCON environment" /><figcaption>
      Databse time zone of the WEBCON environment

    </figcaption></figure>

<p>I’m sending the local time 20:27/8:27 pm (18:27:00Z) to the server which returns the same value without further processing. As you can see the local time is now 18:27/6:27 pm (16:27:00Z)</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-25-15.png" alt="The input value differs from the unprocessed input value " /><figcaption>
      The input value differs from the unprocessed input value

    </figcaption></figure>

<p>If you take a look at the payload body and response, it’s obvious, the time zone information has been removed.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-28-35.png" alt="In this version WEBCON loses the time zone information" /><figcaption>
      In this version WEBCON loses the time zone information

    </figcaption></figure>

<h2 id="logging-uda-vs-public-api">Logging UDA vs public API</h2>
<p>UDA logs are stored in the table <code class="language-plaintext highlighter-rouge">AdminUdaLogs</code> of the <em>content</em> database. The logs about the public APIs are stored in the table <code class="language-plaintext highlighter-rouge">AdminAPILogs</code> of the <em>configuration</em> database.</p>

<h2 id="errors">Errors</h2>
<p>That’s something which definitely needs to be improved. While I sometimes got a useful error message, I quite often encountered a 409 Conflict error.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-45-15.png" alt="Sometimes a generic 409 Conflict error is thrown" /><figcaption>
      Sometimes a generic 409 Conflict error is thrown

    </figcaption></figure>

<p>While you have a dedicated log in the Designer Studio per API and see the error GUID, you are as likely to get any information as not.</p>
<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-46-27.png" alt="The administration tool show no information about the error" /><figcaption>
      The administration tool show no information about the error

    </figcaption></figure>

<p>In this case you need to head over to the table <code class="language-plaintext highlighter-rouge">AdminWFEventLogs</code>in the <em>configuration</em> database and search for the error there.</p>

<figure class=""><img src="/assets/images/posts/2026-05-17-user-defined-API-part-1/2026-05-30-11-48-43.png" alt="The AdminWFEventLogs table in the *configuration* database may contain additional information." /><figcaption>
      The AdminWFEventLogs table in the <em>configuration</em> database may contain additional information.

    </figcaption></figure>

<p>Sometimes the error messages themselves will provide useful information, and the administration tools will do so, too. If you don’t get any information, it’s likely that you will find those in the configuration database. I’ve also encountered one case where there haven’t been any information, but this will be a topic in the <a href="/posts/2026/user-defined-api-part-2-get-data-from-data-sources">Get data from the data source</a> post.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Series" /><category term="User Defined API" /><category term="API Definition" /><category term="UDA" /><summary type="html"><![CDATA[User Defined APIs (API Definitions) are a feature I have greatly neglected. That will change.]]></summary></entry><entry><title type="html">Removing binary content of deleted attachments</title><link href="https://daniels-notes.de/posts/2026/remove-content-of-deleted-attachments" rel="alternate" type="text/html" title="Removing binary content of deleted attachments" /><published>2026-05-16T00:00:00+00:00</published><updated>2026-05-16T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/remove-content-of-deleted-attachments</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/remove-content-of-deleted-attachments"><![CDATA[<h1 id="overview">Overview</h1>

<p>When a user removes an attachment in the BPS Portal, or when an automation removes it without explicitly clearing the file content, the binary data stays in the database. The attachment is flagged as deleted but the bytes are never freed.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-11-18-52.png" alt="Diskspace allocated by deleted attachments." /><figcaption>
      Diskspace allocated by deleted attachments.

    </figcaption></figure>

<p>Whether this is an issue depends on your use case. If it’s a generated document, you likely don’t need those. On the other hand, it may be even an issue, that users can access a deleted version. While you can correct this for the future, you still need to handle the old documents.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-11-55-40.png" alt="A user can view and download a deleted attachment from history." /><figcaption>
      A user can view and download a deleted attachment from history.

    </figcaption></figure>

<p>WEBCON BPS has no built-in mechanism to clean this up.</p>
<blockquote>
  <p><a href="https://community.webcon.com/forum/thread/8056?messageid=8056">Delete attachments with binary data</a><br />
Konrad Keppert (WEBCON)<br />
Unfortunately, there isn’t a simple way. Once an attachment is removed, it is flagged as deleted in the database and further actions do not operate on it.
The functionality to remove binary data of deleted attachments is in backlog and I hope it will be implemented sooner than later.
As a workaround, advanced users may want to <em>manually update</em> ATF_Value to empty string or change ATT_IsDeleted to 0 and then execute an action again.</p>
</blockquote>

<p>If something is not possible with the built-in options, it’s a good topic for a blog post. :)</p>

<h1 id="removing-the-content-of-deleted-attachments">Removing the content of deleted attachments</h1>
<p>The suggested way requires modifying the database directly. We can simply clear the content directly instead of marking the file as not deleted and then trigger the action to remove the content.</p>

<p>I prepared two versions for this:</p>
<ol>
  <li>Single script <br />
  This is intended for a quick execution / limited amount of data you want to remove in a test database. This runs in a single transaction.</li>
  <li>Database backend <br />
  I’ve used this to cleanup a large amount of data in a production environment. Production environments are always special, and I wanted to have a better option to verify what would happen and what did happen. In addition, this will execute the cleanup in transaction batches, which will prevent the transaction log file grows too much.</li>
</ol>

<p>Both approaches clear the file content by setting <code class="language-plaintext highlighter-rouge">ATF_Value</code> to an empty binary value (<code class="language-plaintext highlighter-rouge">0x0</code>) and marking both the attachment and its versions as deleted. Neither approach deletes rows from the database, so no referential integrity is affected.</p>

<p class="notice--warning"><strong>Remark:</strong> If the attachment isn’t deleted I recommend to use the action <code class="language-plaintext highlighter-rouge">Remove attachment</code> and set the mode to <code class="language-plaintext highlighter-rouge">Remove only history of binary data</code>. 
  <img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-00-14.png" alt="It's possible to remove the content of old versions for non-deleted attachments." /></p>

<p class="notice--warning"><strong>Remark:</strong> These scripts modify data directly in the WEBCON BPS database. Always run in dry-run mode first, take a backup before executing, and test in a non-production environment.</p>

<h2 id="option-1-single-script">Option 1: Single script</h2>

<p>This script is suited for targeted cleanups on a smaller number of records. It uses a table variable to collect the records matching your filter, shows a preview in dry-run mode or executes the update inside a single transaction.</p>

<p>The key configuration is at the top of the script:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">declare</span> <span class="o">@</span><span class="n">dryrun</span> <span class="nb">bit</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- Set to 1 for dry run (preview), 0 for actual update</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">WHERE</code> clause controls which attachments are selected. Adjust it to match your scenario:</p>

<div class="language-sql notice--info highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">where</span> <span class="cm">/* ATT_Name like 'GeneratedDocument%' */</span>
    <span class="n">ATT_WFDID</span> <span class="o">=</span> <span class="mi">993</span>
    <span class="k">and</span> <span class="n">ATT_IsDeleted</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="k">and</span> <span class="n">datalength</span><span class="p">(</span><span class="n">ATF_Value</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>
<p><strong>Tip:</strong> Take a look at <a href="#filters">Filters</a> for a few suggestions.</p>

<p>Make sure that you update all references to the correct databases. There are three places in the script in which you need to define the content database and the attachment file database. The latter one can be different for each process. You can simply search for <code class="language-plaintext highlighter-rouge">Content</code> in the script.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-21-57.png" alt="Update the database names to match your process configuration." /><figcaption>
      Update the database names to match your process configuration.

    </figcaption></figure>

<p class="notice--info"><strong>Tip:</strong> If you have already opened up the SQL Management Studio, you can use this statement to get the attachment database name of the processes.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="n">DEF_Name</span><span class="p">,</span> <span class="n">DEF_AttachmentsDatabase</span>
<span class="k">from</span> <span class="n">WFDefinitions</span> 
</code></pre></div></div>

<p>In dry-run mode the script prints the estimated data volume and lists every record that would be affected. When you set <code class="language-plaintext highlighter-rouge">@dryrun = 0</code> the update runs in a single transaction and rolls back automatically if the affected row count does not match the expected count.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-27-27.png" alt="Result of the dry-run." /><figcaption>
      Result of the dry-run.

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-28-07.png" alt="Result of the executed script." /><figcaption>
      Result of the executed script.

    </figcaption></figure>

<p class="notice--warning"><strong>Remark:</strong> Because the update runs in a single transaction, the SQL Server transaction log will grow by at least the total size of the binary data being cleared. Verify that you have sufficient free space in the transaction log and data files before running the actual update on a large dataset. If in doubt, use Option 2.</p>

<h2 id="option-2-database-backend-approach">Option 2: Database backend approach</h2>

<p>This approach is designed for larger or long-running cleanups where you want better visibility and control. It uses a dedicated database <code class="language-plaintext highlighter-rouge">AttachmentCleanupDB</code> as a job log and processes records in configurable batches, committing each batch separately to keep the transaction log manageable.</p>

<p>The four scripts are intended to be run in order.</p>

<h3 id="script-1-setup">Script 1: Setup</h3>

<p>Run <code class="language-plaintext highlighter-rouge">AttachmentCleanupDB_1_setup.sql</code> once. It creates the <code class="language-plaintext highlighter-rouge">AttachmentCleanupDB</code> database, the <code class="language-plaintext highlighter-rouge">AttachmentCleanupLog</code> table, and two stored procedures:</p>

<ul>
  <li>
    <p>sp_AttachmentCleanup_DryRun<br />
Reads the log table and shows all pending records along with their current state in the live database.</p>
  </li>
  <li>
    <p>sp_AttachmentCleanup_BatchDelete<br />
Processes pending records in batches. Each batch is committed independently. Records that fail are marked with status <code class="language-plaintext highlighter-rouge">Error</code> and the error message is stored in the log table for review.</p>
  </li>
</ul>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-29-23.png" alt="The generated database." /><figcaption>
      The generated database.

    </figcaption></figure>

<h3 id="script-2-populate-log-table">Script 2: Populate log table</h3>

<p><code class="language-plaintext highlighter-rouge">AttachmentCleanupDB_2_populate.sql</code> queries the WEBCON BPS databases and inserts the matching attachment records into <code class="language-plaintext highlighter-rouge">AttachmentCleanupLog</code> with status <code class="language-plaintext highlighter-rouge">Pending</code>.</p>

<p>Before running the script, you need to:</p>
<ol>
  <li>Update the database names <br />
  WEBCON BPS can store attachment data in a dedicated database that is separate from the content database. These are defined by each process.</li>
  <li>Update the filter <br />
  This should match your scenario. I’ve listed a few suggestions <a href="#filters">here</a>.</li>
</ol>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-40-18.png" alt="You need to update these parts to match your environment and filter conditions." /><figcaption>
      You need to update these parts to match your environment and filter conditions.

    </figcaption></figure>

<p class="notice--warning"><strong>Remark:</strong> You need to provide the database names in the SELECT because the other stored procedures will generate a statement using these. Otherwise, we would need to modify the script each time to match the target databases.</p>

<p class="notice--info"><strong>Tip:</strong> If you already opened up the SQL Management Studio, you can use this statement to get the attachment database name of the procresses.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="n">DEF_Name</span><span class="p">,</span> <span class="n">DEF_AttachmentsDatabase</span>
<span class="k">from</span> <span class="n">WFDefinitions</span> 
</code></pre></div></div>

<p>You can run this script multiple times with different filters to add more records to the log before starting the deletion.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-44-24.png" alt="You can view the `AttachmentCleanupLog` table to verify the results." /><figcaption>
      You can view the <code class="language-plaintext highlighter-rouge">AttachmentCleanupLog</code> table to verify the results.

    </figcaption></figure>

<h3 id="script-3-dry-run">Script 3: Dry-run</h3>

<p><code class="language-plaintext highlighter-rouge">AttachmentCleanupDB_3_dryrun_proc.sql</code> calls <code class="language-plaintext highlighter-rouge">sp_AttachmentCleanup_DryRun</code>. Review the output to confirm the record list. This is the last step, to ensure that everything is fine.</p>

<ol>
  <li>You can compare the records using the file version</li>
  <li>Verify the current/new status / file size.</li>
  <li>You can view the generated statement.</li>
</ol>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-11-58-23.png" alt="The dry-run offers an option to review the files which are intended to be cleared." /><figcaption>
      The dry-run offers an option to review the files which are intended to be cleared.

    </figcaption></figure>

<h3 id="script-4-batch-delete">Script 4: Batch delete</h3>

<p><code class="language-plaintext highlighter-rouge">AttachmentCleanupDB_4_batchdelete_proc.sql</code> calls <code class="language-plaintext highlighter-rouge">sp_AttachmentCleanup_BatchDelete</code> with a configurable batch size (default 1000). Each batch commits independently, so the transaction log stays under control even for very large datasets.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">exec</span> <span class="n">sp_AttachmentCleanup_BatchDelete</span> <span class="o">@</span><span class="n">BatchSize</span> <span class="o">=</span> <span class="mi">1000</span>
</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-10-57-14.png" alt="The test executed the deletion in batches of 1 for testing purposes." /><figcaption>
      The test executed the deletion in batches of 1 for testing purposes.

    </figcaption></figure>

<p>After completion, you can check the log table. In theory individual errors for a record should be saved, but I haven’t tested it.</p>

<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-11-00-52.png" alt="The executed procedure will update the log table" /><figcaption>
      The executed procedure will update the log table

    </figcaption></figure>

<h1 id="filters">Filters</h1>

<h2 id="filtering-the-attachment-files">Filtering the attachment files</h2>
<p>Recommendations for the filter</p>
<ul>
  <li>ATT_WFDID<br />
Restrict to a specific workflow instance for testing</li>
  <li>ATT_Name <br />
If you generate documents, you can filter using the same pattern used for generating. Make sure that no custom documents are accidentally included.</li>
  <li>ATT_Description
In some processes, I’ve hidden the description in the UI, so that the users couldn’t provide a description and I set a value for all generated values.</li>
  <li>ATT_Attribute1
This stores the value of the <code class="language-plaintext highlighter-rouge">Category(folder)</code> if enabled.</li>
  <li>ATT_IsDeleted = 1
Should always be included to avoid removing versions of still existing files, at least if you don’t want to achieve just that. :)</li>
  <li>AT<em>F</em>_TsUpdated
In case you want to remove only versions which are older than two years.</li>
</ul>

<h2 id="get-all-attachments-of-form-types">Get all attachments of form types</h2>
<p>This script will return all attachments in a given process.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">declare</span> <span class="o">@</span><span class="n">defId</span> <span class="nb">int</span> <span class="o">=</span> <span class="p">(</span><span class="k">select</span> <span class="n">DEF_ID</span> <span class="k">from</span> <span class="n">WFDefinitions</span>  <span class="k">where</span> <span class="n">DEF_Guid</span> <span class="o">=</span> <span class="s1">'de6a610b-ca59-4058-9907-53f6b3887431'</span><span class="p">)</span>

<span class="k">select</span> <span class="n">WFAttachmentFiles</span><span class="p">.</span><span class="n">ATF_ATTID</span><span class="p">,</span> <span class="n">WFAttachmentFiles</span><span class="p">.</span><span class="n">ATF_Version</span><span class="p">,</span> <span class="n">WFAttachmentFiles</span><span class="p">.</span><span class="n">ATF_OrginalName</span>
<span class="k">from</span> 
  <span class="n">WFAttachmentFiles</span> 
  <span class="k">join</span> <span class="n">WFDataAttachmets</span> <span class="k">on</span> <span class="n">ATF_ATTID</span> <span class="o">=</span> <span class="n">ATT_ID</span>   
  <span class="cm">/* The V_WFElements view would be another option, but the execution will be longer because of the numerous joins */</span>
  <span class="k">join</span> <span class="n">WFElements</span> <span class="k">on</span> <span class="n">ATT_WFDID</span> <span class="o">=</span> <span class="n">WFD_ID</span>
  <span class="k">join</span> <span class="n">WFDocTypes</span> <span class="k">on</span>  <span class="n">WFD_DTYPEID</span> <span class="o">=</span> <span class="n">DTYPE_ID</span>
<span class="k">where</span> 
 <span class="n">DTYPE_DEFID</span> <span class="o">=</span> <span class="o">@</span><span class="n">defId</span>

</code></pre></div></div>

<p>You can combine the filters on the attachment files table with the form type like this</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">....</span>
<span class="k">where</span>
   <span class="n">ATT_IsDeleted</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">and</span> <span class="n">datalength</span><span class="p">(</span><span class="n">ATF_Value</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span>
   <span class="k">and</span> <span class="n">ATT_ID</span> <span class="k">in</span> <span class="p">(</span> 
    <span class="k">select</span> <span class="n">WFAttachmentFiles</span><span class="p">.</span><span class="n">ATF_ATTID</span>
    <span class="k">from</span> 
      <span class="n">WFAttachmentFiles</span> 
      <span class="k">join</span> <span class="n">WFDataAttachmets</span> <span class="k">on</span> <span class="n">ATF_ATTID</span> <span class="o">=</span> <span class="n">ATT_ID</span>   
      <span class="cm">/* The V_WFElements view would be another option, but the execution will be longer because of the numerous joins */</span>
      <span class="k">join</span> <span class="n">WFElements</span> <span class="k">on</span> <span class="n">ATT_WFDID</span> <span class="o">=</span> <span class="n">WFD_ID</span>
      <span class="k">join</span> <span class="n">WFDocTypes</span> <span class="k">on</span>  <span class="n">WFD_DTYPEID</span> <span class="o">=</span> <span class="n">DTYPE_ID</span>
    <span class="k">where</span> 
    <span class="n">DTYPE_DEFID</span> <span class="o">=</span> <span class="o">@</span><span class="n">defId</span>
   <span class="p">)</span>
</code></pre></div></div>

<h1 id="disk-space-of-deleted-attachments">Disk space of deleted attachments</h1>
<p>This SQL statement will list how much space is taken by the attachment versions. While this can be an indicator of whether you can free up some space. Make sure that the business is fine for deleting those versions.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">select</span> 
 <span class="p">(</span> <span class="k">select</span> <span class="k">sum</span><span class="p">(</span><span class="n">datalength</span><span class="p">(</span><span class="n">ATF_Value</span><span class="p">)</span><span class="o">/</span><span class="mi">1024</span><span class="o">/</span><span class="mi">1024</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span><span class="p">]</span>
  <span class="k">from</span> <span class="n">dbo</span><span class="p">.</span><span class="n">WFAttachmentFiles</span> <span class="k">join</span> <span class="n">WFDataAttachmets</span> <span class="k">on</span>  <span class="n">ATF_ATTID</span> <span class="o">=</span> <span class="n">ATT_ID</span>
  <span class="k">where</span> <span class="n">ATF_IsDeleted</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">and</span> <span class="n">ATT_IsDeleted</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span> <span class="k">of</span> <span class="n">deleted</span> <span class="n">attachments</span> <span class="n">versions</span><span class="p">,</span> <span class="n">which</span> <span class="n">files</span> <span class="n">have</span> <span class="k">not</span> <span class="n">been</span> <span class="n">deleted</span><span class="p">]</span> <span class="p">,</span>
  <span class="p">(</span> <span class="k">select</span> <span class="k">sum</span><span class="p">(</span><span class="n">datalength</span><span class="p">(</span><span class="n">ATF_Value</span><span class="p">)</span><span class="o">/</span><span class="mi">1024</span><span class="o">/</span><span class="mi">1024</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span><span class="p">]</span>
  <span class="k">from</span> <span class="n">dbo</span><span class="p">.</span><span class="n">WFAttachmentFiles</span> <span class="k">join</span> <span class="n">WFDataAttachmets</span> <span class="k">on</span>  <span class="n">ATF_ATTID</span> <span class="o">=</span> <span class="n">ATT_ID</span>
  <span class="k">where</span> <span class="n">ATF_IsDeleted</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">and</span> <span class="n">ATT_IsDeleted</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span> <span class="k">of</span> <span class="n">deleted</span> <span class="n">attachments</span> <span class="n">versions</span><span class="p">,</span> <span class="n">which</span> <span class="n">files</span> <span class="n">have</span> <span class="n">been</span> <span class="n">deleted</span><span class="p">]</span> <span class="p">,</span>
  <span class="p">(</span> <span class="k">select</span> <span class="k">sum</span><span class="p">(</span><span class="n">datalength</span><span class="p">(</span><span class="n">ATF_Value</span><span class="p">)</span><span class="o">/</span><span class="mi">1024</span><span class="o">/</span><span class="mi">1024</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span><span class="p">]</span>
  <span class="k">from</span> <span class="n">dbo</span><span class="p">.</span><span class="n">WFAttachmentFiles</span> <span class="k">join</span> <span class="n">WFDataAttachmets</span> <span class="k">on</span>  <span class="n">ATF_ATTID</span> <span class="o">=</span> <span class="n">ATT_ID</span>
  <span class="k">where</span> <span class="n">ATF_IsDeleted</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">[</span><span class="n">Total</span> <span class="n">MB</span> <span class="k">existing</span> <span class="n">attachments</span> <span class="n">versions</span><span class="p">]</span>

</code></pre></div></div>
<figure class=""><img src="/assets/images/posts/2026-05-16-remove-content-of-deleted-attachments/2026-05-16-11-30-23.png" alt="Comparision of the allocated disk space." /><figcaption>
      Comparision of the allocated disk space.

    </figcaption></figure>

<h1 id="download">Download</h1>

<p>You can find the files <a href="https://github.com/Daniel-Krueger/webcon_snippets/tree/main/removeContentOfDeletedAttachments">here</a>.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Governance" /><summary type="html"><![CDATA[Depending on the way an attachment has been deleted in WEBCON BPS, its binary content can stay in the database indefinitely. These scripts let you reclaim that space safely.]]></summary></entry><entry><title type="html">How to hange a puzzle to the wall</title><link href="https://daniels-notes.de/posts/2026/how-to-hange-a-puzzle-to-the-wall" rel="alternate" type="text/html" title="How to hange a puzzle to the wall" /><published>2026-05-10T00:00:00+00:00</published><updated>2026-05-10T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/How-to-hange-a-puzzle-to-the-wall</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/how-to-hange-a-puzzle-to-the-wall"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is a step-by-step guide on my practiced approach on how to glue a puzzle and hang it on the wall. While it’s more for my personal guide for any future puzzles, there may be others who are interested in it. It’s based on this German YouTube video <a href="https://www.youtube.com/watch?v=TfHqVkCEbfA"> DIY - Puzzle XL auf Holzplatte kleben und Rahmen selber bauen | einfach und unkompliziert | Wie man </a>. Since I’m completely unskilled when it comes to physical work I opted for a solution without a frame.</p>

<h1 id="material">Material</h1>
<p>These are the materials I used.</p>
<ul>
  <li>Latex binder <a href="https://baufan.com/de/produkte/grundieren-und-isolieren/latex-bindemittel/750-ml/">Baufan Latex-Bindemittel</a><br />
This will glue the pieces together.</li>
  <li>Press roller <br />
I’ve used such a roller for glueing the pieces.
<img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-10-58-11.png" alt="" /></li>
  <li>Wood <a href="https://www.obi.de/p/6130579/mdf-analog-e1-ungeschliffen-280-cm-x-207-cm-x-0-5-cm-braun">MDF Analog E1 Ungeschliffen </a><br />
The wood to which the puzzle will be glued. It’s 0.5 cm thick. While it’s tempting to buy it before starting the puzzle, the size on the puzzle box is an estimation. I’ve measured the final puzzle.</li>
  <li>
    <p>Wolfcraft pilot drill with countersink and depth stop <a href="https://shop.wolfcraft.com/de-de/products/p_2544_vorbohrermitsenkerundtiefenstopp">Wolfcraft Vorbohrer mit Senker und Tiefenstopp</a> <br />
Will be used to prepare the wood, so that the screws will sit flush to the wood surface. <br />
<img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-10-51-35.png" alt="" /></p>
  </li>
  <li>Wood glue <a href="https://www.ponal.de/products/central-pdp.html/ponal-classic/SAP_0201FTP14EI1/variation/395725.html">Ponal Classic</a><br />
I used the undiluted glue.</li>
  <li>Smoothing squeegee <br />
I’ve used this to spread the glue on the wood.
<img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-10-55-56.png" alt="" /></li>
  <li>Find something which you can use to press the puzzle onto the wood</li>
</ul>

<h1 id="preparation">Preparation</h1>
<h2 id="moving-the-puzzle-onto-the-wood">Moving the puzzle onto the wood</h2>
<p>At this point the puzzle is already finished, and the wood has been purchased. The next home improvement store is OBI, which also offered to cut the wood in the required dimensions. The first step was to move the completed puzzle onto the wood as I wanted to put some protection on the surface before starting with the glueing.</p>

<p>The best approach was to:</p>
<ul>
  <li>Tear down the puzzle box</li>
  <li>Place the wood on a flat angle near the puzzle</li>
  <li>Place the flat puzzle box on the edge of the wood</li>
  <li>Slide it under the puzzle for about 10 cm, this reduced the angle even further</li>
  <li><em>Pull</em> the puzzle onto the wood, depending on the size, you will need a second person</li>
</ul>

<h2 id="drilling-the-sinks-for-the-screws">Drilling the sinks for the screws</h2>
<p>I’ve opted to screw the wood with six screws to the wall. This was fine for a 6000 pieces puzzle and will surely be fine for the 3000 piece puzzle in these pictures.</p>

<p>For each screw I removed four puzzle pieces which are 3 pieces away from the border. I will use the term ‘screw puzzles’ for those. The middle has been marked for drilling. This is way easier than measuring the correct location.</p>

<p>Afterwards I protected the table, slid down the puzzle from the wood and drilled the wood.</p>

<p>I don’t have a picture from this step, but this is how it looks at the end.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-28-30.png" alt="The drilled sink after glueing the puzzle to the wood." /><figcaption>
      The drilled sink after glueing the puzzle to the wood.

    </figcaption></figure>

<h1 id="glueing-the-pieces">Glueing the pieces</h1>
<p>Maybe I’m a bit paranoid but this was a three-phase process. I used the press roller to press the Latex into the connections of the puzzle pieces. 
I’ve done this horizontally, vertically and diagonally. While the Latex looks white, it will fade to transparent over time.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-22-06.png" alt="The white Latex will fade over time" /><figcaption>
      The white Latex will fade over time

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-26-10.png" alt="Closeup of a part where the Latex hasn&#39;t been applied." /><figcaption>
      Closeup of a part where the Latex hasn’t been applied.

    </figcaption></figure>

<p>In case of the screw puzzles, I’ve done this on the front and back.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-26-39.png" alt="Glueing the screw puzzles on the front." /><figcaption>
      Glueing the screw puzzles on the front.

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-26-52.png" alt="Glueing the screw puzzles on the front." /><figcaption>
      Glueing the screw puzzles on the front.

    </figcaption></figure>

<p>I waited approximately an hour until the Latex had dried. Afterwards the puzzle will already be a single piece and can cautiously be moved. The image below shows the lifted puzzle, where the black is a shadow.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-38-48.png" alt="The black part is the shadow of the lifted puzzle." /><figcaption>
      The black part is the shadow of the lifted puzzle.

    </figcaption></figure>

<p>Then I moved the puzzle onto the floor to reuse the cover.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-46-44.png" alt="The dried Latex is no longer visible." /><figcaption>
      The dried Latex is no longer visible.

    </figcaption></figure>

<h1 id="glueing-the-puzzle-onto-the-wood">Glueing the puzzle onto the wood</h1>
<p>When I glued my first puzzle I made two mistakes:</p>
<ul>
  <li>I use a tube of glue</li>
  <li>It was a hot day</li>
</ul>

<p>Due to the dimensions of 1.2 m x 1.8 m applying the glue took way too long and it seemed to dry in some places.</p>

<p>Lessons learned:</p>
<ul>
  <li>Use a bucket of glue</li>
  <li>Pure out some glue and spread it on the wood</li>
</ul>

<p>I placed the wood on the floor, for which I used a part of the cover from before.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-32-05.png" alt="Spreading the glue on the wood" /><figcaption>
      Spreading the glue on the wood

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-36-57.png" alt="Final result of the spread glue." /><figcaption>
      Final result of the spread glue.

    </figcaption></figure>

<p>Once every part, especially the border, was covered, I placed the puzzle onto it. The next step was to press the puzzle pieces to the wood. I used the leaves of my gaming table to distribute the pressure of games I moved onto those. In the picture you will only see the table leaf and games, the puzzle is between the floor and the leaf. I also protected the table leaves by using another part of the cover to place between the puzzle and the leaves.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-49-32.png" alt="Pressing the puzzle pieces to the wood" /><figcaption>
      Pressing the puzzle pieces to the wood

    </figcaption></figure>

<p>I haven’t tested it how long it takes, but in my case, I did this in the evening and went to bed. In the morning everything the puzzle had successfully bound to the wood.</p>

<h1 id="result">Result</h1>
<p>Here are two pictures of the result, before screwing the puzzle to the wall.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-55-20.png" alt="The puzzle with the prepared sinks" /><figcaption>
      The puzzle with the prepared sinks

    </figcaption></figure>

<figure class=""><img src="/assets/images/posts/2026-05-10-How-to-hange-a-puzzle-to-the-wall/2026-05-10-11-56-24.png" alt="The top right screw puzzle has been put back into the puzzle" /><figcaption>
      The top right screw puzzle has been put back into the puzzle

    </figcaption></figure>

<p>Even so I placed the screw puzzle tightly into the puzzle, I was able to extract it with a vacuum cleaner again. This way you can also take your puzzle with you, when you are moving. :)</p>]]></content><author><name>Daniel Krüger</name></author><category term="Private" /><summary type="html"><![CDATA[A step by step guide on how to glue a puzzle on wood with material links.]]></summary></entry><entry><title type="html">Displaying a maintenance page during WEBCON BPS updates</title><link href="https://daniels-notes.de/posts/2026/maintenance-page" rel="alternate" type="text/html" title="Displaying a maintenance page during WEBCON BPS updates" /><published>2026-05-10T00:00:00+00:00</published><updated>2026-05-10T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/maintenance-page</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/maintenance-page"><![CDATA[<h1 id="overview">Overview</h1>

<p>Whenever I update a WEBCON BPS environment, users who try to open the BPS Portal run into a generic IIS error. That’s not a great experience. A dedicated maintenance page is a simple way to communicate what is happening, when the system will be back, and who to contact for urgent matters, without touching BPS at all.</p>

<p><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-20-20-24.png" alt="Display a maintenance page during a WEBCON update." /></p>

<h1 id="implementation">Implementation</h1>
<h2 id="general">General</h2>

<p>The implementation consists of two parts:</p>

<ol>
  <li>A PowerShell setup script <br />
  This creates the IIS sites and copies the bindings from the existing WEBCONBPS site.</li>
  <li>A static HTML maintenance page <br />
  Served by a separate IIS site.</li>
</ol>

<p>The files</p>

<h2 id="iis-setup-script">IIS setup script</h2>

<p>The PowerShell script needs to run once on the web server as Administrator. It reads the HTTPS binding from the existing WEBCON BPS IIS site (certificate hash, store, host header, IP address), creates a new IIS site at the configured maintenance port, assigns the same certificate, and sets <code class="language-plaintext highlighter-rouge">index.html</code> as the default document. The script accepts a few parameters for customization:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Script name: setup-maintenance-site.ps1

Parameters (all optional):
  -SourceWebsiteName      IIS site to copy the HTTPS binding from  (default: "WEBCONBPS")
  -MaintenanceWebsiteName Name for the new IIS site                (default: "MaintenancePage")
  -MaintenancePort        Idle port for the maintenance site        (default: 4443)
  -MaintenancePath        Physical path for the HTML files          (default: C:\inetpub\MaintenancePage)
</code></pre></div></div>

<p>Run with defaults:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="n">\setup-maintenance-site.ps1</span><span class="w">
</span></code></pre></div></div>

<p>Or override individual settings:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="n">\setup-maintenance-site.ps1</span><span class="w"> </span><span class="nt">-SourceWebsiteName</span><span class="w"> </span><span class="s2">"MyWEBCONSite"</span><span class="w"> </span><span class="nt">-MaintenancePort</span><span class="w"> </span><span class="nx">8443</span><span class="w">
</span></code></pre></div></div>

<p>Make sure to unblock the downloaded files from the properties dialog of the file.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-17-38-02.png" alt="Unblock the downloaded files to prevent any issues." /><figcaption>
      Unblock the downloaded files to prevent any issues.

    </figcaption></figure>

<p>Otherwise you may run into an error when executing the PowerShell script.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-17-38-21.png" alt="An error which is thrown, if you want to execute the blocked file." /><figcaption>
      An error which is thrown, if you want to execute the blocked file.

    </figcaption></figure>

<p>This is a result when running the script without any parameters:</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-18-42-22.png" alt="Script has been executed successfully." /><figcaption>
      Script has been executed successfully.

    </figcaption></figure>

<p>You can also verify the settings in the IIS.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-17-48-59.png" alt="Maintenance page has been created." /><figcaption>
      Maintenance page has been created.

    </figcaption></figure>

<p class="notice--info"><strong>Tip:</strong> Set this up right after your initial BPS environment deployment so the maintenance site is ready before you ever need it.</p>

<p class="notice--info"><strong>Remark</strong> If you execute the script a second time, the script will remove any created elements and recreate them, except the created folder in the file system.</p>

<h2 id="maintenance-page-html">Maintenance page (HTML)</h2>

<p>After running the script, copy <code class="language-plaintext highlighter-rouge">index.html</code> into <code class="language-plaintext highlighter-rouge">MaintenancePath</code>. This isn’t done automatically as you have to modify the content anyway.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-17-52-10.png" alt="Copy the index.html to the directory of the maintenance page" /><figcaption>
      Copy the index.html to the directory of the maintenance page

    </figcaption></figure>

<p>At the bottom of the file you can configure:</p>
<ol>
  <li>Supported languages</li>
  <li>Header<br />
  This value is required.</li>
  <li>A message for explaining the downtime<br />
  This value is required.</li>
  <li>Contact information<br />
  It can be left empty.</li>
  <li>A message for expected end of the downtime. <br />
  I decided against an explicit time so that you can be vague. It can be left empty.</li>
</ol>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-20-04-52.png" alt="The texts and languages of the maintenance page can be configured." /><figcaption>
      The texts and languages of the maintenance page can be configured.

    </figcaption></figure>

<p>While I said that some information is required, this only means that the page may look strange without these.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-18-12-14.png" alt="How the page looks, if only the header is defined." /><figcaption>
      How the page looks, if only the header is defined.

    </figcaption></figure>

<h1 id="usage">Usage</h1>

<p>Activating the maintenance page is quite easy</p>

<ol>
  <li>Stop the main <code class="language-plaintext highlighter-rouge">WEBCONBPS</code> IIS website.</li>
  <li>Change the <code class="language-plaintext highlighter-rouge">WEBCONBPS</code> HTTPS binding from port <strong>443</strong> → <strong>444</strong>.</li>
  <li>Change the <code class="language-plaintext highlighter-rouge">MaintenancePage</code> HTTPS binding from port <strong>4443</strong> → <strong>443</strong>.</li>
  <li>Ensure that the <code class="language-plaintext highlighter-rouge">MaintenancePage</code> is running and is not stopped from the last update.</li>
  <li>Restart the main <code class="language-plaintext highlighter-rouge">WEBCONBP</code> IIS website.</li>
</ol>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-19-06-32.png" alt="Switch the ports of the IIS sites." /><figcaption>
      Switch the ports of the IIS sites.

    </figcaption></figure>

<p>Users navigating to the default URL will now see the maintenance page.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-18-16-42.png" alt="Maintenance page is now displayed for the default URL " /><figcaption>
      Maintenance page is now displayed for the default URL

    </figcaption></figure>

<p>This will allow you to install any update, do other maintenance actions in the Portal (1) and also open the Designer Studio (2).</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-19-04-23.png" alt="Adding the port will allow you to still access the WEBCON BPS page via Browser (1) and Designer Studio (2)." /><figcaption>
      Adding the port will allow you to still access the WEBCON BPS page via Browser (1) and Designer Studio (2).

    </figcaption></figure>

<p>When you are done you can just revert the steps.</p>
<ol>
  <li>Stop the main <code class="language-plaintext highlighter-rouge">WEBCONBPS</code> IIS website.</li>
  <li>Change the <code class="language-plaintext highlighter-rouge">MaintenancePage</code> HTTPS binding from port <strong>443</strong> → <strong>4443</strong>.</li>
  <li>Change the <code class="language-plaintext highlighter-rouge">WEBCONBPS</code> HTTPS binding from port <strong>4443</strong> → <strong>443</strong>.</li>
  <li>Restart the main <code class="language-plaintext highlighter-rouge">WEBCONBP</code> IIS website.</li>
  <li>Optionally, stop the <code class="language-plaintext highlighter-rouge">MaintenancePage</code></li>
</ol>

<p>This is a short checklist:</p>
<ol>
  <li>Update the completion time in the <code class="language-plaintext highlighter-rouge">index.html</code>.</li>
  <li>Switch the ports, ensure both pages are running.</li>
  <li>Install the WEBCON BPS update.</li>
  <li>Do other maintenance.</li>
  <li>Switch the ports again.</li>
</ol>

<h1 id="remarks">Remarks</h1>
<h2 id="microsoft-entra-authentication">Microsoft Entra authentication</h2>

<p>If the WEBCON BPS environment uses Microsoft Entra ID (Azure AD) for authentication you will need to add a redirect URI with the alternative port (4443) <strong>before</strong> the maintenance window. Without it, you won’t be able to login.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://&lt;your-bps-hostname&gt;:&lt;MaintenancePort&gt;/signin-aad
</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-18-30-55.png" alt="Adding the RedirectURI to the App Registration in Microsoft Entra" /><figcaption>
      Adding the RedirectURI to the App Registration in Microsoft Entra

    </figcaption></figure>

<h2 id="infrastructure-considerations">Infrastructure considerations</h2>
<p>Depending on your network setup, additional changes may be required alongside the IIS binding swap:</p>

<ul>
  <li>Firewalls / NSGs <br />
The maintenance port (default <code class="language-plaintext highlighter-rouge">4443</code>) must be reachable from the internet if you want to access WEBCON BPS. The maintenance page would already be accessible after switching to port 443.<br />
The setup script will add a rule to allow inbound traffic 
<img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-18-46-40.png" alt="" /></li>
  <li>Reverse proxies / load balancers <br />
If BPS is fronted by a proxy, the port swap needs to happen at the right layer. Depending on your setup you may need to adjust the proxy upstream instead of (or in addition to) the IIS binding.</li>
  <li>SSL certificate <br />
We are only changing the port, we can simply reuse the same SSL certificate.</li>
</ul>

<h2 id="maintenance-banner">Maintenance banner</h2>
<p>Before entering the maintenance window you can show users an advance notification. There are two options in the WEBCON community:</p>
<ul>
  <li><a href="https://community.webcon.com/forum/thread/6817?messageid=6817">Maintenance banner</a></li>
  <li><a href="https://community.webcon.com/forum/thread/5925?messageid=5925">Schedule maintenance with notification on homepage</a></li>
</ul>

<p>This is an updated version for 2026 which can be added to the CSS:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.top-menu__content--center-node</span><span class="nd">:before</span> <span class="p">{</span>
<span class="nl">content</span><span class="p">:</span> <span class="s1">"Planned maintenance 2026-05-14"</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--colorStatusDangerStroke2</span><span class="p">);</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--fontSize36</span><span class="p">);</span>
<span class="p">}</span> 
<span class="nc">.top-menu__content--center-node</span> <span class="p">{</span>
    <span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every user should take notice of it:</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-19-45-53.png" alt="Displaying maintenance information with CSS." /><figcaption>
      Displaying maintenance information with CSS.

    </figcaption></figure>

<p>Especially if they don’t use the browser in full screen mode.</p>
<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-19-45-47.png" alt="It&#39;s really noticeable in a smaller browser window." /><figcaption>
      It’s really noticeable in a smaller browser window.

    </figcaption></figure>

<h2 id="verify-the-maintenance-information">Verify the maintenance information</h2>
<p>Of course, you can update the maintenance information and verify that maintenance page is correctly served when you use the maintenance port.</p>

<figure class=""><img src="/assets/images/posts/2026-05-10-maintenance-page/2026-05-10-19-02-24.png" alt="Ensure the correct display of the maintenance page before entering the maintenance windows using the port" /><figcaption>
      Ensure the correct display of the maintenance page before entering the maintenance windows using the port

    </figcaption></figure>

<h1 id="download">Download</h1>

<p>You can find the files <a href="https://github.com/Daniel-Krueger/webcon_snippets/tree/main/maintenancePage">here</a>.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Installation" /><category term="Governance" /><category term="User Experience" /><summary type="html"><![CDATA[Serve a branded, multilingual maintenance page to BPS Portal users during planned downtime so they know when the system will be back and whom to contact.]]></summary></entry><entry><title type="html">Local build of the blog on a new machine</title><link href="https://daniels-notes.de/posts/2026/setup-blog-on-new-machine" rel="alternate" type="text/html" title="Local build of the blog on a new machine" /><published>2026-05-09T00:00:00+00:00</published><updated>2026-05-09T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/Setup-blog-on.new-machine</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/setup-blog-on-new-machine"><![CDATA[<h1 id="overview">Overview</h1>
<p>I should have been aware that building my blog on a new machine with the latest components would work. Since I don’t want to spend numerous hours the next time again, I’m creating this guide.</p>

<h1 id="original-approach">Original approach</h1>
<h2 id="install-ruby">Install ruby</h2>
<p>From a command line search for the available Ruby versions</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>winget search Ruby
</code></pre></div></div>
<p>Install the latest version with RubyWithDevKit</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>winget install RubyInstallerTeam.RubyWithDevKit.4.0
</code></pre></div></div>
<h2 id="install-jekyll-and-bundler">Install jekyll and bundler</h2>
<p>After the installation a new command line session needs to be started. Ruby will update the PATH variable.
Install jekyll and bundler from the blog root folder.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem install jekyll bundler
</code></pre></div></div>
<h2 id="update-bundler-verion">Update bundler verion</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle update --bundler 
</code></pre></div></div>
<h2 id="install-gems">Install gems</h2>
<p>Trigger the installation of all listed gems from the blog root folder.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle install
</code></pre></div></div>

<h2 id="result">Result</h2>
<p>Happy debugging in finding all issues which are caused due to old gem versions which are not compatible with the latest Ruby/jekyll/bundler combination. One of the symptoms I’ve run into:</p>
<ul>
  <li>Gem is not compatible with a version</li>
  <li>Gem building fails</li>
  <li>According to internet searches it’s fixed in the latest version</li>
  <li>Silently an older gem version was installed which was installed because a nested gem isn’t compatible with other components</li>
</ul>

<h1 id="future-approach">Future approach</h1>
<h2 id="installation">Installation</h2>
<ol>
  <li>Install the latest versions 
  Same approach as before but an improved process to find out all issues.</li>
  <li>Define latest github-pages gem
  Search for the latest version
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  gem search github-pages
</code></pre></div>    </div>
    <p>and force the latest version in the gemfile</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  gem "github-pages", "232"
</code></pre></div>    </div>
  </li>
  <li>Execute <code class="language-plaintext highlighter-rouge">bundle install</code>
  This may output a version conflict with the installed components. I don’t have any idea how to check the supported Ruby, jekyll version without installing a version before.
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Because github-pages &gt;= 228, &lt; 232 depends on jekyll-commonmark-ghpages = 0.4.0
 and github-pages &gt;= 232 depends on jekyll-commonmark-ghpages = 0.5.1,
 github-pages &gt;= 228 requires jekyll-commonmark-ghpages = 0.4.0 OR = 0.5.1.
 And because jekyll-commonmark-ghpages &gt;= 0.2.0 depends on jekyll-commonmark ~&gt;
 1.4.0,
 github-pages &gt;= 228 requires jekyll-commonmark ~&gt; 1.4.0.
 And because jekyll-commonmark &gt;= 1.4.0 depends on commonmarker ~&gt; 0.22
 and commonmarker &gt;= 0.22.0, &lt; 1.0.0.pre depends on Ruby &gt;= 2.6, &lt; 4.0,
 github-pages &gt;= 228 requires Ruby &gt;= 2.6, &lt; 4.0.
 So, because Gemfile depends on github-pages &gt;= 230
 and current Ruby version is = 4.0.3,
 version solving has failed.
</code></pre></div>    </div>
  </li>
  <li>Install the allowed version of Ruby</li>
  <li>Execute <code class="language-plaintext highlighter-rouge">bundle install</code> again</li>
  <li>Execute <code class="language-plaintext highlighter-rouge">run_server.bat</code>file and take a look at the warnings regarding missing gems. There may be gems which are no longer installed by default.
` warning: fiddle/import is found in fiddle, which will no longer be part of the default gems starting from Ruby 4.0.0. `
    <h2 id="minimal-mistake-migration">Minimal mistake migration</h2>
    <p>In case there’s a new version of minimal mistakes:</p>
  </li>
  <li>Review the <a href="https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/">Quick-Start Guide</a> for new gem files to reference</li>
  <li>Update the remote theme version</li>
  <li>Execute <code class="language-plaintext highlighter-rouge">run_serve_trace</code> as this does not use the <code class="language-plaintext highlighter-rouge">incremental</code> flag which causes problems when updating files in the next step</li>
  <li>The files in the <code class="language-plaintext highlighter-rouge">_includes</code> and <code class="language-plaintext highlighter-rouge">_layouts</code> should be compared and updated accordingly. The <code class="language-plaintext highlighter-rouge">custom.html</code> can be ignored, it contains only custom HTML.</li>
</ol>]]></content><author><name>Daniel Krüger</name></author><category term="Private" /><summary type="html"><![CDATA[Notes for my future self on how to setup/update the blog/gems on a new windows machine.]]></summary></entry><entry><title type="html">Display actions next to an item list</title><link href="https://daniels-notes.de/posts/2026/item-list-actions" rel="alternate" type="text/html" title="Display actions next to an item list" /><published>2026-03-22T00:00:00+00:00</published><updated>2026-03-22T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/Item-list-Actions</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/item-list-actions"><![CDATA[<h1 id="overview">Overview</h1>
<p>I often have an item list, and the users want to do something with it. For example, it’s used to plan tasks and for some of those subworkflows should be spawned. This typically requires either a <a href="https://docs.webcon.com/docs/2026R1/Studio/Workflow/Step/Actions/Action_Button">Menu button</a> or a path. Since neither one is located near the item list, the user needs to be aware of it. Wouldn’t it be better to display those next to the item list?</p>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/QYS6NYqAcns?autoplay=1&amp;loop=1&amp;mute=1&amp;rel=0" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<p class="notice--info"><strong>Info:</strong> If you are confused by the strange button labels, I had to make sure that these characters don’t break anything. :)</p>

<p class="notice--warning"><strong>Remark:</strong> This won’t work in the multi-tenant SaaS environment due to the limited database access.</p>

<h1 id="implementation">Implementation</h1>
<h2 id="general">General</h2>
<p>This time there’s a slight deviation from the general approach.</p>

<p>We need:</p>
<ol>
  <li>A global business rule.</li>
  <li>A global form rule.</li>
  <li>A HTML field for the configuration</li>
  <li>A HTML field for the button generation</li>
</ol>

<p>Yes, we will need 1+x HTML fields, where x is the number of item lists for which buttons should be generated.</p>

<h2 id="global-business-rule">Global business rule</h2>

<p>You can <a href="#download">download</a> the business rule from the repository. When you are creating the business rule you need to use another data source with higher privileges than the <code class="language-plaintext highlighter-rouge">&lt;Current BPS database&gt;</code>. We need to read a table to which the bps_user doesn’t have access.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rule name: ActionButtonData
Description: 
Will return the action (menu) button data of the current workflow instance. It's limited to the information which is needed for generating the HTML buttons.
This needs to be executed with higher privileges than the `&lt;Current BPS database&gt;`

</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-19-37-23.png" alt="You need to change the `Rule data source` in the business rule." /><figcaption>
      You need to change the <code class="language-plaintext highlighter-rouge">Rule data source</code> in the business rule.

    </figcaption></figure>

<h2 id="global-form-rule">Global form rule</h2>

<p>You can <a href="#download">download</a> the JS for this form rule from the repository and paste it to the new form rule. Don’t forget to switch the type to <code class="language-plaintext highlighter-rouge">JavaScript mode</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rule name: ItemListActions
Edit mode: JavaScript mode
Description: 
The recommended approach is to load this form rule in a configuration HTML field and trigger the button generation in another field
Configuration field example:
&lt;style id="dkrItemListActionCSSContainer"&gt;&lt;/style&gt;

&lt;script&gt;
InvokeRule(ThisFormRule_594});
    dkr.itemListActions.ActionButtonDefinitionString = 'ActionButtonDataBusinessRule';
    dkr.itemListActions.ItemListActionConfiguration = {
        "ItemListId": [
            {
                "menuActionId": ActionButtonVariable,
                "isVisible": true,
                "hideMenuAction": true
            }]}
        dkr.itemListActions.init();
&lt;/script&gt;

Button generation field:
&lt;script&gt;
dkr.itemListActions.execute(0,4,ItemListId);
&lt;/script&gt;

</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-19-44-53.png" alt="The global form rule." /><figcaption>
      The global form rule.

    </figcaption></figure>

<h2 id="html-fields">HTML fields</h2>
<h3 id="why-are-there-multiple-fields">Why are there multiple fields?</h3>
<p>Before I explain the implementation let me explain why we at least two fields. I started working with a single field which is placed below the item list. This will ensure that there’s minimal impact when the logic for creating the buttons is executed.
In my test scenario, I have tabs with item lists, and I added one field for each item list below it.</p>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-19-52-03.png" alt="The button generation fields are placed below the item list." /><figcaption>
      The button generation fields are placed below the item list.

    </figcaption></figure>

<p>This was working fine, but hiding the menu buttons in the toolbar caused flickering. I prevented this by adding another HTML field which is displayed in the top panel.</p>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-19-54-10.png" alt="Adding a second field removed the flickering of the hidden menu buttons." /><figcaption>
      Adding a second field removed the flickering of the hidden menu buttons.

    </figcaption></figure>

<h3 id="configuration-field">Configuration field</h3>
<p>As mentioned, this HTML field should be placed in the top panel. This is the minimal content:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style </span><span class="na">id=</span><span class="s">"dkrItemListActionCSSContainer"</span><span class="nt">&gt;&lt;/style&gt;</span>

<span class="nt">&lt;script&gt;</span>
    <span class="nx">InvokeRule</span><span class="p">(</span><span class="err">#</span><span class="p">{</span><span class="nl">BRUX</span><span class="p">:</span><span class="mi">594</span><span class="p">:</span><span class="nx">ID</span><span class="p">}</span><span class="err">#</span><span class="p">);</span>
    <span class="nx">dkr</span><span class="p">.</span><span class="nx">itemListActions</span><span class="p">.</span><span class="nx">ActionButtonDefinitionString</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#{BRD:586}#</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">dkr</span><span class="p">.</span><span class="nx">itemListActions</span><span class="p">.</span><span class="nx">ItemListActionConfiguration</span> <span class="o">=</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">#{WFCON:812}#</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span>
                <span class="dl">"</span><span class="s2">menuActionId</span><span class="dl">"</span><span class="p">:</span> <span class="err">#</span><span class="p">{</span><span class="na">ACB</span><span class="p">:</span><span class="mi">23</span><span class="p">}</span><span class="err">#</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">isVisible</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
                <span class="dl">"</span><span class="s2">hideMenuAction</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span>
            <span class="p">}</span>
        <span class="p">]</span>
    <span class="p">}</span>
    <span class="nx">dkr</span><span class="p">.</span><span class="nx">itemListActions</span><span class="p">.</span><span class="nx">init</span><span class="p">();</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>
<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-20-07-38.png" alt="Referencing variables in the configuration." /><figcaption>
      Referencing variables in the configuration.

    </figcaption></figure>

<p>Here’s a short explanation of the content:</p>
<ul>
  <li>The <code class="language-plaintext highlighter-rouge">style</code> element will be used to hide the configured menu buttons.</li>
  <li>After invoking the global form rule, the relevant menu buttons are configured</li>
  <li>The initialization will hide the menu buttons and prepare the button generation.</li>
</ul>

<p>Don’t forget to mark the HTML field as visible in the field matrix.</p>

<p class="notice--warning"><strong>Remark:</strong> If your actions are only working in edit mode, you should activate a different HTML content in readonly mode. You could also define other actions for view mode.<br /></p>
<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-19-56-50.png" alt="" /><figcaption>
      

    </figcaption></figure>

<h3 id="button-generation-field">Button generation field</h3>
<p>This one is really simple, but it should be added below each item list. If you have multiple item list on one tab, you place one below the last item list, but this may cause flickering, when the buttons are added to the first item list. Creating multiple fields will ensure that they are generated after the item list is prepared. In case of tabs, the HTML field will ensure that the buttons are recreated when the user switches back to the item list. The only difference of the HTML content will be the passed item list id.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="nx">dkr</span><span class="p">.</span><span class="nx">itemListActions</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="err">#</span><span class="p">{</span><span class="nl">WFCON</span><span class="p">:</span><span class="mi">812</span><span class="p">}</span><span class="err">#</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>
<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-20-10-59.png" alt="Button generation field." /><figcaption>
      Button generation field.

    </figcaption></figure>

<p><strong>Remark:</strong> If your actions are only working in edit mode, you should activate a different HTML content in readonly mode. You could also define other actions for the view mode.</p>

<h2 id="usage">Usage</h2>
<p>Make sure that you don’t define a <code class="language-plaintext highlighter-rouge">Availability restriction</code> for a menu button.</p>
<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-20-15-07.png" alt="Don&#39;t use `Availability restrictions.`" /><figcaption>
      Don’t use <code class="language-plaintext highlighter-rouge">Availability restrictions.</code>

    </figcaption></figure>

<p>Otherwise, the generated button can’t execute it and will generate a silent error:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>installHook.js:1  [WARN] No action button with ID: 24 {message: 'Warning while executing rule: InvokeMenuAction'}
</code></pre></div></div>

<p>Besides this, there’s nothing to add. The user can simply click on the buttons. :)</p>

<h1 id="explanations">Explanations</h1>
<h2 id="using-menu-buttons">Using menu buttons</h2>
<p>This is the easiest way.</p>
<ul>
  <li>Configure the menu buttons as you are used to in the Designer Studio
    <ul>
      <li>Automation</li>
      <li>Icon</li>
    </ul>
  </li>
  <li>Configure the object in the configuration field
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
    "menuActionId": #{ACB:23}#,
    "isVisible": true,
    "hideMenuAction": true
}
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="custom-buttons">Custom buttons</h2>
<p>This was actually an unintended feature when I was working on minimizing the necessary configuration data for menu buttons.
These are the properties you need to define:</p>

<ul>
  <li>label <br />
If you need a multilingual label, you can use a <a href="/posts/2024/translations#business-rule-function-text">business rule</a>.</li>
  <li>icon <br />
You can use the <code class="language-plaintext highlighter-rouge">Icon</code> setting to search for an appropriate one. If you replace the spaces with underscores and use lower case, you have the icon name.</li>
  <li>isVisible <br /></li>
  <li>onClick <br />
This needs to be a function and will be executed when the button is clicked. You could also execute a form rule.</li>
</ul>

<p>Example:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="dl">"</span><span class="s2">label</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Custom JS action</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">icon</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">arrow_turn_right_up</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">isVisible</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">onClick</span><span class="dl">"</span><span class="p">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="nx">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">Custom action</span><span class="dl">"</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-20-31-30.png" alt="How to get the icon name." /><figcaption>
      How to get the icon name.

    </figcaption></figure>

<h2 id="nested-replace-functions-in-the-business-rule">Nested replace functions in the business rule</h2>
<p>I had to make sure that the data can successfully be transferred to the client and that JavaScript can process it. This required encoding special characters with their Unicode representation for the labels. I had my fair share of issues with these characters in labels when working in multilingual environments.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">,</span> <span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="k">replace</span><span class="p">(</span><span class="n">DefaultName</span><span class="p">,</span> <span class="s1">'</span><span class="se">\'</span><span class="s1">, '</span><span class="err">\</span><span class="n">u005c</span><span class="s1">'), </span><span class="se">''''</span><span class="s1">, '</span><span class="err">\</span><span class="n">u0027</span><span class="s1">'), '</span><span class="nv">"', '</span><span class="se">\u</span><span class="nv">0022'), '/', '</span><span class="se">\u</span><span class="nv">002f'), '$', '</span><span class="se">\u</span><span class="nv">0024'), '{', '</span><span class="se">\u</span><span class="nv">007b'), '}', '</span><span class="se">\u</span><span class="nv">007d'), '[', '</span><span class="se">\u</span><span class="nv">005b'), ']', '</span><span class="se">\u</span><span class="nv">005d') as DefaultName
</span></code></pre></div></div>
<h2 id="style-element-in-configuration-field"><code class="language-plaintext highlighter-rouge">Style</code> element in configuration field</h2>
<p>This will be used to add the CSS for hiding the menu buttons. When the user leaves form, the element will be destroyed. This will ensure that it does only affect the current workflow instance.</p>

<h2 id="downgrading-for-2025">Downgrading for 2025</h2>
<p>If you are interested in this for version lower than 2026 R1, you will need to change the generated button html. At least the icon classes have changed in 2026 R1.
In addition, you need to ensure that the <code class="language-plaintext highlighter-rouge">icon</code> option is used. The <code class="language-plaintext highlighter-rouge">predefined</code> setting has been removed in 2026 R1.</p>

<figure class=""><img src="/assets/images/posts/2026-03-22-Item-list-actions/2026-03-15-20-22-11.png" alt="In 2026 R1 the `Predefined` option has been removed." /><figcaption>
      In 2026 R1 the <code class="language-plaintext highlighter-rouge">Predefined</code> option has been removed.

    </figcaption></figure>

<h1 id="download">Download</h1>
<p>You can find a version of the rules for WEBCON BPS 2026 R1 <a href="https://github.com/Daniel-Krueger/webcon_snippets/tree/main/itemListActions">here</a>.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Private" /><category term="Item list" /><category term="Form rules" /><summary type="html"><![CDATA[If your user needs to interact with an item list with menu buttons, shouldn't these actions be displayed next to it?]]></summary></entry><entry><title type="html">Collapse item list groups</title><link href="https://daniels-notes.de/posts/2026/item-list-collapse-groups" rel="alternate" type="text/html" title="Collapse item list groups" /><published>2026-03-15T00:00:00+00:00</published><updated>2026-03-15T00:00:00+00:00</updated><id>https://daniels-notes.de/posts/2026/Item-list-Collapse-groups</id><content type="html" xml:base="https://daniels-notes.de/posts/2026/item-list-collapse-groups"><![CDATA[<h1 id="overview">Overview</h1>
<p>This is another little improvement for the item list. When we are activating the grouping for the item list WEBCON BPS will display the rows expanded (1). If you activated the summary for a column, you would get a summary for the group and the grand total (2).</p>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-07-57-36.png" alt="Standard rendering of item list with grouping enabled." /><figcaption>
      Standard rendering of item list with grouping enabled.

    </figcaption></figure>

<p>Depending on your use case, it would be sufficient to see the collapsed information and the grand total can be useless. For example, if you would display the quantity of different items.</p>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-04-49.png" alt="Customization options for item list with groups." /><figcaption>
      Customization options for item list with groups.

    </figcaption></figure>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/QYS6NYqAcns?autoplay=1&amp;loop=1&amp;mute=1&amp;rel=0" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<h1 id="implementation">Implementation</h1>
<h2 id="general">General</h2>
<p>I’m not sure how often I’ve written this already, but it’s the same approach as always. :)</p>

<p>We need:</p>
<ol>
  <li>A global form rule.</li>
  <li>A HTML field to pass some configuration</li>
  <li>Activate the grouping</li>
</ol>

<h2 id="global-form-rule">Global form rule</h2>

<p>You can <a href="#download">download</a> the JS for this form rule from the repository and paste it to the new form rule. Don’t forget to switch the type to <code class="language-plaintext highlighter-rouge">JavaScript mode</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Rule name: CollapseItemListGroups
Edit mode: JavaScript mode
Description: 
Provides the following features:
- option to collapse the groups of an item list by default
- Collapse/expand all groups
- Hide grand total summary row

Usage in an HTML field

&lt;script&gt;
InvokeRule({BRUX:1182:ID});
// ....execute = function (currentIteration, maxIterations, itemListId, collapseAllGroups, hideGrandTotalSummaryRow) {
// collapseAllGroups and hideGrandTotalSummaryRow are set to true by default.
// The WFCON variable is not in the correct format, to prevent any issues. 
dkr.collapseItemListsGroups.execute(0,4,{WFCON:425})
&lt;/script&gt;

</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-11-47.png" alt="The global form rule." /><figcaption>
      The global form rule.

    </figcaption></figure>

<h2 id="html-field">HTML field</h2>
<p>This is also simple, we need to load the form rule within an HTML field, call the <code class="language-plaintext highlighter-rouge">execute</code>function and pass at least the item list id.</p>

<p>The HTML content:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="nx">InvokeRule</span><span class="p">(</span><span class="err">#</span><span class="p">{</span><span class="nl">BRUX</span><span class="p">:</span><span class="mi">585</span><span class="p">:</span><span class="nx">ID</span><span class="p">}</span><span class="err">#</span><span class="p">)</span>
<span class="c1">// currentIteration, maxIterations, itemListId, collapseAllGroups, hideGrandTotalSummaryRow) </span>
<span class="c1">//dkr.collapseItemListsGroups.execute(0,4,{WFCON:425},true,true)</span>
<span class="nx">dkr</span><span class="p">.</span><span class="nx">collapseItemListsGroups</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="err">#</span><span class="p">{</span><span class="nl">WFCON</span><span class="p">:</span><span class="mi">812</span><span class="p">}</span><span class="err">#</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-19-54.png" alt="HTML field definition" /><figcaption>
      HTML field definition

    </figcaption></figure>

<p>Don’t forget to mark the HTML field as visible in the field matrix.</p>

<p>The HTML field should be placed below the Item list field. If you have an item list in a tab, you need to create multiple fields. The reason for this is, that switching to a new tab will destroy the fields in the previous one and create the new fields.</p>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-22-14.png" alt="In case of tabs, create multiple fields." /><figcaption>
      In case of tabs, create multiple fields.

    </figcaption></figure>

<h2 id="activate-grouping">Activate grouping</h2>
<p>For whatever reason, you have to activate the grouping in the form configuration of the step. I hope this will be redesigned when WEBCON has finished the transition to the web-based designer studio.</p>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-26-36.png" alt="Grouping needs to be activated for each step." /><figcaption>
      Grouping needs to be activated for each step.

    </figcaption></figure>

<h2 id="usage">Usage</h2>
<p>In this case the only ‘usable’ element is the <code class="language-plaintext highlighter-rouge">toggle all</code> icon in the top left corner.</p>

<figure class=""><img src="/assets/images/posts/2026-03-15-Collapse-item-list-groups/2026-03-14-08-33-39.png" alt="" /><figcaption>
      

    </figcaption></figure>

<h1 id="explanations">Explanations</h1>
<h2 id="parameters">Parameters</h2>

<p>The <code class="language-plaintext highlighter-rouge">execute</code> function will try a defined number of times to find the item list and execute the logic. It may take some time until it’s rendered. Placing the HTML field below the item list helps to reduce the number of executed tries</p>
<ul>
  <li>currentIteration
The current try number. This is passed as a parameter, so that the function can be called for different item lists.</li>
  <li>maxIterations
When the target element is not found after this number of tries, the script execution will stop.</li>
  <li>itemListId</li>
  <li>collapseAllGroups
The target initial state of the item list groups</li>
  <li>hideGrandTotalSummaryRow
Defines whether the grand total summary should be hidden.</li>
</ul>

<h2 id="when-do-i-use-configuration-variables-and-when-parameters">When do I use configuration variables and when parameters</h2>

<p>In my previous post <a href="/posts/2026/single-row-edit-add-new">Single row edit: Apply and add new</a> I used a property to configure multiple item lists:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="nx">InvokeRule</span><span class="p">(</span><span class="err">#</span><span class="p">{</span><span class="nl">BRUX</span><span class="p">:</span><span class="mi">1448</span><span class="p">:</span><span class="nx">ID</span><span class="p">}</span><span class="err">#</span><span class="p">);</span>

<span class="nx">dkr</span><span class="p">.</span><span class="nx">singleRowEditApplyAndAddNew</span><span class="p">.</span><span class="nx">itemLists</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span><span class="na">listId</span><span class="p">:</span> <span class="err">#</span><span class="p">{</span><span class="na">WFCON</span><span class="p">:</span><span class="mi">1132</span><span class="p">}</span><span class="err">#</span><span class="p">,</span> <span class="na">columnId</span> <span class="p">:</span> <span class="err">#</span><span class="p">{</span><span class="na">DCN</span><span class="p">:</span><span class="mi">167</span><span class="p">}</span><span class="err">#</span> <span class="p">}</span>
  <span class="p">,</span> <span class="p">{</span><span class="na">listId</span><span class="p">:</span> <span class="err">#</span><span class="p">{</span><span class="na">WFCON</span><span class="p">:</span><span class="mi">1129</span><span class="p">}</span><span class="err">#</span><span class="p">,</span> <span class="na">columnId</span> <span class="p">:</span> <span class="err">#</span><span class="p">{</span><span class="na">DCN</span><span class="p">:</span><span class="mi">158</span><span class="p">}</span><span class="err">#</span> <span class="p">}</span>
<span class="p">];</span>

<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>In this post I’m using a different approach:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="nx">InvokeRule</span><span class="p">(</span><span class="err">#</span><span class="p">{</span><span class="nl">BRUX</span><span class="p">:</span><span class="mi">585</span><span class="p">:</span><span class="nx">ID</span><span class="p">}</span><span class="err">#</span><span class="p">)</span>
<span class="c1">// currentIteration, maxIterations, itemListId, collapseAllGroups, hideGrandTotalSummaryRow) </span>
<span class="c1">//dkr.collapseItemListsGroups.execute(0,4,#{WFCON:425}#,true,true)</span>
<span class="nx">dkr</span><span class="p">.</span><span class="nx">collapseItemListsGroups</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="err">#</span><span class="p">{</span><span class="nl">WFCON</span><span class="p">:</span><span class="mi">812</span><span class="p">}</span><span class="err">#</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>The reason for this is the following. The first one doesn’t interact with an immediately available element. It needs to wait that something needs to happen. The second one should interact with the item list as soon as it is available. In addition, it needs to be executed again every time the item list is rendered again, for example if the user switches tabs.</p>

<h2 id="display-the-group-and-group-summary-in-one-row">Display the group and group summary in one row</h2>
<p>I tried to display the group summary in the same row as the group information, but I haven’t been able to achieve this in a way that it’s working reliable and in a technical way I liked. It would need support:</p>
<ul>
  <li>Initial rendering of the item list group</li>
  <li>Toggling of individual groups</li>
  <li>Toggling of all groups</li>
  <li>Working with item lists which have different numbers of columns
If someone comes up with a feasible solution, feel free to contact me.</li>
</ul>

<h1 id="download">Download</h1>
<p>You can find two different versions for WEBCON BPS 2025 and WEBCON BPS 2026 <a href="https://github.com/Daniel-Krueger/webcon_snippets/tree/main/itemListCollapseGroups">here</a>.</p>

<p>I’ve decided to split these as it turned out, that I don’t have the time to test them for different versions and make them compatible.</p>]]></content><author><name>Daniel Krüger</name></author><category term="WEBCON BPS" /><category term="Private" /><category term="Item list" /><category term="Form rules" /><summary type="html"><![CDATA[Adds the option to collapse all item list groups, set the initial state and hide the grand total summary]]></summary></entry></feed>