Here is an attempt:
In general, with an output argument (mode -), the caller must pass a variable for that argument, and the predicate would backtrack over the possible solutions, i.e. enumerate them. Next, we want also to allow the caller to pass an instantiated or partially instantiated argument: e.g. partially instantiated to filter on the enumerated solutions and only get those that match the given partial term; or maybe fully instantiated, to just check that some term constitutes a valid solution for our predicate; etc.
And that’s where steadfast (mode ?) comes into play: the annotation means exactly that we can pass a (fully or) partially instantiated argument and obtain the behaviour described above, filtering/checking and similar. But the annotation/declaration per se only expresses an intent, we need to code our predicate in such a way that it does work as intended with the partially instantiated arguments, there is no automagic there.
And that is it, a concept more than a pattern, because how to do that concretely quite depends on the specific predicate and code at hand…
That said, the following I think is the most typical example, and the simplest at that:
%! do_some(+, -) is ...
%! my_pred(+, -) is ...
my_pred(X, Y) :- do_some(X, Y), !.
must become:
%! do_some(+, -) is ...
%! my_pred(+, ?) is ...
my_pred(X, Y) :- do_some(X, Y_), !, Y = Y_.
Here is a somewhat less trivial example I have found in my code (on Gist):
%! unfold_f(+X0, ?X) is det.
%
% Unfolds all notations in formula `X0` to `X`.
%
% Unfolds recursively on subterms of `X0`
% by the first matching clause of `notation/2`.
unfold_f(X0, X) :- % (future-safe: evars)
var(X0), !, X = X0.
unfold_f(X0, X) :-
atomic(X0), !, X = X0.
unfold_f(X0, X) :-
notation(X0, X1), !,
unfold_f__do(X1, X).
unfold_f(X0, X) :-
unfold_f__do(X0, X).
% unfold_f__do(+X0, ?X) is det.
unfold_f__do(X0, X) :-
X0 =.. [F|X0s],
unfold_fs(X0s, Xs),
X =.. [F|Xs].
%! unfold_fs(+X0s, ?Xs) is det.
%
% Unfolds all formulas in list `X0s` to `Xs`.
%
% See `unfold_f/2` for more details.
unfold_fs([], []) :- !.
unfold_fs([X0|X0s], [X|Xs]) :-
unfold_f(X0, X),
unfold_fs(X0s, Xs).
These are mutually recursive, but that’s immaterial here: the point is the output argument (the second argument) is steadfast, which means these predicates will work correctly (as expected) even when called with partially instantiated arguments (modulo any bugs I have not yet seen): and you can see that the business of binding the actual output after the cut is indeed happening, just now at the leaves of the recursion, i.e. the first two clauses of unfold_f.
Now, those are all examples with det or semidet predicates, hence there is a cut involved and we can work with that. For predicates that might be multi or nondet, think e.g. a nat/1 that is supposed to enumerate but also check the “natural numbers”, we rather typically have to firstly check the type of the input, then branch on that: e.g. if it’s a var do the enumeration, if it’s a structure do something else, otherwise throw; or similar, typically in order to avoid infinite loops, i.e. no solution unifies, keeps retrying forever.
On that, things are in fact complicated by the fact that SWI, as other systems, does argument indexing, so our nat/1 would in fact not need much explicit coding (in SWI) to handle steadfastness on its argument: but argument indexing per se does not solve steadfastness in all cases, so still some thought and often some coding in order to get correct behaviour is needed…
Anyway, to reiterate, that’s all primarily conceptual (specifications and semantics), then somewhat simpler than it sounds: indeed, in my experience, once you start keeping these things in mind in your design and coding, things quickly become clearer: at least the problems, not always the solutions…